Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,382 @@
import { h, ref, computed, Transition, onBeforeUnmount, withDirectives, getCurrentInstance } from 'vue'
import QIcon from '../icon/QIcon.js'
import QSpinner from '../spinner/QSpinner.js'
import Ripple from '../../directives/ripple/Ripple.js'
import useBtn, { useBtnProps } from './use-btn.js'
import { createComponent } from '../../utils/private.create/create.js'
import { hMergeSlot } from '../../utils/private.render/render.js'
import { stop, prevent, stopAndPrevent, listenOpts } from '../../utils/event/event.js'
import { isKeyCode } from '../../utils/private.keyboard/key-composition.js'
const { passiveCapture } = listenOpts
let
touchTarget = null,
keyboardTarget = null,
mouseTarget = null
export default createComponent({
name: 'QBtn',
props: {
...useBtnProps,
percentage: Number,
darkPercentage: Boolean,
onTouchstart: [ Function, Array ]
},
emits: [ 'click', 'keydown', 'mousedown', 'keyup' ],
setup (props, { slots, emit }) {
const { proxy } = getCurrentInstance()
const {
classes, style, innerClasses,
attributes,
hasLink, linkTag, navigateOnClick,
isActionable
} = useBtn(props)
const rootRef = ref(null)
const blurTargetRef = ref(null)
let localTouchTargetEl = null, avoidMouseRipple, mouseTimer = null
const hasLabel = computed(() =>
props.label !== void 0 && props.label !== null && props.label !== ''
)
const ripple = computed(() => (
props.disable === true || props.ripple === false
? false
: {
keyCodes: hasLink.value === true ? [ 13, 32 ] : [ 13 ],
...(props.ripple === true ? {} : props.ripple)
}
))
const rippleProps = computed(() => ({ center: props.round }))
const percentageStyle = computed(() => {
const val = Math.max(0, Math.min(100, props.percentage))
return val > 0
? { transition: 'transform 0.6s', transform: `translateX(${ val - 100 }%)` }
: {}
})
const onEvents = computed(() => {
if (props.loading === true) {
return {
onMousedown: onLoadingEvt,
onTouchstart: onLoadingEvt,
onClick: onLoadingEvt,
onKeydown: onLoadingEvt,
onKeyup: onLoadingEvt
}
}
if (isActionable.value === true) {
const acc = {
onClick,
onKeydown,
onMousedown
}
if (proxy.$q.platform.has.touch === true) {
const suffix = props.onTouchstart !== void 0
? ''
: 'Passive'
acc[ `onTouchstart${ suffix }` ] = onTouchstart
}
return acc
}
return {
// needed; especially for disabled <a> tags
onClick: stopAndPrevent
}
})
const nodeProps = computed(() => ({
ref: rootRef,
class: 'q-btn q-btn-item non-selectable no-outline ' + classes.value,
style: style.value,
...attributes.value,
...onEvents.value
}))
function onClick (e) {
// is it already destroyed?
if (rootRef.value === null) return
if (e !== void 0) {
if (e.defaultPrevented === true) return
const el = document.activeElement
// focus button if it came from ENTER on form
// prevent the new submit (already done)
if (
props.type === 'submit'
&& el !== document.body
&& rootRef.value.contains(el) === false
// required for iOS and desktop Safari
&& el.contains(rootRef.value) === false
) {
e.qAvoidFocus !== true && rootRef.value.focus()
const onClickCleanup = () => {
document.removeEventListener('keydown', stopAndPrevent, true)
document.removeEventListener('keyup', onClickCleanup, passiveCapture)
rootRef.value?.removeEventListener('blur', onClickCleanup, passiveCapture)
}
document.addEventListener('keydown', stopAndPrevent, true)
document.addEventListener('keyup', onClickCleanup, passiveCapture)
rootRef.value.addEventListener('blur', onClickCleanup, passiveCapture)
}
}
navigateOnClick(e)
}
function onKeydown (e) {
// is it already destroyed?
if (rootRef.value === null) return
emit('keydown', e)
if (isKeyCode(e, [ 13, 32 ]) === true && keyboardTarget !== rootRef.value) {
keyboardTarget !== null && cleanup()
if (e.defaultPrevented !== true) {
// focus external button if the focus helper was focused before
e.qAvoidFocus !== true && rootRef.value.focus()
keyboardTarget = rootRef.value
rootRef.value.classList.add('q-btn--active')
document.addEventListener('keyup', onPressEnd, true)
rootRef.value.addEventListener('blur', onPressEnd, passiveCapture)
}
stopAndPrevent(e)
}
}
function onTouchstart (e) {
// is it already destroyed?
if (rootRef.value === null) return
emit('touchstart', e)
if (e.defaultPrevented === true) return
if (touchTarget !== rootRef.value) {
touchTarget !== null && cleanup()
touchTarget = rootRef.value
localTouchTargetEl = e.target
localTouchTargetEl.addEventListener('touchcancel', onPressEnd, passiveCapture)
localTouchTargetEl.addEventListener('touchend', onPressEnd, passiveCapture)
}
// avoid duplicated mousedown event
// triggering another early ripple
avoidMouseRipple = true
mouseTimer !== null && clearTimeout(mouseTimer)
mouseTimer = setTimeout(() => {
mouseTimer = null
avoidMouseRipple = false
}, 200)
}
function onMousedown (e) {
// is it already destroyed?
if (rootRef.value === null) return
e.qSkipRipple = avoidMouseRipple === true
emit('mousedown', e)
if (e.defaultPrevented !== true && mouseTarget !== rootRef.value) {
mouseTarget !== null && cleanup()
mouseTarget = rootRef.value
rootRef.value.classList.add('q-btn--active')
document.addEventListener('mouseup', onPressEnd, passiveCapture)
}
}
function onPressEnd (e) {
// is it already destroyed?
if (rootRef.value === null) return
// needed for IE (because it emits blur when focusing button from focus helper)
if (
e?.type === 'blur'
&& document.activeElement === rootRef.value
) return
if (e?.type === 'keyup') {
if (keyboardTarget === rootRef.value && isKeyCode(e, [ 13, 32 ]) === true) {
// for click trigger
const evt = new MouseEvent('click', e)
evt.qKeyEvent = true
e.defaultPrevented === true && prevent(evt)
e.cancelBubble === true && stop(evt)
rootRef.value.dispatchEvent(evt)
stopAndPrevent(e)
// for ripple
e.qKeyEvent = true
}
emit('keyup', e)
}
cleanup()
}
function cleanup (destroying) {
const blurTarget = blurTargetRef.value
if (
destroying !== true
&& (touchTarget === rootRef.value || mouseTarget === rootRef.value)
&& blurTarget !== null
&& blurTarget !== document.activeElement
) {
blurTarget.setAttribute('tabindex', -1)
blurTarget.focus()
}
if (touchTarget === rootRef.value) {
if (localTouchTargetEl !== null) {
localTouchTargetEl.removeEventListener('touchcancel', onPressEnd, passiveCapture)
localTouchTargetEl.removeEventListener('touchend', onPressEnd, passiveCapture)
}
touchTarget = localTouchTargetEl = null
}
if (mouseTarget === rootRef.value) {
document.removeEventListener('mouseup', onPressEnd, passiveCapture)
mouseTarget = null
}
if (keyboardTarget === rootRef.value) {
document.removeEventListener('keyup', onPressEnd, true)
rootRef.value?.removeEventListener('blur', onPressEnd, passiveCapture)
keyboardTarget = null
}
rootRef.value?.classList.remove('q-btn--active')
}
function onLoadingEvt (evt) {
stopAndPrevent(evt)
evt.qSkipRipple = true
}
onBeforeUnmount(() => {
cleanup(true)
})
// expose public methods
Object.assign(proxy, {
click: e => {
if (isActionable.value === true) {
onClick(e)
}
}
})
return () => {
let inner = []
props.icon !== void 0 && inner.push(
h(QIcon, {
name: props.icon,
left: props.stack !== true && hasLabel.value === true,
role: 'img'
})
)
hasLabel.value === true && inner.push(
h('span', { class: 'block' }, [ props.label ])
)
inner = hMergeSlot(slots.default, inner)
if (props.iconRight !== void 0 && props.round === false) {
inner.push(
h(QIcon, {
name: props.iconRight,
right: props.stack !== true && hasLabel.value === true,
role: 'img'
})
)
}
const child = [
h('span', {
class: 'q-focus-helper',
ref: blurTargetRef
})
]
if (props.loading === true && props.percentage !== void 0) {
child.push(
h('span', {
class: 'q-btn__progress absolute-full overflow-hidden' + (props.darkPercentage === true ? ' q-btn__progress--dark' : '')
}, [
h('span', {
class: 'q-btn__progress-indicator fit block',
style: percentageStyle.value
})
])
)
}
child.push(
h('span', {
class: 'q-btn__content text-center col items-center q-anchor--skip ' + innerClasses.value
}, inner)
)
props.loading !== null && child.push(
h(Transition, {
name: 'q-transition--fade'
}, () => (
props.loading === true
? [
h('span', {
key: 'loading',
class: 'absolute-full flex flex-center'
}, slots.loading !== void 0 ? slots.loading() : [ h(QSpinner) ])
]
: null
))
)
return withDirectives(
h(
linkTag.value,
nodeProps.value,
child
),
[ [
Ripple,
ripple.value,
void 0,
rippleProps.value
] ]
)
}
}
})

