Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting custom tabIndex on the <Switch /> component #2966

Merged
merged 2 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Hidden />` 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 `<Switch />` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966))

## [2.0.0-alpha.4] - 2024-01-03

Expand Down
41 changes: 41 additions & 0 deletions packages/@headlessui-react/src/components/switch/switch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Switch checked={false} onChange={console.log}>
<span>Enable notifications</span>
</Switch>
)
assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '0' },
})
})

it('should be possible to override the `tabIndex`', () => {
render(
<Switch checked={false} onChange={console.log} tabIndex={3}>
<span>Enable notifications</span>
</Switch>
)
assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '3' },
})
})

it('should not be possible to override the `tabIndex` to `-1`', () => {
render(
<Switch checked={false} onChange={console.log} tabIndex={-1}>
<span>Enable notifications</span>
</Switch>
)
assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '0' },
})
})
})

describe('`type` attribute', () => {
it('should set the `type` to "button" by default', async () => {
render(
Expand Down
10 changes: 3 additions & 7 deletions packages/@headlessui-react/src/components/switch/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG> = Props<
TTag,
Expand All @@ -136,6 +131,7 @@ export type SwitchProps<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG> =
form?: string
autoFocus?: boolean
disabled?: boolean
tabIndex?: number
}
>

Expand Down Expand Up @@ -220,7 +216,7 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1145,14 +1145,16 @@ export function assertSwitch(
textContent?: string
label?: string
description?: string
attributes?: Record<string, string | null>
},
switchElement = getSwitch()
) {
try {
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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<MenuItem>` ([#2929](https://github.com/tailwindlabs/headlessui/pull/2929))
- Add `hidden` attribute to internal `<Hidden />` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955))
- Allow setting custom `tabIndex` on the `<Switch />` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966))

## [1.7.17] - 2024-01-08

Expand Down
32 changes: 32 additions & 0 deletions packages/@headlessui-vue/src/components/switch/switch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,38 @@ describe('Rendering', () => {
expect(handleChange).toHaveBeenNthCalledWith(3, true)
})
})

describe('`tabIndex` attribute', () => {
it('should have a default tabIndex of `0`', () => {
renderTemplate(html`<Switch :checked="false" :tabIndex="0">Enable notifications</Switch>`)

assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '0' },
})
})

it('should be possible to override the `tabIndex`', () => {
renderTemplate(html`<Switch :checked="false" :tabIndex="3">Enable notifications</Switch>`)

assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '3' },
})
})

it('should not be possible to override the `tabIndex` to `-1`', () => {
renderTemplate(html`<Switch :checked="false" :tabIndex="-1">Enable notifications</Switch>`)

assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '0' },
})
})
})
})

describe('Render composition', () => {
Expand Down
5 changes: 3 additions & 2 deletions packages/@headlessui-vue/src/components/switch/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -978,14 +978,16 @@ export function assertSwitch(
textContent?: string
label?: string
description?: string
attributes?: Record<string, string | null>
},
switchElement = getSwitch()
) {
try {
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)
Expand Down Expand Up @@ -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
Expand Down