Skip to content

Commit

Permalink
feat: stepper accessebility (#3050)
Browse files Browse the repository at this point in the history
* add stepper aria attrs

* add keyboard focus

* trigger focus on step only by arrow keys

* fix: added stepper aria keys to i18n config

---------

Co-authored-by: Maksim Nedoshev <m0ksem1337@gmail.com>
  • Loading branch information
kushich and m0ksem authored Mar 9, 2023
1 parent bbe596a commit ce405ca
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 4 deletions.
86 changes: 84 additions & 2 deletions packages/ui/src/components/va-stepper/VaStepper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@
<div
class="va-stepper"
:class="{ 'va-stepper--vertical': $props.vertical }"
v-bind="ariaAttributesComputed"
>
<ol
class="va-stepper__navigation"
ref="stepperNavigation"
:class="{ 'va-stepper__navigation--vertical': $props.vertical }"

@click="onNavigationValueChange()"
@keyup.enter="onNavigationValueChange()"
@keyup.space="onNavigationValueChange()"
@keyup.left="onArrowKeyPress('prev')"
@keyup.up="onArrowKeyPress('prev')"
@keyup.right="onArrowKeyPress('next')"
@keyup.down="onArrowKeyPress('next')"
@focusout="resetFocus"
>
<template
v-for="(step, i) in $props.steps"
Expand All @@ -19,6 +30,7 @@
<span
class="va-stepper__divider"
:class="{ 'va-stepper__divider--vertical': $props.vertical }"
aria-hidden="true"
/>
</slot>

Expand All @@ -34,6 +46,8 @@
:step="step"
:stepControls="stepControls"
:navigationDisabled="navigationDisabled"

:focus="focusedStep"
/>
</slot>
</template>
Expand Down Expand Up @@ -74,8 +88,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, Ref } from 'vue'
import { useColors, useStateful, useStatefulProps } from '../../composables'
import { computed, defineComponent, nextTick, PropType, ref, Ref, shallowRef, watch } from 'vue'
import { useColors, useStateful, useStatefulProps, useTranslation } from '../../composables'
import type { Step, StepControls } from './types'
import VaStepperControls from './VaStepperControls.vue'
import VaStepperStepButton from './VaStepperStepButton.vue'
Expand All @@ -100,18 +114,70 @@ export default defineComponent({
},
emits: ['update:modelValue', 'finish'],
setup (props, { emit }) {
const stepperNavigation = shallowRef<HTMLElement>()
const { valueComputed: modelValue }: { valueComputed: Ref<number> } = useStateful(props, emit, 'modelValue', { defaultValue: 0 })
const focusedStep = ref({ force: false, stepIndex: props.navigationDisabled ? -1 : props.modelValue })
const { getColor } = useColors()
const stepperColor = getColor(props.color)
const isNextStepDisabled = (index: number) => props.nextDisabled && index > modelValue.value
const { t } = useTranslation()
const setStep = (index: number) => {
if (props.steps[index].disabled) { return }
emit('update:modelValue', index)
}
const setFocus = (direction: 'prev' | 'next') => {
if (props.navigationDisabled) { return }
if (direction === 'next') {
setFocusNextStep(1)
} else {
setFocusPrevStep(1)
}
}
const setFocusNextStep = (idx: number) => {
const newValue = focusedStep.value.stepIndex + idx
if (isNextStepDisabled(newValue)) { return }
if (newValue < props.steps.length) {
if (props.steps[newValue].disabled) {
setFocusNextStep(idx + 1)
return
}
focusedStep.value.stepIndex = newValue
focusedStep.value.force = true
}
}
const setFocusPrevStep = (idx: number) => {
const newValue = focusedStep.value.stepIndex - idx
if (newValue >= 0) {
if (props.steps[newValue].disabled) {
setFocusPrevStep(idx + 1)
return
}
focusedStep.value.stepIndex = newValue
focusedStep.value.force = true
}
}
const resetFocus = (e: any) => {
requestAnimationFrame(() => {
if (!stepperNavigation.value?.contains(document.activeElement)) {
focusedStep.value.stepIndex = props.modelValue
focusedStep.value.force = false
}
})
}
watch(() => props.modelValue, () => {
focusedStep.value.stepIndex = props.modelValue
focusedStep.value.force = false
})
const nextStep = (stepsToSkip = 0) => {
const targetIndex = modelValue.value + 1 + stepsToSkip
Expand All @@ -138,16 +204,32 @@ export default defineComponent({
const getIterableSlotData = (step: Step, index: number) => ({
...stepControls,
step,
focus: focusedStep,
isActive: props.modelValue === index,
isCompleted: props.modelValue > index,
})
return {
stepperNavigation,
resetFocus,
focusedStep,
isNextStepDisabled,
stepperColor,
getColor,
stepControls,
getIterableSlotData,
onArrowKeyPress: (direction: 'prev' | 'next') => {
setFocus(direction)
},
onNavigationValueChange: () => {
focusedStep.value.stepIndex = props.modelValue
focusedStep.value.force = true
},
ariaAttributesComputed: computed(() => ({
role: 'group',
'aria-label': t('progress'),
'aria-orientation': props.vertical ? 'vertical' as const : 'horizontal' as const,
})),
}
},
})
Expand Down
27 changes: 25 additions & 2 deletions packages/ui/src/components/va-stepper/VaStepperStepButton.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<template>
<li
ref="stepElement"
class="va-stepper__step-button"
:class="computedClass"
@click="!$props.navigationDisabled && $props.stepControls.setStep($props.stepIndex)"
@keyup.enter="!$props.navigationDisabled && $props.stepControls.setStep($props.stepIndex)"
@keyup.space="!$props.navigationDisabled && $props.stepControls.setStep($props.stepIndex)"
v-bind="ariaAttributesComputed"
>
<div class="va-stepper__step-button__icon">
<va-icon
Expand All @@ -18,9 +22,9 @@
</li>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { computed, defineComponent, nextTick, PropType, shallowRef, watch } from 'vue'
import { VaIcon } from '../va-icon'
import { useBem, useColors } from '../../composables'
import { useBem, useColors, useTranslation } from '../../composables'
import type { Step, StepControls } from './types'
export default defineComponent({
Expand All @@ -36,26 +40,43 @@ export default defineComponent({
stepIndex: { type: Number, required: true },
navigationDisabled: { type: Boolean, required: true },
nextDisabled: { type: Boolean, required: true },
focus: { type: Object, required: true },
stepControls: { type: Object as PropType<StepControls>, required: true },
},
emits: ['update:modelValue'],
setup (props) {
const stepElement = shallowRef<HTMLElement>()
const { getColor } = useColors()
const stepperColor = getColor(props.color)
const isNextStepDisabled = (index: number) => props.nextDisabled && index > props.modelValue
const { t } = useTranslation()
const computedClass = useBem('va-stepper__step-button', () => ({
active: props.modelValue >= props.stepIndex,
disabled: props.step.disabled || isNextStepDisabled(props.stepIndex),
'navigation-disabled': props.navigationDisabled,
}))
watch(() => props.focus, () => {
if (props.focus.force) {
nextTick(() => stepElement.value?.focus())
}
}, { deep: true })
return {
stepElement,
isNextStepDisabled,
stepperColor,
getColor,
computedClass,
ariaAttributesComputed: computed(() => ({
tabindex: props.focus.stepIndex === props.stepIndex && !props.navigationDisabled ? 0 : undefined,
'aria-disabled': props.step.disabled || isNextStepDisabled(props.stepIndex) ? true : undefined,
'aria-current': props.modelValue === props.stepIndex ? t('step') : undefined,
})),
}
},
})
Expand All @@ -75,6 +96,8 @@ export default defineComponent({
flex-shrink: 0;
padding: var(--va-stepper-step-button-padding);
@include keyboard-focus-outline;
&::after {
content: "";
position: absolute;
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/services/i18n/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,7 @@ export const getI18nConfigDefaults = () => ({
back: 'Previous',
/** Stepper finish button text */
finish: 'Finish',

step: 'step',
progress: 'progress',
})

0 comments on commit ce405ca

Please sign in to comment.