View file

@ -0,0 +1,106 @@
{
"mixins": [ "components/btn/use-btn" ],
"meta": {
"docsUrl": "https://v2.quasar.dev/vue-components/button"
},
"props": {
"round": {
"type": "Boolean",
"desc": "Makes a circle shaped button",
"category": "style"
},
"percentage": {
"type": "Number",
"desc": "Percentage (0.0 < x < 100.0); To be used along 'loading' prop; Display a progress bar on the background",
"category": "behavior"
},
"dark-percentage": {
"type": "Boolean",
"desc": "Progress bar on the background should have dark color; To be used along with 'percentage' and 'loading' props",
"category": "behavior"
}
},
"slots": {
"default": {
"desc": "Use for custom content, instead of relying on 'icon' and 'label' props"
},
"loading": {
"desc": "Override the default QSpinner when in 'loading' state"
}
},
"methods": {
"click": {
"desc": "Emulate click on QBtn",
"params": {
"evt": {
"extends": "evt"
}
},
"returns": null
}
},
"events": {
"click": {
"desc": "Emitted when the component is clicked",
"params": {
"evt": {
"extends": "evt",
"desc": "JS event object; If you are using route navigation ('to'/'replace' props) and you want to cancel navigation then call evt.preventDefault() synchronously in your event handler"
},
"go": {
"type": "Function",
"desc": "Available ONLY if you are using route navigation ('to'/'replace' props); When you need to control the time at which the component should trigger the route navigation then call evt.preventDefault() synchronously and then call this function at your convenience; Useful if you have async work to be done before the actual route navigation or if you want to redirect somewhere else",
"required": false,
"addedIn": "v2.9",
"params": {
"opts": {
"type": "Object",
"desc": "Optional options",
"required": false,
"definition": {
"to": {
"type": [ "String", "Object" ],
"desc": "Equivalent to Vue Router <router-link> 'to' property; Specify it explicitly otherwise it will be set with same value as component's 'to' prop",
"required": false,
"examples": [
"'/home/dashboard'",
"{ name: 'my-route-name' }"
]
},
"replace": {
"type": "Boolean",
"desc": "Equivalent to Vue Router <router-link> 'replace' property; Specify it explicitly otherwise it will be set with same value as component's 'replace' prop",
"required": false
},
"returnRouterError": {
"type": "Boolean",
"desc": "Return the router error, if any; Otherwise the returned Promise will always fulfill",
"required": false
}
}
}
},
"returns": {
"type": "Promise<any>",
"desc": "Returns the router's navigation promise"
}
}
}
},
"touchstart": { "internal": true },
"keydown": { "internal": true },
"keyup": { "internal": true },
"mousedown": { "internal": true }
}
}

