From 7fce110760359d86264b951e5231f86a1a037067 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 1 Feb 2021 18:27:40 +0100 Subject: [PATCH] add Switch.Description component for Vue --- .../src/components/switch/switch.test.tsx | 62 ++++++++++++++++++- .../src/components/switch/switch.ts | 35 ++++++++++- packages/@headlessui-vue/src/index.test.ts | 1 + .../test-utils/accessibility-assertions.ts | 14 +++++ 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/packages/@headlessui-vue/src/components/switch/switch.test.tsx b/packages/@headlessui-vue/src/components/switch/switch.test.tsx index de6cc985aa..a557881b8f 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-vue/src/components/switch/switch.test.tsx @@ -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, @@ -15,7 +15,7 @@ import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' jest.mock('../../hooks/use-id') function renderTemplate(input: string | Partial[0]>) { - let defaultComponents = { Switch, SwitchLabel, SwitchGroup } + let defaultComponents = { Switch, SwitchLabel, SwitchDescription, SwitchGroup } if (typeof input === 'string') { return render(defineComponent({ template: input, components: defaultComponents })) @@ -31,7 +31,10 @@ function renderTemplate(input: string | Partial { - it.each([['SwitchLabel', SwitchLabel]])( + it.each([ + ['SwitchLabel', SwitchLabel], + ['SwitchDescription', SwitchDescription], + ])( 'should error when we are using a <%s /> without a parent ', suppressConsoleLogs((name, Component) => { expect(() => render(Component)).toThrowError( @@ -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: ` + + This is an important feature + + + `, + 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: ` + + + This is an important feature + + `, + 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: ` + + Label A + + This is an important feature + + `, + setup: () => ({ checked: ref(false) }), + }) + + await new Promise(requestAnimationFrame) + + assertSwitch({ + state: SwitchState.Off, + label: 'Label A', + description: 'This is an important feature', + }) + }) }) describe('Keyboard interactions', () => { diff --git a/packages/@headlessui-vue/src/components/switch/switch.ts b/packages/@headlessui-vue/src/components/switch/switch.ts index a2521ac5c3..db14e4fcdf 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.ts +++ b/packages/@headlessui-vue/src/components/switch/switch.ts @@ -9,6 +9,7 @@ type StateDefinition = { // State switchRef: Ref labelRef: Ref + descriptionRef: Ref } let GroupContext = Symbol('GroupContext') as InjectionKey @@ -35,8 +36,9 @@ export let SwitchGroup = defineComponent({ setup(props, { slots, attrs }) { let switchRef = ref(null) let labelRef = ref(null) + let descriptionRef = ref(null) - let api = { switchRef, labelRef } + let api = { switchRef, labelRef, descriptionRef } provide(GroupContext, api) @@ -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 = { @@ -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, @@ -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, + } + }, +}) diff --git a/packages/@headlessui-vue/src/index.test.ts b/packages/@headlessui-vue/src/index.test.ts index 97e1a259b3..abb49acaef 100644 --- a/packages/@headlessui-vue/src/index.test.ts +++ b/packages/@headlessui-vue/src/index.test.ts @@ -23,5 +23,6 @@ it('should expose the correct components', () => { 'SwitchGroup', 'Switch', 'SwitchLabel', + 'SwitchDescription', ]) }) diff --git a/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts index 7957ff29b3..1bb8a3339b 100644 --- a/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts +++ b/packages/@headlessui-vue/src/test-utils/accessibility-assertions.ts @@ -535,6 +535,7 @@ export function assertSwitch( tag?: string textContent?: string label?: string + description?: string }, switchElement = getSwitch() ) { @@ -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') @@ -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)