From e9df8dd9303bd07a1601f9e95c548124dc183af0 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 9ebc348d8e..41f8949a72 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Expose `disabled` state on `` component ([#2918](https://github.com/tailwindlabs/headlessui/pull/2918)) - Prevent default behaviour when clicking outside of a `Dialog.Panel` ([#2919](https://github.com/tailwindlabs/headlessui/pull/2919)) - 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.18] - 2024-01-08 diff --git a/packages/@headlessui-react/src/components/switch/switch.test.tsx b/packages/@headlessui-react/src/components/switch/switch.test.tsx index c7615fcaae..11fb52170b 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 ca2f94eec3..a4d6393f65 100644 --- a/packages/@headlessui-react/src/components/switch/switch.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.tsx @@ -102,12 +102,7 @@ let DEFAULT_SWITCH_TAG = 'button' as const interface SwitchRenderPropArg { checked: 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, @@ -120,6 +115,7 @@ export type SwitchProps = Props< name?: string value?: string form?: string + tabIndex?: number } > @@ -172,7 +168,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': groupContext?.labelledby, 'aria-describedby': groupContext?.describedby, diff --git a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts index 2c7bbd169b..5d59ed840b 100644 --- a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts +++ b/packages/@headlessui-react/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 diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index 1ec43196c8..21d72dd3a6 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -13,6 +13,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.19] - 2024-02-07 diff --git a/packages/@headlessui-vue/src/components/switch/switch.test.tsx b/packages/@headlessui-vue/src/components/switch/switch.test.tsx index 2dfd961b16..e6f6e7092f 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 7613fc26fc..abfb677838 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: null }, + tabIndex: { type: Number, default: 0 }, }, inheritAttrs: false, setup(props, { emit, attrs, slots, expose }) { @@ -145,14 +146,14 @@ export let Switch = defineComponent({ }) return () => { - let { name, value, form, ...theirProps } = props + let { 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 2c7bbd169b..5d59ed840b 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