Appearance
Appearance
import Button from '~/ui/Button.vue'Triggers an action on click
isPlaying, use Toggle or Switch insteadconst {
uiBlockSize = useContext().value.uiBlockSize ?? '48',
uiOvershootNone = useContext().value.uiOvershootNone,
uiRaised = useContext().value.uiRaised ?? undefined,
...props
} = defineProps<WithAttributes<{
class: {
align?: 'align-start' | 'align-stretch'
justify?: 'justify-start' | 'justify-stretch'
color?: `${'color-primary' | 'color-destructive' | 'color-default' | 'color-yellow' | 'color-green'}${'-solid' | '-ghost'}` | 'color-accent' | 'color-default'
shape?: 'shape-circle' | 'shape-square' | 'shape-field-grow' | 'shape-field-minimal' | 'shape-field-full'
}
/**
* Indicates whether an element is being updated in the background and will not respond to user interactions.
* Note: Use string 'false' to convey 'aria-busy may happen', otherwise use booleans for developer convenience.
* @see https://www.w3.org/TR/wai-aria-1.2/#aria-busy
*/
ariaBusy?: 'true' | 'false' | boolean
/**
* Indicates element that is visually and semantically current when element is part of a set of related elements
* - `true`: Current.
* - `'step'`: Current step in a multi-step process.
* - `'location'`: Current location within an environment or context.
* - `'date'`: Current date.
* - `'time'`: Current time.
* @see https://www.w3.org/TR/wai-aria-1.2/#aria-current
*/
ariaCurrent?: boolean | 'step' | 'location' | 'date' | 'time'
/**
* Move focus to this component when it is mounted. Move focus back to previous when it is unmounted.
*/
autofocus?: boolean
/**
* Use for buttons that the user expects at a certain position, for example in a toolbar or in a row of actions
* Otherwise, remove the button instead.
*/
disabled?: boolean
ariaDisabled?: 'true' | 'false' | boolean
uiBlockSize?: '24' | '40' | '48'
/**
* Use in a context where you want to prevent the default overlapping of round edges
*/
uiOvershootNone?: boolean
uiRaised?: boolean
/**
* Default to `submit` in the absence of `@click`
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#type
*/
onClick?: (e: MouseEvent) => void
} & Labeled & Activated>>()The default action of buttons is submit [mdn]. Specify an @click to trigger a custom effect.
<Button
:disabled="alerts.length === 0"
@click="alerts = []"
>
Clear all
<Badge v-if="alerts.length > 0">
{{ alerts.length }}
</Badge>
</Button>Long-running actions are indicated by a loader and an accessible aria-busy attribute.
/**
* Makes component busy while async function is running
* @param onClick
* @returns A reactive object - use with v-bind on buttons
*/
const useBusyClick = (onClick: (e: MouseEvent) => Promise<void>) => {
const props = reactive<{
ariaBusy?: 'true' | 'false'
onClick: (e: MouseEvent) => void
}>({
ariaBusy: undefined,
onClick: async (e: MouseEvent) => {
const result = onClick(e)
const makeBusy = setTimeout(() => {
props.ariaBusy = 'true'
}, 200)
try {
await result
} finally {
props.ariaBusy = 'false'
clearTimeout(makeBusy)
}
}
})
return props
}
const useAddAlert = useBusyClick(async () => {
await delay((Math.random() + 1) * 1000)
alerts.value.unshift(Date.now())
})<Button
aria-current="time"
aria-label="Add alert"
class="color-primary-solid shape-circle"
title="Add alert"
v-bind="useAddAlert"
>
<Icon
icon="bi:plus"
style="font-size: 24px;"
/>
</Button>If the user is likely to choose a certain button in a given situation, give it the autofocus attribute. This will put the focus on it so that the user can confirm it by pressing SPACE and doesn't need to navigate to it. The button will be emphasized with a contrasting outline. Primary buttons will draw the outline outside their shape for increased emphasis and visibility (A11y).
Make sure to not steal focus from a more important control, and to to visibly return focus to whare it was before after the button is gone. See the Modal component (docs) for a complete example.
Note that submit buttons (no @click) trigger when the user hits ENTER while focusing anywhere within its associated form.
Using this component
<script setup lang="ts">
import { ref, reactive } from 'vue'
import Button from '~/ui/Button.vue'
import Alert from '~/ui/Alert.vue'
import Toolbar from '~/ui/Toolbar.vue'
import Badge from '~/ui/Badge.vue'
import { Icon } from '@iconify/vue'
const alerts = ref<number[]>([])
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
// #region useBusyClick
/**
* Makes component busy while async function is running
* @param onClick
* @returns A reactive object - use with v-bind on buttons
*/
const useBusyClick = (onClick: (e: MouseEvent) => Promise<void>) => {
const props = reactive<{
ariaBusy?: 'true' | 'false'
onClick: (e: MouseEvent) => void
}>({
ariaBusy: undefined,
onClick: async (e: MouseEvent) => {
const result = onClick(e)
const makeBusy = setTimeout(() => {
props.ariaBusy = 'true'
}, 200)
try {
await result
} finally {
props.ariaBusy = 'false'
clearTimeout(makeBusy)
}
}
})
return props
}
const useAddAlert = useBusyClick(async () => {
await delay((Math.random() + 1) * 1000)
alerts.value.unshift(Date.now())
})
// #endregion useBusyClick
</script>
<template>
<Toolbar>
<!-- #region snippet -->
<!-- #region immediate -->
<Button
:disabled="alerts.length === 0"
@click="alerts = []"
>
Clear all
<Badge v-if="alerts.length > 0">
{{ alerts.length }}
</Badge>
</Button>
<!-- #endregion immediate -->
<!-- #region delayed -->
<Button
aria-current="time"
aria-label="Add alert"
class="color-primary-solid shape-circle"
title="Add alert"
v-bind="useAddAlert"
>
<Icon
icon="bi:plus"
style="font-size: 24px;"
/>
</Button>
<!-- #endregion delayed -->
<!-- #endregion snippet -->
<div
class="layout-stack"
style="position: fixed; bottom: 0; right: 0;"
>
<Alert
v-for="(date, index) in alerts"
:key="index" class="color-green-solid"
aria-label="Dismiss"
@click="alerts.splice(index, 1)"
>
{{ new Date(date).toLocaleString(undefined, { timeStyle: 'medium' }) }}
</Alert>
</div>
</Toolbar>
</template>