Skip to content

Commit

Permalink
Enable native label behavior for Switch component (#2265)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
deebov and thecrypticace authored Feb 28, 2023
1 parent 213dd52 commit de7ddf9
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

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

## [1.7.12] - 2023-02-24

Expand Down
1 change: 1 addition & 0 deletions packages/@headlessui-react/src/components/label/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(

if (passive) {
if ('onClick' in ourProps) {
delete (ourProps as any)['htmlFor']
delete (ourProps as any)['onClick']
}

Expand Down
28 changes: 27 additions & 1 deletion packages/@headlessui-react/src/components/switch/switch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
getSwitchLabel,
getByText,
} from '../../test-utils/accessibility-assertions'
import { press, click, focus, Keys } from '../../test-utils/interactions'
import { press, click, focus, Keys, mouseEnter } from '../../test-utils/interactions'

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

Expand Down Expand Up @@ -595,6 +595,32 @@ describe('Mouse interactions', () => {
// Ensure state is still off
assertSwitch({ state: SwitchState.Off })
})

xit('should be possible to hover the label and trigger a hover on the switch', async () => {
// This test doen't work in JSDOM :(
// Keeping it here for reference when we can test this in a real browser
function Example() {
let [state] = useState(false)
return (
<Switch.Group>
<style>{`.bg{background-color:rgba(0,255,0)}.bg-on-hover:hover{background-color:rgba(255,0,0)}`}</style>
<Switch checked={state} className="bg bg-on-hover" />
<Switch.Label>The label</Switch.Label>
</Switch.Group>
)
}

render(<Example />)

// Verify the switch is not hovered
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(0, 255, 0)')

// Hover over the *label*
await mouseEnter(getSwitchLabel())

// Make sure the switch gets hover styles
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(255, 0, 0)')
})
})

describe('Form compatibility', () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/@headlessui-react/src/components/switch/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
<LabelProvider
name="Switch.Label"
props={{
onClick() {
htmlFor: context.switch?.id,
onClick(event: React.MouseEvent<HTMLLabelElement>) {
if (!switchElement) return
if (event.currentTarget.tagName === 'LABEL') {
event.preventDefault()
}
switchElement.click()
switchElement.focus({ preventScroll: true })
},
Expand Down
4 changes: 3 additions & 1 deletion packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
### Fixed

- Enable native label behavior for `<Switch>` where possible ([#2265](https://github.com/tailwindlabs/headlessui/pull/2265))

## [1.7.11] - 2023-02-24

Expand Down
5 changes: 5 additions & 0 deletions packages/@headlessui-vue/src/components/label/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ export let Label = defineComponent({
// @ts-expect-error props are dynamic via context, some components will provide an onClick
// then we can delete it.
delete ourProps['onClick']

// @ts-expect-error props are dynamic via context, some components will provide an htmlFor
// then we can delete it.
delete ourProps['htmlFor']

// @ts-expect-error props are dynamic via context, some components will provide an onClick
// then we can delete it.
delete theirProps['onClick']
Expand Down
35 changes: 34 additions & 1 deletion packages/@headlessui-vue/src/components/switch/switch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
getSwitchLabel,
getByText,
} from '../../test-utils/accessibility-assertions'
import { press, click, Keys } from '../../test-utils/interactions'
import { press, click, Keys, mouseEnter } from '../../test-utils/interactions'
import { html } from '../../test-utils/html'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'

Expand Down Expand Up @@ -712,6 +712,39 @@ describe('Mouse interactions', () => {
// Ensure state is still Off
assertSwitch({ state: SwitchState.Off })
})

xit('should be possible to hover the label and trigger a hover on the switch', async () => {
// This test doen't work in JSDOM :(
// Keeping it here for reference when we can test this in a real browser
renderTemplate({
template: html`
<SwitchGroup>
<style>
.bg {
background-color: rgba(0, 255, 0);
}
.bg-on-hover:hover {
background-color: rgba(255, 0, 0);
}
</style>
<Switch v-model="checked" className="bg bg-on-hover" />
<SwitchLabel>The label</SwitchLabel>
</SwitchGroup>
`,
setup() {
return { checked: ref(false) }
},
})

// Verify the switch is not hovered
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(0, 255, 0)')

// Hover over the *label*
await mouseEnter(getSwitchLabel())

// Make sure the switch gets hover styles
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(255, 0, 0)')
})
})

describe('Form compatibility', () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/@headlessui-vue/src/components/switch/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ export let SwitchGroup = defineComponent({
let labelledby = useLabels({
name: 'SwitchLabel',
props: {
onClick() {
htmlFor: computed(() => switchRef.value?.id),
onClick(event: MouseEvent & { currentTarget: HTMLElement }) {
if (!switchRef.value) return
if (event.currentTarget.tagName === 'LABEL') {
event.preventDefault()
}
switchRef.value.click()
switchRef.value.focus({ preventScroll: true })
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function Home() {
className={({ checked }) =>
classNames(
'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',
checked ? 'bg-indigo-600' : 'bg-gray-200'
checked ? 'bg-indigo-600 hover:bg-indigo-800' : 'bg-gray-200 hover:bg-gray-400'
)
}
>
Expand Down
15 changes: 10 additions & 5 deletions packages/playground-vue/src/components/switch/switch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
<SwitchGroup as="div" class="flex items-center space-x-4">
<SwitchLabel>Enable notifications</SwitchLabel>

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

<script>
import { defineComponent, h, ref, onMounted, watchEffect, watch } from 'vue'
import { ref } from 'vue'
import { SwitchGroup, Switch, SwitchLabel } from '@headlessui/vue'
function classNames(...classes) {
Expand All @@ -23,15 +28,15 @@ function classNames(...classes) {
export default {
components: { SwitchGroup, Switch, SwitchLabel },
setup(props, context) {
setup() {
let state = ref(false)
return {
state,
resolveSwitchClass({ checked }) {
return classNames(
'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',
checked ? 'bg-indigo-600' : 'bg-gray-200'
'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',
checked ? 'bg-indigo-600 hover:bg-indigo-800' : 'bg-gray-200 hover:bg-gray-400'
)
},
}
Expand Down

0 comments on commit de7ddf9

Please sign in to comment.