From 3dd74c18839ebfb8b5e7f58d4c4f33539b3aec15 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 1 Feb 2021 18:12:04 +0100 Subject: [PATCH] add Switch.Description component for React --- .../src/components/switch/switch.test.tsx | 43 ++++++++++++++++++- .../src/components/switch/switch.tsx | 36 +++++++++++++++- .../test-utils/accessibility-assertions.ts | 14 ++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/packages/@headlessui-react/src/components/switch/switch.test.tsx b/packages/@headlessui-react/src/components/switch/switch.test.tsx index 66742cd563..24664d3542 100644 --- a/packages/@headlessui-react/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.test.tsx @@ -15,7 +15,10 @@ import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' jest.mock('../../hooks/use-id') describe('Safe guards', () => { - it.each([['Switch.Label', Switch.Label]])( + it.each([ + ['Switch.Label', Switch.Label], + ['Switch.Description', Switch.Description], + ])( 'should error when we are using a <%s /> without a parent ', suppressConsoleLogs((name, Component) => { expect(() => render(createElement(Component))).toThrowError( @@ -115,6 +118,44 @@ 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)', () => { + render( + + This is an important feature + + + ) + + 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)', () => { + render( + + + This is an important feature + + ) + + 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', () => { + render( + + Label A + + This is an important feature + + ) + + assertSwitch({ + state: SwitchState.Off, + label: 'Label A', + description: 'This is an important feature', + }) + }) }) describe('Keyboard interactions', () => { diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx index 621bd2e2bb..ddc6f5311e 100644 --- a/packages/@headlessui-react/src/components/switch/switch.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.tsx @@ -22,9 +22,11 @@ import { isDisabledReactIssue7711 } from '../../utils/bugs' interface StateDefinition { switch: HTMLButtonElement | null label: HTMLLabelElement | null + description: HTMLParagraphElement | null setSwitch(element: HTMLButtonElement): void setLabel(element: HTMLLabelElement): void + setDescription(element: HTMLParagraphElement): void } let GroupContext = createContext(null) @@ -47,15 +49,25 @@ let DEFAULT_GROUP_TAG = Fragment function Group(props: Props) { let [switchElement, setSwitchElement] = useState(null) let [labelElement, setLabelElement] = useState(null) + let [descriptionElement, setDescriptionElement] = useState(null) let context = useMemo( () => ({ switch: switchElement, - label: labelElement, setSwitch: setSwitchElement, + label: labelElement, setLabel: setLabelElement, + description: descriptionElement, + setDescription: setDescriptionElement, }), - [switchElement, setSwitchElement, labelElement, setLabelElement] + [ + switchElement, + setSwitchElement, + labelElement, + setLabelElement, + descriptionElement, + setDescriptionElement, + ] ) return ( @@ -76,6 +88,8 @@ type SwitchPropsWeControl = | 'role' | 'tabIndex' | 'aria-checked' + | 'aria-labelledby' + | 'aria-describedby' | 'onClick' | 'onKeyUp' | 'onKeyPress' @@ -129,6 +143,7 @@ export function Switch( className: resolvePropValue(className, propsBag), 'aria-checked': checked, 'aria-labelledby': groupContext?.label?.id, + 'aria-describedby': groupContext?.description?.id, onClick: handleClick, onKeyUp: handleKeyUp, onKeyPress: handleKeyPress, @@ -165,5 +180,22 @@ function Label( // --- +let DEFAULT_DESCRIPTIONL_TAG = 'p' as const +interface DescriptionRenderPropArg {} +type DescriptionPropsWeControl = 'id' | 'ref' + +function Description( + props: Props +) { + let state = useGroupContext([Switch.name, Description.name].join('.')) + let id = `headlessui-switch-description-${useId()}` + + let propsWeControl = { ref: state.setDescription, id } + return render({ ...props, ...propsWeControl }, {}, DEFAULT_DESCRIPTIONL_TAG) +} + +// --- + Switch.Group = Group Switch.Label = Label +Switch.Description = Description diff --git a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts b/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts index f9c2a5cf7a..361f47282a 100644 --- a/packages/@headlessui-react/src/test-utils/accessibility-assertions.ts +++ b/packages/@headlessui-react/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') @@ -718,6 +723,15 @@ export function assertLabelValue(element: HTMLElement | null, value: string) { expect(element).toHaveTextContent(value) } +// --- +// +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 getDialogButton(): HTMLElement | null {