Skip to content

Commit

Permalink
Calculate aria-expanded purely based on the open/closed state (#2610)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
RobinMalfait committed Jul 24, 2023
1 parent 076b03c commit 1739edb
Show file tree
Hide file tree
Showing 14 changed files with 38 additions and 144 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/@headlessui-react/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
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 @@ -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

Expand Down
8 changes: 2 additions & 6 deletions packages/@headlessui-vue/src/components/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions packages/@headlessui-vue/src/components/listbox/listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/@headlessui-vue/src/components/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions packages/@headlessui-vue/src/components/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

2 comments on commit 1739edb

@vercel
Copy link

@vercel vercel bot commented on 1739edb Jul 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

headlessui-vue – ./packages/playground-vue

headlessui-vue-git-main-tailwindlabs.vercel.app
headlessui-vue.vercel.app
headlessui-vue-tailwindlabs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 1739edb Jul 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

headlessui-react – ./packages/playground-react

headlessui-react.vercel.app
headlessui-react-git-main-tailwindlabs.vercel.app
headlessui-react-tailwindlabs.vercel.app

Please sign in to comment.