View file

@ -0,0 +1,159 @@
.q-btn
display: inline-flex
flex-direction: column
align-items: stretch
position: relative
outline: 0
border: 0
vertical-align: middle
font-size: $button-font-size
line-height: $button-line-height
text-decoration: none
color: inherit
background: transparent
font-weight: $button-font-weight
text-transform: uppercase
text-align: center
width: auto
height: auto
cursor: default
padding: $button-padding
min-height: 2.572em
.q-icon, .q-spinner
font-size: $button-line-height
&.disabled
opacity: .7 !important
&:before
content: ''
display: block
position: absolute
left: 0
right: 0
top: 0
bottom: 0
border-radius: inherit
box-shadow: $button-shadow
&--actionable
cursor: pointer
&.q-btn--standard
&:before
// This places the button active raise shadow behind adjacent elements
// Active raise shadow will still be visible under adjacent transparent elements, this is ok and coherent with a desired transparency effect.
// Visible active raise shadow can be removed by specifying a background color to the button
// Visible active raise shadow can be removed by specifying a flat or outline button type
transition: box-shadow $button-transition
&:active,
&.q-btn--active
&:before
box-shadow: $button-shadow-active
&--no-uppercase
text-transform: none
&--rectangle
border-radius: $button-border-radius
&--outline
background: transparent !important
&:before
border: 1px solid currentColor
&--push
border-radius: $button-push-border-radius
&:before
border-bottom: 3px solid rgba(0,0,0,.15)
&.q-btn--actionable
transition: transform $button-transition
&:before
transition: border-width $button-transition
&:active,
&.q-btn--active
transform: translateY(2px)
&:before
border-bottom-width: 0
&--rounded
border-radius: $button-rounded-border-radius
&--round
border-radius: 50%
padding: 0
min-width: 3em
min-height: 3em
&--square
border-radius: 0
&--flat, &--outline, &--unelevated
&:before
box-shadow: none
&--dense
padding: $button-dense-padding
min-height: 2em
&.q-btn--round
padding: 0
min-height: 2.4em
min-width: 2.4em
.on-left
margin-right: 6px
.on-right
margin-left: 6px
&--fab, &--fab-mini
.q-icon
font-size: $button-fab-icon-font-size
&--fab
padding: 16px
min-height: 56px
min-width: 56px
.q-icon
margin: auto
&--fab-mini
padding: 8px
min-height: 40px
min-width: 40px
// workaround for alignment/sizing change when showing loader
&__content
transition: opacity .3s
z-index: 0
&--hidden
opacity: 0
pointer-events: none
&__progress
border-radius: inherit
z-index: 0
&-indicator
z-index: -1
transform: translateX(-100%)
background: rgba(255,255,255,.25)
&--dark
.q-btn__progress-indicator
background: rgba(0,0,0,.2)
&--flat, &--outline
.q-btn__progress-indicator
opacity: 0.2
background: currentColor

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
import QBtn from './QBtn.js'
export {
QBtn
}

