Skip to content

Commit e9df8dd

Browse files
committed
Allow setting custom tabIndex on the <Switch /> component (#2966)
* allow setting a custom `tabIndex` on the `<Switch />` component * update changelog
1 parent 04695b2 commit e9df8dd

File tree

8 files changed

+97
-11
lines changed

8 files changed

+97
-11
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Expose `disabled` state on `<Tab />` component ([#2918](https://github.com/tailwindlabs/headlessui/pull/2918))
1919
- Prevent default behaviour when clicking outside of a `Dialog.Panel` ([#2919](https://github.com/tailwindlabs/headlessui/pull/2919))
2020
- Add `hidden` attribute to internal `<Hidden />` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955))
21+
- Allow setting custom `tabIndex` on the `<Switch />` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966))
2122

2223
## [1.7.18] - 2024-01-08
2324

packages/@headlessui-react/src/components/switch/switch.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,47 @@ describe('Rendering', () => {
5959
assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' })
6060
})
6161

62+
describe('`tabIndex` attribute', () => {
63+
it('should have a default tabIndex of `0`', () => {
64+
render(
65+
<Switch checked={false} onChange={console.log}>
66+
<span>Enable notifications</span>
67+
</Switch>
68+
)
69+
assertSwitch({
70+
state: SwitchState.Off,
71+
label: 'Enable notifications',
72+
attributes: { tabindex: '0' },
73+
})
74+
})
75+
76+
it('should be possible to override the `tabIndex`', () => {
77+
render(
78+
<Switch checked={false} onChange={console.log} tabIndex={3}>
79+
<span>Enable notifications</span>
80+
</Switch>
81+
)
82+
assertSwitch({
83+
state: SwitchState.Off,
84+
label: 'Enable notifications',
85+
attributes: { tabindex: '3' },
86+
})
87+
})
88+
89+
it('should not be possible to override the `tabIndex` to `-1`', () => {
90+
render(
91+
<Switch checked={false} onChange={console.log} tabIndex={-1}>
92+
<span>Enable notifications</span>
93+
</Switch>
94+
)
95+
assertSwitch({
96+
state: SwitchState.Off,
97+
label: 'Enable notifications',
98+
attributes: { tabindex: '0' },
99+
})
100+
})
101+
})
102+
62103
describe('`type` attribute', () => {
63104
it('should set the `type` to "button" by default', async () => {
64105
render(

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,7 @@ let DEFAULT_SWITCH_TAG = 'button' as const
102102
interface SwitchRenderPropArg {
103103
checked: boolean
104104
}
105-
type SwitchPropsWeControl =
106-
| 'aria-checked'
107-
| 'aria-describedby'
108-
| 'aria-labelledby'
109-
| 'role'
110-
| 'tabIndex'
105+
type SwitchPropsWeControl = 'aria-checked' | 'aria-describedby' | 'aria-labelledby' | 'role'
111106

112107
export type SwitchProps<TTag extends ElementType> = Props<
113108
TTag,
@@ -120,6 +115,7 @@ export type SwitchProps<TTag extends ElementType> = Props<
120115
name?: string
121116
value?: string
122117
form?: string
118+
tabIndex?: number
123119
}
124120
>
125121

@@ -172,7 +168,7 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
172168
ref: switchRef,
173169
role: 'switch',
174170
type: useResolveButtonType(props, internalSwitchRef),
175-
tabIndex: 0,
171+
tabIndex: props.tabIndex === -1 ? 0 : props.tabIndex ?? 0,
176172
'aria-checked': checked,
177173
'aria-labelledby': groupContext?.labelledby,
178174
'aria-describedby': groupContext?.describedby,

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,14 +978,16 @@ export function assertSwitch(
978978
textContent?: string
979979
label?: string
980980
description?: string
981+
attributes?: Record<string, string | null>
981982
},
982983
switchElement = getSwitch()
983984
) {
984985
try {
985986
if (switchElement === null) return expect(switchElement).not.toBe(null)
986987

987988
expect(switchElement).toHaveAttribute('role', 'switch')
988-
expect(switchElement).toHaveAttribute('tabindex', '0')
989+
let tabIndex = Number(switchElement.getAttribute('tabindex') ?? '0')
990+
expect(tabIndex).toBeGreaterThanOrEqual(0)
989991

990992
if (options.textContent) {
991993
expect(switchElement).toHaveTextContent(options.textContent)
@@ -1015,6 +1017,11 @@ export function assertSwitch(
10151017
default:
10161018
assertNever(options.state)
10171019
}
1020+
1021+
// Ensure disclosure button has the following attributes
1022+
for (let attributeName in options.attributes) {
1023+
expect(switchElement).toHaveAttribute(attributeName, options.attributes[attributeName])
1024+
}
10181025
} catch (err) {
10191026
if (err instanceof Error) Error.captureStackTrace(err, assertSwitch)
10201027
throw err

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
- Prevent default behaviour when clicking outside of a `DialogPanel` ([#2919](https://github.com/tailwindlabs/headlessui/pull/2919))
1414
- Don’t override explicit `disabled` prop for components inside `<MenuItem>` ([#2929](https://github.com/tailwindlabs/headlessui/pull/2929))
1515
- Add `hidden` attribute to internal `<Hidden />` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955))
16+
- Allow setting custom `tabIndex` on the `<Switch />` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966))
1617

1718
## [1.7.19] - 2024-02-07
1819

packages/@headlessui-vue/src/components/switch/switch.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,38 @@ describe('Rendering', () => {
327327
expect(handleChange).toHaveBeenNthCalledWith(3, true)
328328
})
329329
})
330+
331+
describe('`tabIndex` attribute', () => {
332+
it('should have a default tabIndex of `0`', () => {
333+
renderTemplate(html`<Switch :checked="false" :tabIndex="0">Enable notifications</Switch>`)
334+
335+
assertSwitch({
336+
state: SwitchState.Off,
337+
label: 'Enable notifications',
338+
attributes: { tabindex: '0' },
339+
})
340+
})
341+
342+
it('should be possible to override the `tabIndex`', () => {
343+
renderTemplate(html`<Switch :checked="false" :tabIndex="3">Enable notifications</Switch>`)
344+
345+
assertSwitch({
346+
state: SwitchState.Off,
347+
label: 'Enable notifications',
348+
attributes: { tabindex: '3' },
349+
})
350+
})
351+
352+
it('should not be possible to override the `tabIndex` to `-1`', () => {
353+
renderTemplate(html`<Switch :checked="false" :tabIndex="-1">Enable notifications</Switch>`)
354+
355+
assertSwitch({
356+
state: SwitchState.Off,
357+
label: 'Enable notifications',
358+
attributes: { tabindex: '0' },
359+
})
360+
})
361+
})
330362
})
331363

332364
describe('Render composition', () => {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export let Switch = defineComponent({
7878
name: { type: String, optional: true },
7979
value: { type: String, optional: true },
8080
id: { type: String, default: null },
81+
tabIndex: { type: Number, default: 0 },
8182
},
8283
inheritAttrs: false,
8384
setup(props, { emit, attrs, slots, expose }) {
@@ -145,14 +146,14 @@ export let Switch = defineComponent({
145146
})
146147

147148
return () => {
148-
let { name, value, form, ...theirProps } = props
149+
let { name, value, form, tabIndex, ...theirProps } = props
149150
let slot = { checked: checked.value }
150151
let ourProps = {
151152
id,
152153
ref: switchRef,
153154
role: 'switch',
154155
type: type.value,
155-
tabIndex: 0,
156+
tabIndex: tabIndex === -1 ? 0 : tabIndex,
156157
'aria-checked': checked.value,
157158
'aria-labelledby': api?.labelledby.value,
158159
'aria-describedby': api?.describedby.value,

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,14 +978,16 @@ export function assertSwitch(
978978
textContent?: string
979979
label?: string
980980
description?: string
981+
attributes?: Record<string, string | null>
981982
},
982983
switchElement = getSwitch()
983984
) {
984985
try {
985986
if (switchElement === null) return expect(switchElement).not.toBe(null)
986987

987988
expect(switchElement).toHaveAttribute('role', 'switch')
988-
expect(switchElement).toHaveAttribute('tabindex', '0')
989+
let tabIndex = Number(switchElement.getAttribute('tabindex') ?? '0')
990+
expect(tabIndex).toBeGreaterThanOrEqual(0)
989991

990992
if (options.textContent) {
991993
expect(switchElement).toHaveTextContent(options.textContent)
@@ -1015,6 +1017,11 @@ export function assertSwitch(
10151017
default:
10161018
assertNever(options.state)
10171019
}
1020+
1021+
// Ensure disclosure button has the following attributes
1022+
for (let attributeName in options.attributes) {
1023+
expect(switchElement).toHaveAttribute(attributeName, options.attributes[attributeName])
1024+
}
10181025
} catch (err) {
10191026
if (err instanceof Error) Error.captureStackTrace(err, assertSwitch)
10201027
throw err

0 commit comments

Comments
 (0)