From 0e0277a68428104e57ae8c43e7a89cd4dea12b0e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 3 Feb 2024 17:31:02 +0100 Subject: [PATCH] Allow setting custom `tabIndex` on the `` component (#2966) * allow setting a custom `tabIndex` on the `` component * update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + .../src/components/switch/switch.test.tsx | 41 +++++++++++++++++++ .../src/components/switch/switch.tsx | 10 ++--- .../test-utils/accessibility-assertions.ts | 9 +++- packages/@headlessui-vue/CHANGELOG.md | 1 + .../src/components/switch/switch.test.tsx | 32 +++++++++++++++ .../src/components/switch/switch.ts | 5 ++- .../test-utils/accessibility-assertions.ts | 9 +++- 8 files changed, 97 insertions(+), 11 deletions(-) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 61c0898..3908d51 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `children` prop of `Field` component can be a render prop ([#2941](https://github.com/tailwindlabs/headlessui/pull/2941)) - Add `hidden` attribute to internal `` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955)) - Attempt form submission when pressing `Enter` on `Checkbox` component ([#2962](https://github.com/tailwindlabs/headlessui/pull/2962)) +- Allow setting custom `tabIndex` on the `` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966)) ## [2.0.0-alpha.4] - 2024-01-03 diff --git a/packages/@headlessui-react/src/components/switch/switch.test.tsx b/packages/@headlessui-react/src/components/switch/switch.test.tsx index c65ddf5..b6988f2 100644 --- a/packages/@headlessui-react/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.test.tsx @@ -59,6 +59,47 @@ describe('Rendering', () => { assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' }) }) + describe('`tabIndex` attribute', () => { + it('should have a default tabIndex of `0`', () => { + render( + + Enable notifications + + ) + assertSwitch({ + state: SwitchState.Off, + label: 'Enable notifications', + attributes: { tabindex: '0' }, + }) + }) + + it('should be possible to override the `tabIndex`', () => { + render( + + Enable notifications + + ) + assertSwitch({ + state: SwitchState.Off, + label: 'Enable notifications', + attributes: { tabindex: '3' }, + }) + }) + + it('should not be possible to override the `tabIndex` to `-1`', () => { + render( + + Enable notifications + + ) + assertSwitch({ + state: SwitchState.Off, + label: 'Enable notifications', + attributes: { tabindex: '0' }, + }) + }) + }) + describe('`type` attribute', () => { it('should set the `type` to "button" by default', async () => { render( diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx index 3adb57b..76a4ab8 100644 --- a/packages/@headlessui-react/src/components/switch/switch.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.tsx @@ -116,12 +116,7 @@ type SwitchRenderPropArg = { changing: boolean disabled: boolean } -type SwitchPropsWeControl = - | 'aria-checked' - | 'aria-describedby' - | 'aria-labelledby' - | 'role' - | 'tabIndex' +type SwitchPropsWeControl = 'aria-checked' | 'aria-describedby' | 'aria-labelledby' | 'role' export type SwitchProps = Props< TTag, @@ -136,6 +131,7 @@ export type SwitchProps = form?: string autoFocus?: boolean disabled?: boolean + tabIndex?: number } > @@ -220,7 +216,7 @@ function SwitchFn( ref: switchRef, role: 'switch', type: useResolveButtonType(props, internalSwitchRef), - tabIndex: 0, + tabIndex: props.tabIndex === -1 ? 0 : props.tabIndex ?? 0, 'aria-checked': checked, 'aria-labelledby': labelledBy, 'aria-describedby': describedBy, diff --git a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts index 1b7beff..cf30183 100644 --- a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts +++ b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts @@ -1145,6 +1145,7 @@ export function assertSwitch( textContent?: string label?: string description?: string + attributes?: Record }, switchElement = getSwitch() ) { @@ -1152,7 +1153,8 @@ export function assertSwitch( if (switchElement === null) return expect(switchElement).not.toBe(null) expect(switchElement).toHaveAttribute('role', 'switch') - expect(switchElement).toHaveAttribute('tabindex', '0') + let tabIndex = Number(switchElement.getAttribute('tabindex') ?? '0') + expect(tabIndex).toBeGreaterThanOrEqual(0) if (options.textContent) { expect(switchElement).toHaveTextContent(options.textContent) @@ -1182,6 +1184,11 @@ export function assertSwitch( default: assertNever(options.state) } + + // Ensure disclosure button has the following attributes + for (let attributeName in options.attributes) { + expect(switchElement).toHaveAttribute(attributeName, options.attributes[attributeName]) + } } catch (err) { if (err instanceof Error) Error.captureStackTrace(err, assertSwitch) throw err diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index bbcd06e..a778aa6 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prevent default behaviour when clicking outside of a `DialogPanel` ([#2919](https://github.com/tailwindlabs/headlessui/pull/2919)) - Don’t override explicit `disabled` prop for components inside `` ([#2929](https://github.com/tailwindlabs/headlessui/pull/2929)) - Add `hidden` attribute to internal `` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955)) +- Allow setting custom `tabIndex` on the `` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966)) ## [1.7.17] - 2024-01-08 diff --git a/packages/@headlessui-vue/src/components/switch/switch.test.tsx b/packages/@headlessui-vue/src/components/switch/switch.test.tsx index 0b6c510..c4008b4 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-vue/src/components/switch/switch.test.tsx @@ -327,6 +327,38 @@ describe('Rendering', () => { expect(handleChange).toHaveBeenNthCalledWith(3, true) }) }) + + describe('`tabIndex` attribute', () => { + it('should have a default tabIndex of `0`', () => { + renderTemplate(html`Enable notifications`) + + assertSwitch({ + state: SwitchState.Off, + label: 'Enable notifications', + attributes: { tabindex: '0' }, + }) + }) + + it('should be possible to override the `tabIndex`', () => { + renderTemplate(html`Enable notifications`) + + assertSwitch({ + state: SwitchState.Off, + label: 'Enable notifications', + attributes: { tabindex: '3' }, + }) + }) + + it('should not be possible to override the `tabIndex` to `-1`', () => { + renderTemplate(html`Enable notifications`) + + assertSwitch({ + state: SwitchState.Off, + label: 'Enable notifications', + attributes: { tabindex: '0' }, + }) + }) + }) }) describe('Render composition', () => { diff --git a/packages/@headlessui-vue/src/components/switch/switch.ts b/packages/@headlessui-vue/src/components/switch/switch.ts index cd10354..4746dee 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.ts +++ b/packages/@headlessui-vue/src/components/switch/switch.ts @@ -78,6 +78,7 @@ export let Switch = defineComponent({ name: { type: String, optional: true }, value: { type: String, optional: true }, id: { type: String, default: () => `headlessui-switch-${useId()}` }, + tabIndex: { type: Number, default: 0 }, }, inheritAttrs: false, setup(props, { emit, attrs, slots, expose }) { @@ -143,14 +144,14 @@ export let Switch = defineComponent({ }) return () => { - let { id, name, value, form, ...theirProps } = props + let { id, name, value, form, tabIndex, ...theirProps } = props let slot = { checked: checked.value } let ourProps = { id, ref: switchRef, role: 'switch', type: type.value, - tabIndex: 0, + tabIndex: tabIndex === -1 ? 0 : tabIndex, 'aria-checked': checked.value, 'aria-labelledby': api?.labelledby.value, 'aria-describedby': api?.describedby.value, diff --git a/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts index 2c7bbd1..5d59ed8 100644 --- a/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts +++ b/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts @@ -978,6 +978,7 @@ export function assertSwitch( textContent?: string label?: string description?: string + attributes?: Record }, switchElement = getSwitch() ) { @@ -985,7 +986,8 @@ export function assertSwitch( if (switchElement === null) return expect(switchElement).not.toBe(null) expect(switchElement).toHaveAttribute('role', 'switch') - expect(switchElement).toHaveAttribute('tabindex', '0') + let tabIndex = Number(switchElement.getAttribute('tabindex') ?? '0') + expect(tabIndex).toBeGreaterThanOrEqual(0) if (options.textContent) { expect(switchElement).toHaveTextContent(options.textContent) @@ -1015,6 +1017,11 @@ export function assertSwitch( default: assertNever(options.state) } + + // Ensure disclosure button has the following attributes + for (let attributeName in options.attributes) { + expect(switchElement).toHaveAttribute(attributeName, options.attributes[attributeName]) + } } catch (err) { if (err instanceof Error) Error.captureStackTrace(err, assertSwitch) throw err