View file

@ -0,0 +1,225 @@
import { computed } from 'vue'
import useAlign, { useAlignProps } from '../../composables/private.use-align/use-align.js'
import useSize, { useSizeProps } from '../../composables/private.use-size/use-size.js'
import useRouterLink, { useRouterLinkNonMatchingProps } from '../../composables/private.use-router-link/use-router-link.js'
export const btnPadding = {
none: 0,
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32
}
export const defaultSizes = {
xs: 8,
sm: 10,
md: 14,
lg: 20,
xl: 24
}
const formTypes = [ 'button', 'submit', 'reset' ]
const mediaTypeRE = /[^\s]\/[^\s]/
export const btnDesignOptions = [ 'flat', 'outline', 'push', 'unelevated' ]
export function getBtnDesign (props, defaultValue) {
if (props.flat === true) return 'flat'
if (props.outline === true) return 'outline'
if (props.push === true) return 'push'
if (props.unelevated === true) return 'unelevated'
return defaultValue
}
export function getBtnDesignAttr (props) {
const design = getBtnDesign(props)
return design !== void 0
? { [ design ]: true }
: {}
}
export const nonRoundBtnProps = {
...useSizeProps,
...useRouterLinkNonMatchingProps,
type: {
type: String,
default: 'button'
},
label: [ Number, String ],
icon: String,
iconRight: String,
...btnDesignOptions.reduce(
(acc, val) => (acc[ val ] = Boolean) && acc,
{}
),
square: Boolean,
rounded: Boolean,
glossy: Boolean,
size: String,
fab: Boolean,
fabMini: Boolean,
padding: String,
color: String,
textColor: String,
noCaps: Boolean,
noWrap: Boolean,
dense: Boolean,
tabindex: [ Number, String ],
ripple: {
type: [ Boolean, Object ],
default: true
},
align: {
...useAlignProps.align,
default: 'center'
},
stack: Boolean,
stretch: Boolean,
loading: {
type: Boolean,
default: null
},
disable: Boolean
}
export const useBtnProps = {
...nonRoundBtnProps,
round: Boolean
}
export default function (props) {
const sizeStyle = useSize(props, defaultSizes)
const alignClass = useAlign(props)
const { hasRouterLink, hasLink, linkTag, linkAttrs, navigateOnClick } = useRouterLink({
fallbackTag: 'button'
})
const style = computed(() => {
const obj = props.fab === false && props.fabMini === false
? sizeStyle.value
: {}
return props.padding !== void 0
? Object.assign({}, obj, {
padding: props.padding
.split(/\s+/)
.map(v => (v in btnPadding ? btnPadding[ v ] + 'px' : v))
.join(' '),
minWidth: '0',
minHeight: '0'
})
: obj
})
const isRounded = computed(() =>
props.rounded === true || props.fab === true || props.fabMini === true
)
const isActionable = computed(() =>
props.disable !== true && props.loading !== true
)
const tabIndex = computed(() => (
isActionable.value === true ? props.tabindex || 0 : -1
))
const design = computed(() => getBtnDesign(props, 'standard'))
const attributes = computed(() => {
const acc = { tabindex: tabIndex.value }
if (hasLink.value === true) {
Object.assign(acc, linkAttrs.value)
}
else if (formTypes.includes(props.type) === true) {
acc.type = props.type
}
if (linkTag.value === 'a') {
if (props.disable === true) {
acc[ 'aria-disabled' ] = 'true'
}
else if (acc.href === void 0) {
acc.role = 'button'
}
if (hasRouterLink.value !== true && mediaTypeRE.test(props.type) === true) {
acc.type = props.type
}
}
else if (props.disable === true) {
acc.disabled = ''
acc[ 'aria-disabled' ] = 'true'
}
if (props.loading === true && props.percentage !== void 0) {
Object.assign(acc, {
role: 'progressbar',
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': props.percentage
})
}
return acc
})
const classes = computed(() => {
let colors
if (props.color !== void 0) {
if (props.flat === true || props.outline === true) {
colors = `text-${ props.textColor || props.color }`
}
else {
colors = `bg-${ props.color } text-${ props.textColor || 'white' }`
}
}
else if (props.textColor) {
colors = `text-${ props.textColor }`
}
const shape = props.round === true
? 'round'
: `rectangle${ isRounded.value === true ? ' q-btn--rounded' : (props.square === true ? ' q-btn--square' : '') }`
return `q-btn--${ design.value } q-btn--${ shape }`
+ (colors !== void 0 ? ' ' + colors : '')
+ (isActionable.value === true ? ' q-btn--actionable q-focusable q-hoverable' : (props.disable === true ? ' disabled' : ''))
+ (props.fab === true ? ' q-btn--fab' : (props.fabMini === true ? ' q-btn--fab-mini' : ''))
+ (props.noCaps === true ? ' q-btn--no-uppercase' : '')
+ (props.dense === true ? ' q-btn--dense' : '')
+ (props.stretch === true ? ' no-border-radius self-stretch' : '')
+ (props.glossy === true ? ' glossy' : '')
+ (props.square ? ' q-btn--square' : '')
})
const innerClasses = computed(() =>
alignClass.value + (props.stack === true ? ' column' : ' row')
+ (props.noWrap === true ? ' no-wrap text-no-wrap' : '')
+ (props.loading === true ? ' q-btn__content--hidden' : '')
)
return {
classes,
style,
innerClasses,
attributes,
hasLink,
linkTag,
navigateOnClick,
isActionable
}
}

