Calculate aria-expanded purely based on the open/closed state (#2610)
* define `aria-expanded` based on open/closed state You shouldn't be able to open a Listbox/Menu/Combobox/... when the component is in a disabled state, however if you open it, and then disable it then it is still in an open state. Therefore the `aria-expanded` should still be present. This is also how other libraries behave. It is also how the native `<select>` behaves. You can open it, disable it programmatically and then you are still able to make a selection. This seems enough evidence that this change is an improvement without being a breaking change. Fixes: #2602 * update changelog
This commit is contained in:
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Ensure the caret is in a consistent position when syncing the `Combobox.Input` value ([#2568](https://github.com/tailwindlabs/headlessui/pull/2568))
|
||||
- Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572))
|
||||
- Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580))
|
||||
- Calculate `aria-expanded` purely based on the open/closed state ([#2610](https://github.com/tailwindlabs/headlessui/pull/2610))
|
||||
|
||||
## [1.7.15] - 2023-06-01
|
||||
|
||||
|
||||
@@ -1020,7 +1020,7 @@ function InputFn<
|
||||
role: 'combobox',
|
||||
type,
|
||||
'aria-controls': data.optionsRef.current?.id,
|
||||
'aria-expanded': data.disabled ? undefined : data.comboboxState === ComboboxState.Open,
|
||||
'aria-expanded': data.comboboxState === ComboboxState.Open,
|
||||
'aria-activedescendant':
|
||||
data.activeOptionIndex === null ? undefined : data.options[data.activeOptionIndex]?.id,
|
||||
'aria-labelledby': labelledby,
|
||||
@@ -1152,7 +1152,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
|
||||
tabIndex: -1,
|
||||
'aria-haspopup': 'listbox',
|
||||
'aria-controls': data.optionsRef.current?.id,
|
||||
'aria-expanded': data.disabled ? undefined : data.comboboxState === ComboboxState.Open,
|
||||
'aria-expanded': data.comboboxState === ComboboxState.Open,
|
||||
'aria-labelledby': labelledby,
|
||||
disabled: data.disabled,
|
||||
onClick: handleClick,
|
||||
|
||||
@@ -340,9 +340,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
|
||||
ref: buttonRef,
|
||||
id,
|
||||
type,
|
||||
'aria-expanded': props.disabled
|
||||
? undefined
|
||||
: state.disclosureState === DisclosureStates.Open,
|
||||
'aria-expanded': state.disclosureState === DisclosureStates.Open,
|
||||
'aria-controls': state.linkedPanel ? state.panelId : undefined,
|
||||
onKeyDown: handleKeyDown,
|
||||
onKeyUp: handleKeyUp,
|
||||
|
||||
@@ -680,7 +680,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
|
||||
type: useResolveButtonType(props, data.buttonRef),
|
||||
'aria-haspopup': 'listbox',
|
||||
'aria-controls': data.optionsRef.current?.id,
|
||||
'aria-expanded': data.disabled ? undefined : data.listboxState === ListboxStates.Open,
|
||||
'aria-expanded': data.listboxState === ListboxStates.Open,
|
||||
'aria-labelledby': labelledby,
|
||||
disabled: data.disabled,
|
||||
onKeyDown: handleKeyDown,
|
||||
|
||||
@@ -400,7 +400,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
|
||||
type: useResolveButtonType(props, state.buttonRef),
|
||||
'aria-haspopup': 'menu',
|
||||
'aria-controls': state.itemsRef.current?.id,
|
||||
'aria-expanded': props.disabled ? undefined : state.menuState === MenuStates.Open,
|
||||
'aria-expanded': state.menuState === MenuStates.Open,
|
||||
onKeyDown: handleKeyDown,
|
||||
onKeyUp: handleKeyUp,
|
||||
onClick: handleClick,
|
||||
|
||||
@@ -591,7 +591,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
|
||||
ref: buttonRef,
|
||||
id: state.buttonId,
|
||||
type,
|
||||
'aria-expanded': props.disabled ? undefined : state.popoverState === PopoverStates.Open,
|
||||
'aria-expanded': state.popoverState === PopoverStates.Open,
|
||||
'aria-controls': state.panel ? state.panelId : undefined,
|
||||
onKeyDown: handleKeyDown,
|
||||
onKeyUp: handleKeyUp,
|
||||
|
||||
@@ -62,20 +62,12 @@ export function assertMenuButton(
|
||||
|
||||
case MenuState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case MenuState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -352,20 +344,12 @@ export function assertComboboxInput(
|
||||
|
||||
case ComboboxState.InvisibleHidden:
|
||||
expect(input).toHaveAttribute('aria-controls')
|
||||
if (input.hasAttribute('disabled')) {
|
||||
expect(input).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(input).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(input).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case ComboboxState.InvisibleUnmounted:
|
||||
expect(input).not.toHaveAttribute('aria-controls')
|
||||
if (input.hasAttribute('disabled')) {
|
||||
expect(input).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(input).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(input).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -458,20 +442,12 @@ export function assertComboboxButton(
|
||||
|
||||
case ComboboxState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case ComboboxState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -798,20 +774,12 @@ export function assertListboxButton(
|
||||
|
||||
case ListboxState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case ListboxState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -1100,20 +1068,12 @@ export function assertDisclosureButton(
|
||||
|
||||
case DisclosureState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case DisclosureState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -1232,20 +1192,12 @@ export function assertPopoverButton(
|
||||
|
||||
case PopoverState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case PopoverState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
|
||||
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572))
|
||||
- Improve performance of `Combobox` component ([#2574](https://github.com/tailwindlabs/headlessui/pull/2574))
|
||||
- Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580))
|
||||
- Calculate `aria-expanded` purely based on the open/closed state ([#2610](https://github.com/tailwindlabs/headlessui/pull/2610))
|
||||
|
||||
## [1.7.14] - 2023-06-01
|
||||
|
||||
|
||||
@@ -662,9 +662,7 @@ export let ComboboxButton = defineComponent({
|
||||
tabindex: '-1',
|
||||
'aria-haspopup': 'listbox',
|
||||
'aria-controls': dom(api.optionsRef)?.id,
|
||||
'aria-expanded': api.disabled.value
|
||||
? undefined
|
||||
: api.comboboxState.value === ComboboxStates.Open,
|
||||
'aria-expanded': api.comboboxState.value === ComboboxStates.Open,
|
||||
'aria-labelledby': api.labelRef.value ? [dom(api.labelRef)?.id, id].join(' ') : undefined,
|
||||
disabled: api.disabled.value === true ? true : undefined,
|
||||
onKeydown: handleKeydown,
|
||||
@@ -980,9 +978,7 @@ export let ComboboxInput = defineComponent({
|
||||
let { id, displayValue, onChange: _onChange, ...theirProps } = props
|
||||
let ourProps = {
|
||||
'aria-controls': api.optionsRef.value?.id,
|
||||
'aria-expanded': api.disabled.value
|
||||
? undefined
|
||||
: api.comboboxState.value === ComboboxStates.Open,
|
||||
'aria-expanded': api.comboboxState.value === ComboboxStates.Open,
|
||||
'aria-activedescendant':
|
||||
api.activeOptionIndex.value === null
|
||||
? undefined
|
||||
|
||||
@@ -229,9 +229,7 @@ export let DisclosureButton = defineComponent({
|
||||
id,
|
||||
ref: internalButtonRef,
|
||||
type: type.value,
|
||||
'aria-expanded': props.disabled
|
||||
? undefined
|
||||
: api.disclosureState.value === DisclosureStates.Open,
|
||||
'aria-expanded': api.disclosureState.value === DisclosureStates.Open,
|
||||
'aria-controls': dom(api.panel) ? api.panelId.value : undefined,
|
||||
disabled: props.disabled ? true : undefined,
|
||||
onClick: handleClick,
|
||||
|
||||
@@ -532,9 +532,7 @@ export let ListboxButton = defineComponent({
|
||||
type: type.value,
|
||||
'aria-haspopup': 'listbox',
|
||||
'aria-controls': dom(api.optionsRef)?.id,
|
||||
'aria-expanded': api.disabled.value
|
||||
? undefined
|
||||
: api.listboxState.value === ListboxStates.Open,
|
||||
'aria-expanded': api.listboxState.value === ListboxStates.Open,
|
||||
'aria-labelledby': api.labelRef.value ? [dom(api.labelRef)?.id, id].join(' ') : undefined,
|
||||
disabled: api.disabled.value === true ? true : undefined,
|
||||
onKeydown: handleKeyDown,
|
||||
|
||||
@@ -320,7 +320,7 @@ export let MenuButton = defineComponent({
|
||||
type: type.value,
|
||||
'aria-haspopup': 'menu',
|
||||
'aria-controls': dom(api.itemsRef)?.id,
|
||||
'aria-expanded': props.disabled ? undefined : api.menuState.value === MenuStates.Open,
|
||||
'aria-expanded': api.menuState.value === MenuStates.Open,
|
||||
onKeydown: handleKeyDown,
|
||||
onKeyup: handleKeyUp,
|
||||
onClick: handleClick,
|
||||
|
||||
@@ -438,9 +438,7 @@ export let PopoverButton = defineComponent({
|
||||
ref: elementRef,
|
||||
id,
|
||||
type: type.value,
|
||||
'aria-expanded': props.disabled
|
||||
? undefined
|
||||
: api.popoverState.value === PopoverStates.Open,
|
||||
'aria-expanded': api.popoverState.value === PopoverStates.Open,
|
||||
'aria-controls': dom(api.panel) ? api.panelId.value : undefined,
|
||||
disabled: props.disabled ? true : undefined,
|
||||
onKeydown: handleKeyDown,
|
||||
|
||||
@@ -62,20 +62,12 @@ export function assertMenuButton(
|
||||
|
||||
case MenuState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case MenuState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -352,20 +344,12 @@ export function assertComboboxInput(
|
||||
|
||||
case ComboboxState.InvisibleHidden:
|
||||
expect(input).toHaveAttribute('aria-controls')
|
||||
if (input.hasAttribute('disabled')) {
|
||||
expect(input).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(input).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(input).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case ComboboxState.InvisibleUnmounted:
|
||||
expect(input).not.toHaveAttribute('aria-controls')
|
||||
if (input.hasAttribute('disabled')) {
|
||||
expect(input).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(input).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(input).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -458,20 +442,12 @@ export function assertComboboxButton(
|
||||
|
||||
case ComboboxState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case ComboboxState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -798,20 +774,12 @@ export function assertListboxButton(
|
||||
|
||||
case ListboxState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case ListboxState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -1100,20 +1068,12 @@ export function assertDisclosureButton(
|
||||
|
||||
case DisclosureState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case DisclosureState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -1232,20 +1192,12 @@ export function assertPopoverButton(
|
||||
|
||||
case PopoverState.InvisibleHidden:
|
||||
expect(button).toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
case PopoverState.InvisibleUnmounted:
|
||||
expect(button).not.toHaveAttribute('aria-controls')
|
||||
if (button.hasAttribute('disabled')) {
|
||||
expect(button).not.toHaveAttribute('aria-expanded')
|
||||
} else {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
}
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
break
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user