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 `