View file

@ -0,0 +1,187 @@
{
"mixins": [ "composables/private.use-size/use-size" ],
"props": {
"type":{
"type": "String",
"desc": "1) Define the button native type attribute (submit, reset, button) or 2) render component with <a> tag so you can access events even if disable or 3) Use 'href' prop and specify 'type' as a media tag",
"default": "'button'",
"examples": [
"'a'", "'submit'", "'button'", "'reset'",
"'image/png'",
"# href=\"https://quasar.dev\" target=\"_blank\""
],
"category": "general"
},
"to": {
"type": [ "String", "Object" ],
"desc": "Equivalent to Vue Router <router-link> 'to' property; Superseded by 'href' prop if used",
"examples": [
"'/home/dashboard'",
"{ name: 'my-route-name' }"
],
"category": "navigation"
},
"replace": {
"type": "Boolean",
"desc": "Equivalent to Vue Router <router-link> 'replace' property; Superseded by 'href' prop if used",
"category": "navigation"
},
"href": {
"type": "String",
"desc": "Native <a> link href attribute; Has priority over the 'to' and 'replace' props",
"examples": [ "'https://quasar.dev'", "# href=\"https://quasar.dev\" target=\"_blank\"" ],
"category": "navigation",
"addedIn": "v2.4"
},
"target": {
"type": "String",
"desc": "Native <a> link target attribute; Use it only with 'to' or 'href' props",
"examples": [ "'_blank'", "'_self'", "'_parent'", "'_top'" ],
"category": "navigation",
"addedIn": "v2.4"
},
"label":{
"type": [ "String", "Number" ],
"desc": "The text that will be shown on the button",
"examples": [ "'Button Label'" ],
"category": "content"
},
"icon": {
"extends": "icon"
},
"icon-right": {
"extends": "icon"
},
"outline": {
"type": "Boolean",
"desc": "Use 'outline' design",
"category": "style"
},
"flat": {
"type": "Boolean",
"desc": "Use 'flat' design",
"category": "style"
},
"unelevated": {
"type": "Boolean",
"desc": "Remove shadow",
"category": "style"
},
"rounded": {
"type": "Boolean",
"desc": "Applies a more prominent border-radius for a squared shape button",
"category": "style"
},
"push": {
"type": "Boolean",
"desc": "Use 'push' design",
"category": "style"
},
"square": {
"extends": "square",
"addedIn": "v2.7.6"
},
"glossy": {
"type": "Boolean",
"desc": "Applies a glossy effect",
"category": "style"
},
"fab": {
"type": "Boolean",
"desc": "Makes button size and shape to fit a Floating Action Button",
"category": "style"
},
"fab-mini": {
"type": "Boolean",
"desc": "Makes button size and shape to fit a small Floating Action Button",
"category": "style"
},
"padding": {
"type": "String",
"desc": "Apply custom padding (vertical [horizontal]); Size in CSS units, including unit name or standard size name (none|xs|sm|md|lg|xl); Also removes the min width and height when set",
"examples": [ "'16px'", "'10px 5px'", "'2rem'", "'xs'", "'md lg'", "'2px 2px 5px 7px'" ],
"category": "style"
},
"color": {
"extends": "color"
},
"text-color": {
"extends": "text-color"
},
"no-caps": {
"type": "Boolean",
"desc": "Avoid turning label text into caps (which happens by default)",
"category": "content"
},
"no-wrap": {
"type": "Boolean",
"desc": "Avoid label text wrapping",
"category": "content"
},
"dense": {
"extends": "dense"
},
"ripple": {
"extends": "ripple"
},
"tabindex": {
"extends": "tabindex"
},
"align": {
"type": "String",
"desc": "Label or content alignment",
"default": "'center'",
"values": [ "'left'", "'right'", "'center'", "'around'", "'between'", "'evenly'" ],
"category": "content"
},
"stack": {
"type": "Boolean",
"desc": "Stack icon and label vertically instead of on same line (like it is by default)",
"category": "content"
},
"stretch": {
"type": "Boolean",
"desc": "When used on flexbox parent, button will stretch to parent's height",
"category": "content"
},
"loading": {
"type": [ "Boolean", "null" ],
"default": "null",
"desc": "Put button into loading state (displays a QSpinner -- can be overridden by using a 'loading' slot)",
"category": "behavior|state"
},
"disable": {
"extends": "disable"
}
}
}

