Skip to content

Commit 1739edb

Browse files
authored
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
1 parent 076b03c commit 1739edb

File tree

14 files changed

+38
-144
lines changed

14 files changed

+38
-144
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Ensure the caret is in a consistent position when syncing the `Combobox.Input` value ([#2568](https://github.com/tailwindlabs/headlessui/pull/2568))
1313
- Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572))
1414
- Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580))
15+
- Calculate `aria-expanded` purely based on the open/closed state ([#2610](https://github.com/tailwindlabs/headlessui/pull/2610))
1516

1617
## [1.7.15] - 2023-06-01
1718

packages/@headlessui-react/src/components/combobox/combobox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,7 @@ function InputFn<
10201020
role: 'combobox',
10211021
type,
10221022
'aria-controls': data.optionsRef.current?.id,
1023-
'aria-expanded': data.disabled ? undefined : data.comboboxState === ComboboxState.Open,
1023+
'aria-expanded': data.comboboxState === ComboboxState.Open,
10241024
'aria-activedescendant':
10251025
data.activeOptionIndex === null ? undefined : data.options[data.activeOptionIndex]?.id,
10261026
'aria-labelledby': labelledby,
@@ -1152,7 +1152,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
11521152
tabIndex: -1,
11531153
'aria-haspopup': 'listbox',
11541154
'aria-controls': data.optionsRef.current?.id,
1155-
'aria-expanded': data.disabled ? undefined : data.comboboxState === ComboboxState.Open,
1155+
'aria-expanded': data.comboboxState === ComboboxState.Open,
11561156
'aria-labelledby': labelledby,
11571157
disabled: data.disabled,
11581158
onClick: handleClick,

packages/@headlessui-react/src/components/disclosure/disclosure.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
340340
ref: buttonRef,
341341
id,
342342
type,
343-
'aria-expanded': props.disabled
344-
? undefined
345-
: state.disclosureState === DisclosureStates.Open,
343+
'aria-expanded': state.disclosureState === DisclosureStates.Open,
346344
'aria-controls': state.linkedPanel ? state.panelId : undefined,
347345
onKeyDown: handleKeyDown,
348346
onKeyUp: handleKeyUp,

packages/@headlessui-react/src/components/listbox/listbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
680680
type: useResolveButtonType(props, data.buttonRef),
681681
'aria-haspopup': 'listbox',
682682
'aria-controls': data.optionsRef.current?.id,
683-
'aria-expanded': data.disabled ? undefined : data.listboxState === ListboxStates.Open,
683+
'aria-expanded': data.listboxState === ListboxStates.Open,
684684
'aria-labelledby': labelledby,
685685
disabled: data.disabled,
686686
onKeyDown: handleKeyDown,

packages/@headlessui-react/src/components/menu/menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
400400
type: useResolveButtonType(props, state.buttonRef),
401401
'aria-haspopup': 'menu',
402402
'aria-controls': state.itemsRef.current?.id,
403-
'aria-expanded': props.disabled ? undefined : state.menuState === MenuStates.Open,
403+
'aria-expanded': state.menuState === MenuStates.Open,
404404
onKeyDown: handleKeyDown,
405405
onKeyUp: handleKeyUp,
406406
onClick: handleClick,

packages/@headlessui-react/src/components/popover/popover.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
591591
ref: buttonRef,
592592
id: state.buttonId,
593593
type,
594-
'aria-expanded': props.disabled ? undefined : state.popoverState === PopoverStates.Open,
594+
'aria-expanded': state.popoverState === PopoverStates.Open,
595595
'aria-controls': state.panel ? state.panelId : undefined,
596596
onKeyDown: handleKeyDown,
597597
onKeyUp: handleKeyUp,

packages/@headlessui-react/src/test-utils/accessibility-assertions.ts

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,12 @@ export function assertMenuButton(
6262

6363
case MenuState.InvisibleHidden:
6464
expect(button).toHaveAttribute('aria-controls')
65-
if (button.hasAttribute('disabled')) {
66-
expect(button).not.toHaveAttribute('aria-expanded')
67-
} else {
68-
expect(button).toHaveAttribute('aria-expanded', 'false')
69-
}
65+
expect(button).toHaveAttribute('aria-expanded', 'false')
7066
break
7167

7268
case MenuState.InvisibleUnmounted:
7369
expect(button).not.toHaveAttribute('aria-controls')
74-
if (button.hasAttribute('disabled')) {
75-
expect(button).not.toHaveAttribute('aria-expanded')
76-
} else {
77-
expect(button).toHaveAttribute('aria-expanded', 'false')
78-
}
70+
expect(button).toHaveAttribute('aria-expanded', 'false')
7971
break
8072

8173
default:
@@ -352,20 +344,12 @@ export function assertComboboxInput(
352344

353345
case ComboboxState.InvisibleHidden:
354346
expect(input).toHaveAttribute('aria-controls')
355-
if (input.hasAttribute('disabled')) {
356-
expect(input).not.toHaveAttribute('aria-expanded')
357-
} else {
358-
expect(input).toHaveAttribute('aria-expanded', 'false')
359-
}
347+
expect(input).toHaveAttribute('aria-expanded', 'false')
360348
break
361349

362350
case ComboboxState.InvisibleUnmounted:
363351
expect(input).not.toHaveAttribute('aria-controls')
364-
if (input.hasAttribute('disabled')) {
365-
expect(input).not.toHaveAttribute('aria-expanded')
366-
} else {
367-
expect(input).toHaveAttribute('aria-expanded', 'false')
368-
}
352+
expect(input).toHaveAttribute('aria-expanded', 'false')
369353
break
370354

371355
default:
@@ -458,20 +442,12 @@ export function assertComboboxButton(
458442

459443
case ComboboxState.InvisibleHidden:
460444
expect(button).toHaveAttribute('aria-controls')
461-
if (button.hasAttribute('disabled')) {
462-
expect(button).not.toHaveAttribute('aria-expanded')
463-
} else {
464-
expect(button).toHaveAttribute('aria-expanded', 'false')
465-
}
445+
expect(button).toHaveAttribute('aria-expanded', 'false')
466446
break
467447

468448
case ComboboxState.InvisibleUnmounted:
469449
expect(button).not.toHaveAttribute('aria-controls')
470-
if (button.hasAttribute('disabled')) {
471-
expect(button).not.toHaveAttribute('aria-expanded')
472-
} else {
473-
expect(button).toHaveAttribute('aria-expanded', 'false')
474-
}
450+
expect(button).toHaveAttribute('aria-expanded', 'false')
475451
break
476452

477453
default:
@@ -798,20 +774,12 @@ export function assertListboxButton(
798774

799775
case ListboxState.InvisibleHidden:
800776
expect(button).toHaveAttribute('aria-controls')
801-
if (button.hasAttribute('disabled')) {
802-
expect(button).not.toHaveAttribute('aria-expanded')
803-
} else {
804-
expect(button).toHaveAttribute('aria-expanded', 'false')
805-
}
777+
expect(button).toHaveAttribute('aria-expanded', 'false')
806778
break
807779

808780
case ListboxState.InvisibleUnmounted:
809781
expect(button).not.toHaveAttribute('aria-controls')
810-
if (button.hasAttribute('disabled')) {
811-
expect(button).not.toHaveAttribute('aria-expanded')
812-
} else {
813-
expect(button).toHaveAttribute('aria-expanded', 'false')
814-
}
782+
expect(button).toHaveAttribute('aria-expanded', 'false')
815783
break
816784

817785
default:
@@ -1100,20 +1068,12 @@ export function assertDisclosureButton(
11001068

11011069
case DisclosureState.InvisibleHidden:
11021070
expect(button).toHaveAttribute('aria-controls')
1103-
if (button.hasAttribute('disabled')) {
1104-
expect(button).not.toHaveAttribute('aria-expanded')
1105-
} else {
1106-
expect(button).toHaveAttribute('aria-expanded', 'false')
1107-
}
1071+
expect(button).toHaveAttribute('aria-expanded', 'false')
11081072
break
11091073

11101074
case DisclosureState.InvisibleUnmounted:
11111075
expect(button).not.toHaveAttribute('aria-controls')
1112-
if (button.hasAttribute('disabled')) {
1113-
expect(button).not.toHaveAttribute('aria-expanded')
1114-
} else {
1115-
expect(button).toHaveAttribute('aria-expanded', 'false')
1116-
}
1076+
expect(button).toHaveAttribute('aria-expanded', 'false')
11171077
break
11181078

11191079
default:
@@ -1232,20 +1192,12 @@ export function assertPopoverButton(
12321192

12331193
case PopoverState.InvisibleHidden:
12341194
expect(button).toHaveAttribute('aria-controls')
1235-
if (button.hasAttribute('disabled')) {
1236-
expect(button).not.toHaveAttribute('aria-expanded')
1237-
} else {
1238-
expect(button).toHaveAttribute('aria-expanded', 'false')
1239-
}
1195+
expect(button).toHaveAttribute('aria-expanded', 'false')
12401196
break
12411197

12421198
case PopoverState.InvisibleUnmounted:
12431199
expect(button).not.toHaveAttribute('aria-controls')
1244-
if (button.hasAttribute('disabled')) {
1245-
expect(button).not.toHaveAttribute('aria-expanded')
1246-
} else {
1247-
expect(button).toHaveAttribute('aria-expanded', 'false')
1248-
}
1200+
expect(button).toHaveAttribute('aria-expanded', 'false')
12491201
break
12501202

12511203
default:

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572))
1414
- Improve performance of `Combobox` component ([#2574](https://github.com/tailwindlabs/headlessui/pull/2574))
1515
- Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580))
16+
- Calculate `aria-expanded` purely based on the open/closed state ([#2610](https://github.com/tailwindlabs/headlessui/pull/2610))
1617

1718
## [1.7.14] - 2023-06-01
1819

packages/@headlessui-vue/src/components/combobox/combobox.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -662,9 +662,7 @@ export let ComboboxButton = defineComponent({
662662
tabindex: '-1',
663663
'aria-haspopup': 'listbox',
664664
'aria-controls': dom(api.optionsRef)?.id,
665-
'aria-expanded': api.disabled.value
666-
? undefined
667-
: api.comboboxState.value === ComboboxStates.Open,
665+
'aria-expanded': api.comboboxState.value === ComboboxStates.Open,
668666
'aria-labelledby': api.labelRef.value ? [dom(api.labelRef)?.id, id].join(' ') : undefined,
669667
disabled: api.disabled.value === true ? true : undefined,
670668
onKeydown: handleKeydown,
@@ -980,9 +978,7 @@ export let ComboboxInput = defineComponent({
980978
let { id, displayValue, onChange: _onChange, ...theirProps } = props
981979
let ourProps = {
982980
'aria-controls': api.optionsRef.value?.id,
983-
'aria-expanded': api.disabled.value
984-
? undefined
985-
: api.comboboxState.value === ComboboxStates.Open,
981+
'aria-expanded': api.comboboxState.value === ComboboxStates.Open,
986982
'aria-activedescendant':
987983
api.activeOptionIndex.value === null
988984
? undefined

packages/@headlessui-vue/src/components/disclosure/disclosure.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,7 @@ export let DisclosureButton = defineComponent({
229229
id,
230230
ref: internalButtonRef,
231231
type: type.value,
232-
'aria-expanded': props.disabled
233-
? undefined
234-
: api.disclosureState.value === DisclosureStates.Open,
232+
'aria-expanded': api.disclosureState.value === DisclosureStates.Open,
235233
'aria-controls': dom(api.panel) ? api.panelId.value : undefined,
236234
disabled: props.disabled ? true : undefined,
237235
onClick: handleClick,

0 commit comments

Comments
 (0)