Skip to content

Commit de7ddf9

Browse files
Enable native label behavior for Switch component (#2265)
* add native label behavior for switch * Add reference tests for React and Vue These don’t work in JSDOM so they’re skipped but we can use these to reference expected behavior once we have playwright-based tests * Fix Vue playground switch example * Only prevent default when the element is a label * Port change to Vue * Update changelog --------- Co-authored-by: Jordan Pittman <[email protected]>
1 parent 213dd52 commit de7ddf9

File tree

10 files changed

+92
-11
lines changed

10 files changed

+92
-11
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Ensure `Transition` component completes if nothing is transitioning ([#2318](https://github.com/tailwindlabs/headlessui/pull/2318))
13+
- Enable native label behavior for `<Switch>` where possible ([#2265](https://github.com/tailwindlabs/headlessui/pull/2265))
1314

1415
## [1.7.12] - 2023-02-24
1516

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
9999

100100
if (passive) {
101101
if ('onClick' in ourProps) {
102+
delete (ourProps as any)['htmlFor']
102103
delete (ourProps as any)['onClick']
103104
}
104105

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
getSwitchLabel,
1111
getByText,
1212
} from '../../test-utils/accessibility-assertions'
13-
import { press, click, focus, Keys } from '../../test-utils/interactions'
13+
import { press, click, focus, Keys, mouseEnter } from '../../test-utils/interactions'
1414

1515
jest.mock('../../hooks/use-id')
1616

@@ -595,6 +595,32 @@ describe('Mouse interactions', () => {
595595
// Ensure state is still off
596596
assertSwitch({ state: SwitchState.Off })
597597
})
598+
599+
xit('should be possible to hover the label and trigger a hover on the switch', async () => {
600+
// This test doen't work in JSDOM :(
601+
// Keeping it here for reference when we can test this in a real browser
602+
function Example() {
603+
let [state] = useState(false)
604+
return (
605+
<Switch.Group>
606+
<style>{`.bg{background-color:rgba(0,255,0)}.bg-on-hover:hover{background-color:rgba(255,0,0)}`}</style>
607+
<Switch checked={state} className="bg bg-on-hover" />
608+
<Switch.Label>The label</Switch.Label>
609+
</Switch.Group>
610+
)
611+
}
612+
613+
render(<Example />)
614+
615+
// Verify the switch is not hovered
616+
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(0, 255, 0)')
617+
618+
// Hover over the *label*
619+
await mouseEnter(getSwitchLabel())
620+
621+
// Make sure the switch gets hover styles
622+
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(255, 0, 0)')
623+
})
598624
})
599625

600626
describe('Form compatibility', () => {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,12 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
6565
<LabelProvider
6666
name="Switch.Label"
6767
props={{
68-
onClick() {
68+
htmlFor: context.switch?.id,
69+
onClick(event: React.MouseEvent<HTMLLabelElement>) {
6970
if (!switchElement) return
71+
if (event.currentTarget.tagName === 'LABEL') {
72+
event.preventDefault()
73+
}
7074
switchElement.click()
7175
switchElement.focus({ preventScroll: true })
7276
},

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Enable native label behavior for `<Switch>` where possible ([#2265](https://github.com/tailwindlabs/headlessui/pull/2265))
1113

1214
## [1.7.11] - 2023-02-24
1315

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ export let Label = defineComponent({
9090
// @ts-expect-error props are dynamic via context, some components will provide an onClick
9191
// then we can delete it.
9292
delete ourProps['onClick']
93+
94+
// @ts-expect-error props are dynamic via context, some components will provide an htmlFor
95+
// then we can delete it.
96+
delete ourProps['htmlFor']
97+
9398
// @ts-expect-error props are dynamic via context, some components will provide an onClick
9499
// then we can delete it.
95100
delete theirProps['onClick']

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
getSwitchLabel,
1111
getByText,
1212
} from '../../test-utils/accessibility-assertions'
13-
import { press, click, Keys } from '../../test-utils/interactions'
13+
import { press, click, Keys, mouseEnter } from '../../test-utils/interactions'
1414
import { html } from '../../test-utils/html'
1515
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
1616

@@ -712,6 +712,39 @@ describe('Mouse interactions', () => {
712712
// Ensure state is still Off
713713
assertSwitch({ state: SwitchState.Off })
714714
})
715+
716+
xit('should be possible to hover the label and trigger a hover on the switch', async () => {
717+
// This test doen't work in JSDOM :(
718+
// Keeping it here for reference when we can test this in a real browser
719+
renderTemplate({
720+
template: html`
721+
<SwitchGroup>
722+
<style>
723+
.bg {
724+
background-color: rgba(0, 255, 0);
725+
}
726+
.bg-on-hover:hover {
727+
background-color: rgba(255, 0, 0);
728+
}
729+
</style>
730+
<Switch v-model="checked" className="bg bg-on-hover" />
731+
<SwitchLabel>The label</SwitchLabel>
732+
</SwitchGroup>
733+
`,
734+
setup() {
735+
return { checked: ref(false) }
736+
},
737+
})
738+
739+
// Verify the switch is not hovered
740+
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(0, 255, 0)')
741+
742+
// Hover over the *label*
743+
await mouseEnter(getSwitchLabel())
744+
745+
// Make sure the switch gets hover styles
746+
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(255, 0, 0)')
747+
})
715748
})
716749

717750
describe('Form compatibility', () => {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ export let SwitchGroup = defineComponent({
4646
let labelledby = useLabels({
4747
name: 'SwitchLabel',
4848
props: {
49-
onClick() {
49+
htmlFor: computed(() => switchRef.value?.id),
50+
onClick(event: MouseEvent & { currentTarget: HTMLElement }) {
5051
if (!switchRef.value) return
52+
if (event.currentTarget.tagName === 'LABEL') {
53+
event.preventDefault()
54+
}
5155
switchRef.value.click()
5256
switchRef.value.focus({ preventScroll: true })
5357
},

packages/playground-react/pages/switch/switch-with-pure-tailwind.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function Home() {
1818
className={({ checked }) =>
1919
classNames(
2020
'focus:shadow-outline relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none',
21-
checked ? 'bg-indigo-600' : 'bg-gray-200'
21+
checked ? 'bg-indigo-600 hover:bg-indigo-800' : 'bg-gray-200 hover:bg-gray-400'
2222
)
2323
}
2424
>

packages/playground-vue/src/components/switch/switch.vue

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
<SwitchGroup as="div" class="flex items-center space-x-4">
44
<SwitchLabel>Enable notifications</SwitchLabel>
55

6-
<Switch as="button" v-model="state" :className="resolveSwitchClass" v-slot="{ checked }">
6+
<Switch
7+
as="button"
8+
v-model="state"
9+
:class="resolveSwitchClass({ checked: state })"
10+
v-slot="{ checked }"
11+
>
712
<span
813
class="inline-block h-5 w-5 transform rounded-full bg-white transition duration-200 ease-in-out"
914
:class="{ 'translate-x-5': checked, 'translate-x-0': !checked }"
@@ -14,7 +19,7 @@
1419
</template>
1520

1621
<script>
17-
import { defineComponent, h, ref, onMounted, watchEffect, watch } from 'vue'
22+
import { ref } from 'vue'
1823
import { SwitchGroup, Switch, SwitchLabel } from '@headlessui/vue'
1924
2025
function classNames(...classes) {
@@ -23,15 +28,15 @@ function classNames(...classes) {
2328
2429
export default {
2530
components: { SwitchGroup, Switch, SwitchLabel },
26-
setup(props, context) {
31+
setup() {
2732
let state = ref(false)
2833
2934
return {
3035
state,
3136
resolveSwitchClass({ checked }) {
3237
return classNames(
33-
'relative inline-flex flex-shrink-0 h-6 transition-colors duration-200 ease-in-out border-2 border-transparent rounded-full cursor-pointer w-11 focus:outline-none focus:shadow-outline',
34-
checked ? 'bg-indigo-600' : 'bg-gray-200'
38+
'focus:shadow-outline relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none',
39+
checked ? 'bg-indigo-600 hover:bg-indigo-800' : 'bg-gray-200 hover:bg-gray-400'
3540
)
3641
},
3742
}

0 commit comments

Comments
 (0)