View file

@ -0,0 +1,142 @@
import { mount } from '@vue/test-utils'
import { describe, test, expect } from 'vitest'
import { defineComponent } from 'vue'
import useBtn, {
btnPadding, defaultSizes, btnDesignOptions,
useBtnProps, nonRoundBtnProps,
getBtnDesign, getBtnDesignAttr
} from './use-btn.js'
describe('[useBtn API]', () => {
describe('[Variables]', () => {
describe('[(variable)btnPadding]', () => {
test('is defined correctly', () => {
expect(btnPadding).toBeTypeOf('object')
expect(Object.keys(btnPadding)).not.toHaveLength(0)
})
})
describe('[(variable)defaultSizes]', () => {
test('is defined correctly', () => {
expect(defaultSizes).toBeTypeOf('object')
expect(Object.keys(defaultSizes)).not.toHaveLength(0)
})
})
describe('[(variable)btnDesignOptions]', () => {
test('is defined correctly', () => {
expect(btnDesignOptions).toBeTypeOf('object')
expect(Object.keys(btnDesignOptions)).not.toHaveLength(0)
})
})
describe('[(variable)useBtnProps]', () => {
test('is defined correctly', () => {
expect(useBtnProps).toBeTypeOf('object')
expect(Object.keys(useBtnProps)).not.toHaveLength(0)
})
})
describe('[(variable)nonRoundBtnProps]', () => {
test('is defined correctly', () => {
expect(nonRoundBtnProps).toBeTypeOf('object')
expect(Object.keys(nonRoundBtnProps)).not.toHaveLength(0)
})
})
})
describe('[Functions]', () => {
describe('[(function)default]', () => {
test('has correct return value', () => {
const wrapper = mount(
defineComponent({
template: '<div />',
setup () {
const result = useBtn({})
return { result }
}
})
)
expect(
wrapper.vm.result
).toStrictEqual({
classes: expect.$ref(expect.any(String)),
style: expect.$ref(expect.any(Object)),
innerClasses: expect.$ref(expect.any(String)),
attributes: expect.$ref(expect.any(Object)),
hasLink: expect.$ref(expect.any(Boolean)),
linkTag: expect.$ref(expect.any(String)),
navigateOnClick: expect.any(Function),
isActionable: expect.$ref(expect.any(Boolean))
})
})
})
describe('[(function)getBtnDesign]', () => {
test('returns correctly with single value', () => {
for (const prop of btnDesignOptions) {
expect(
getBtnDesign({ [ prop ]: true })
).toBe(prop)
expect(
getBtnDesign({ [ prop ]: true }, 'default')
).toBe(prop)
expect(
getBtnDesign({ [ prop ]: false })
).toBeUndefined()
expect(
getBtnDesign({ [ prop ]: false }, 'default')
).toBe('default')
}
})
test('returns correctly with multiple values', () => {
for (const prop of btnDesignOptions) {
const propMap = btnDesignOptions
.reduce((acc, val) => {
if (val !== prop) {
acc[ val ] = true
}
return acc
}, {})
expect(
getBtnDesign(propMap)
).not.toBe(prop)
expect(
getBtnDesign(propMap, 'default')
).not.toBe('default')
}
})
})
describe('[(function)getBtnDesignAttr]', () => {
test('has correct return value', () => {
expect(
getBtnDesignAttr({})
).toStrictEqual({})
expect(
getBtnDesignAttr({ something: true })
).toStrictEqual({})
for (const prop of btnDesignOptions) {
expect(
getBtnDesignAttr({ [ prop ]: true })
).toStrictEqual({ [ prop ]: true })
expect(
getBtnDesignAttr({ [ prop ]: false })
).toStrictEqual({})
}
})
})
})
})