Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Combobox accessibility #2153

Merged
merged 4 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix false positive warning when using `<Popover.Button />` in React 17 ([#2163](https://github.com/tailwindlabs/headlessui/pull/2163))
- Fix `failed to removeChild on Node` bug ([#2164](https://github.com/tailwindlabs/headlessui/pull/2164))
- Don’t overwrite classes during SSR when rendering fragments ([#2173](https://github.com/tailwindlabs/headlessui/pull/2173))
- Improve `Combobox` accessibility ([#2153](https://github.com/tailwindlabs/headlessui/pull/2153))

## [1.7.7] - 2022-12-16

Expand Down
43 changes: 34 additions & 9 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ type InputPropsWeControl =
| 'aria-labelledby'
| 'aria-expanded'
| 'aria-activedescendant'
| 'aria-autocomplete'
| 'onKeyDown'
| 'onChange'
| 'displayValue'
Expand Down Expand Up @@ -741,6 +742,37 @@ let Input = forwardRefWithAs(function Input<
[currentDisplayValue, data.comboboxState]
)

// Trick VoiceOver in behaving a little bit better. Manually "resetting" the input makes VoiceOver
// a bit more happy and doesn't require some changes manually first before announcing items
// correctly. This is a bit of a hacks, but it is a workaround for a VoiceOver bug.
//
// TODO: VoiceOver is still relatively buggy if you start VoiceOver while the Combobox is already
// in an open state.
useWatch(
([newState], [oldState]) => {
if (newState === ComboboxState.Open && oldState === ComboboxState.Closed) {
let input = data.inputRef.current
if (!input) return

// Capture current state
let currentValue = input.value
let { selectionStart, selectionEnd, selectionDirection } = input

// Trick VoiceOver into announcing the value
input.value = ''

// Rollback to original state
input.value = currentValue
if (selectionDirection !== null) {
input.setSelectionRange(selectionStart, selectionEnd, selectionDirection)
} else {
input.setSelectionRange(selectionStart, selectionEnd)
}
}
},
[data.comboboxState]
)

let isComposing = useRef(false)
let handleCompositionStart = useEvent(() => {
isComposing.current = true
Expand Down Expand Up @@ -905,6 +937,7 @@ let Input = forwardRefWithAs(function Input<
data.activeOptionIndex === null ? undefined : data.options[data.activeOptionIndex]?.id,
'aria-multiselectable': data.mode === ValueMode.Multi ? true : undefined,
'aria-labelledby': labelledby,
'aria-autocomplete': 'list',
defaultValue:
props.defaultValue ??
(data.defaultValue !== undefined
Expand Down Expand Up @@ -1090,13 +1123,7 @@ let DEFAULT_OPTIONS_TAG = 'ul' as const
interface OptionsRenderPropArg {
open: boolean
}
type OptionsPropsWeControl =
| 'aria-activedescendant'
| 'aria-labelledby'
| 'hold'
| 'onKeyDown'
| 'role'
| 'tabIndex'
type OptionsPropsWeControl = 'aria-labelledby' | 'hold' | 'onKeyDown' | 'role' | 'tabIndex'

let OptionsRenderFeatures = Features.RenderStrategy | Features.Static

Expand Down Expand Up @@ -1154,8 +1181,6 @@ let Options = forwardRefWithAs(function Options<
[data]
)
let ourProps = {
'aria-activedescendant':
data.activeOptionIndex === null ? undefined : data.options[data.activeOptionIndex]?.id,
'aria-labelledby': labelledby,
role: 'listbox',
id,
Expand Down
1 change: 1 addition & 0 deletions packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix arrow key handling in `Tab` (after DOM order changes) ([#2145](https://github.com/tailwindlabs/headlessui/pull/2145))
- Fix `Tab` key with non focusable elements in `Popover.Panel` ([#2147](https://github.com/tailwindlabs/headlessui/pull/2147))
- Don’t overwrite classes during SSR when rendering fragments ([#2173](https://github.com/tailwindlabs/headlessui/pull/2173))
- Improve `Combobox` accessibility ([#2153](https://github.com/tailwindlabs/headlessui/pull/2153))

## [1.7.7] - 2022-12-16

Expand Down
34 changes: 30 additions & 4 deletions packages/@headlessui-vue/src/components/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,35 @@ export let ComboboxInput = defineComponent({
},
{ immediate: true }
)

// Trick VoiceOver in behaving a little bit better. Manually "resetting" the input makes
// VoiceOver a bit more happy and doesn't require some changes manually first before
// announcing items correctly. This is a bit of a hacks, but it is a workaround for a
// VoiceOver bug.
//
// TODO: VoiceOver is still relatively buggy if you start VoiceOver while the Combobox is
// already in an open state.
watch([api.comboboxState], ([newState], [oldState]) => {
if (newState === ComboboxStates.Open && oldState === ComboboxStates.Closed) {
let input = dom(api.inputRef)
if (!input) return

// Capture current state
let currentValue = input.value
let { selectionStart, selectionEnd, selectionDirection } = input

// Trick VoiceOver into announcing the value
input.value = ''

// Rollback to original state
input.value = currentValue
if (selectionDirection !== null) {
input.setSelectionRange(selectionStart, selectionEnd, selectionDirection)
} else {
input.setSelectionRange(selectionStart, selectionEnd)
}
}
})
})

let isComposing = ref(false)
Expand Down Expand Up @@ -880,6 +909,7 @@ export let ComboboxInput = defineComponent({
: api.options.value[api.activeOptionIndex.value]?.id,
'aria-multiselectable': api.mode.value === ValueMode.Multi ? true : undefined,
'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id,
'aria-autocomplete': 'list',
id,
onCompositionstart: handleCompositionstart,
onCompositionend: handleCompositionend,
Expand Down Expand Up @@ -956,10 +986,6 @@ export let ComboboxOptions = defineComponent({
return () => {
let slot = { open: api.comboboxState.value === ComboboxStates.Open }
let ourProps = {
'aria-activedescendant':
api.activeOptionIndex.value === null
? undefined
: api.options.value[api.activeOptionIndex.value]?.id,
'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id,
id,
ref: api.optionsRef,
Expand Down