Skip to content

Commit

Permalink
add Switch.Description component for Vue
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Feb 1, 2021
1 parent 89dece8 commit 07a8363
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 4 deletions.
62 changes: 59 additions & 3 deletions packages/@headlessui-vue/src/components/switch/switch.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineComponent, ref, watch } from 'vue'
import { render } from '../../test-utils/vue-testing-library'

import { Switch, SwitchLabel, SwitchGroup } from './switch'
import { Switch, SwitchLabel, SwitchDescription, SwitchGroup } from './switch'
import {
SwitchState,
assertSwitch,
Expand All @@ -15,7 +15,7 @@ import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
jest.mock('../../hooks/use-id')

function renderTemplate(input: string | Partial<Parameters<typeof defineComponent>[0]>) {
let defaultComponents = { Switch, SwitchLabel, SwitchGroup }
let defaultComponents = { Switch, SwitchLabel, SwitchDescription, SwitchGroup }

if (typeof input === 'string') {
return render(defineComponent({ template: input, components: defaultComponents }))
Expand All @@ -31,7 +31,10 @@ function renderTemplate(input: string | Partial<Parameters<typeof defineComponen
}

describe('Safe guards', () => {
it.each([['SwitchLabel', SwitchLabel]])(
it.each([
['SwitchLabel', SwitchLabel],
['SwitchDescription', SwitchDescription],
])(
'should error when we are using a <%s /> without a parent <SwitchGroup />',
suppressConsoleLogs((name, Component) => {
expect(() => render(Component)).toThrowError(
Expand Down Expand Up @@ -165,6 +168,59 @@ describe('Render composition', () => {
// Thus: Label A should not be part of the "label" in this case
assertSwitch({ state: SwitchState.Off, label: 'Label B' })
})

it('should be possible to render a Switch.Group, Switch and Switch.Description (before the Switch)', async () => {
renderTemplate({
template: `
<SwitchGroup>
<SwitchDescription>This is an important feature</SwitchDescription>
<Switch v-model="checked" />
</SwitchGroup>
`,
setup: () => ({ checked: ref(false) }),
})

await new Promise(requestAnimationFrame)

assertSwitch({ state: SwitchState.Off, description: 'This is an important feature' })
})

it('should be possible to render a Switch.Group, Switch and Switch.Description (after the Switch)', async () => {
renderTemplate({
template: `
<SwitchGroup>
<Switch v-model="checked" />
<SwitchDescription>This is an important feature</SwitchDescription>
</SwitchGroup>
`,
setup: () => ({ checked: ref(false) }),
})

await new Promise(requestAnimationFrame)

assertSwitch({ state: SwitchState.Off, description: 'This is an important feature' })
})

it('should be possible to render a Switch.Group, Switch, Switch.Label and Switch.Description', async () => {
renderTemplate({
template: `
<SwitchGroup>
<SwitchLabel>Label A</SwitchLabel>
<Switch v-model="checked" />
<SwitchDescription>This is an important feature</SwitchDescription>
</SwitchGroup>
`,
setup: () => ({ checked: ref(false) }),
})

await new Promise(requestAnimationFrame)

assertSwitch({
state: SwitchState.Off,
label: 'Label A',
description: 'This is an important feature',
})
})
})

describe('Keyboard interactions', () => {
Expand Down
35 changes: 34 additions & 1 deletion packages/@headlessui-vue/src/components/switch/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type StateDefinition = {
// State
switchRef: Ref<HTMLButtonElement | null>
labelRef: Ref<HTMLLabelElement | null>
descriptionRef: Ref<HTMLParagraphElement | null>
}

let GroupContext = Symbol('GroupContext') as InjectionKey<StateDefinition>
Expand All @@ -35,8 +36,9 @@ export let SwitchGroup = defineComponent({
setup(props, { slots, attrs }) {
let switchRef = ref<StateDefinition['switchRef']['value']>(null)
let labelRef = ref<StateDefinition['labelRef']['value']>(null)
let descriptionRef = ref<StateDefinition['descriptionRef']['value']>(null)

let api = { switchRef, labelRef }
let api = { switchRef, labelRef, descriptionRef }

provide(GroupContext, api)

Expand All @@ -60,6 +62,7 @@ export let Switch = defineComponent({
let { class: defaultClass, className = defaultClass } = this.$props

let labelledby = computed(() => api?.labelRef.value?.id)
let describedby = computed(() => api?.descriptionRef.value?.id)

let slot = { checked: this.$props.modelValue }
let propsWeControl = {
Expand All @@ -70,6 +73,7 @@ export let Switch = defineComponent({
class: resolvePropValue(className, slot),
'aria-checked': this.$props.modelValue,
'aria-labelledby': labelledby.value,
'aria-describedby': describedby.value,
onClick: this.handleClick,
onKeyUp: this.handleKeyUp,
onKeyPress: this.handleKeyPress,
Expand Down Expand Up @@ -146,3 +150,32 @@ export let SwitchLabel = defineComponent({
}
},
})

// ---

export let SwitchDescription = defineComponent({
name: 'SwitchDescription',
props: { as: { type: [Object, String], default: 'p' } },
render() {
let propsWeControl = {
id: this.id,
ref: 'el',
}

return render({
props: { ...this.$props, ...propsWeControl },
slot: {},
attrs: this.$attrs,
slots: this.$slots,
})
},
setup() {
let api = useGroupContext('SwitchDescription')
let id = `headlessui-switch-description-${useId()}`

return {
id,
el: api.descriptionRef,
}
},
})
1 change: 1 addition & 0 deletions packages/@headlessui-vue/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ it('should expose the correct components', () => {
'SwitchGroup',
'Switch',
'SwitchLabel',
'SwitchDescription',
])
})
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export function assertSwitch(
tag?: string
textContent?: string
label?: string
description?: string
},
switchElement = getSwitch()
) {
Expand All @@ -556,6 +557,10 @@ export function assertSwitch(
assertLabelValue(switchElement, options.label)
}

if (options.description) {
assertDescriptionValue(switchElement, options.description)
}

switch (options.state) {
case SwitchState.On:
expect(switchElement).toHaveAttribute('aria-checked', 'true')
Expand Down Expand Up @@ -600,6 +605,15 @@ export function assertLabelValue(element: HTMLElement | null, value: string) {

// ---

export function assertDescriptionValue(element: HTMLElement | null, value: string) {
if (element === null) return expect(element).not.toBe(null)

let id = element.getAttribute('aria-describedby')!
expect(document.getElementById(id)?.textContent).toEqual(value)
}

// ---

export function assertActiveElement(element: HTMLElement | null) {
try {
if (element === null) return expect(element).not.toBe(null)
Expand Down

0 comments on commit 07a8363

Please sign in to comment.