Skip to content

Commit

Permalink
add Switch.Description component for React
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Feb 1, 2021
1 parent 5e6a2b9 commit 3dd74c1
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 3 deletions.
43 changes: 42 additions & 1 deletion packages/@headlessui-react/src/components/switch/switch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Switch.Group />',
suppressConsoleLogs((name, Component) => {
expect(() => render(createElement(Component))).toThrowError(
Expand Down Expand Up @@ -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(
<Switch.Group>
<Switch.Description>This is an important feature</Switch.Description>
<Switch checked={false} onChange={console.log} />
</Switch.Group>
)

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(
<Switch.Group>
<Switch checked={false} onChange={console.log} />
<Switch.Description>This is an important feature</Switch.Description>
</Switch.Group>
)

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(
<Switch.Group>
<Switch.Label>Label A</Switch.Label>
<Switch checked={false} onChange={console.log} />
<Switch.Description>This is an important feature</Switch.Description>
</Switch.Group>
)

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

describe('Keyboard interactions', () => {
Expand Down
36 changes: 34 additions & 2 deletions packages/@headlessui-react/src/components/switch/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<StateDefinition | null>(null)
Expand All @@ -47,15 +49,25 @@ let DEFAULT_GROUP_TAG = Fragment
function Group<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(props: Props<TTag>) {
let [switchElement, setSwitchElement] = useState<HTMLButtonElement | null>(null)
let [labelElement, setLabelElement] = useState<HTMLLabelElement | null>(null)
let [descriptionElement, setDescriptionElement] = useState<HTMLParagraphElement | null>(null)

let context = useMemo<StateDefinition>(
() => ({
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 (
Expand All @@ -76,6 +88,8 @@ type SwitchPropsWeControl =
| 'role'
| 'tabIndex'
| 'aria-checked'
| 'aria-labelledby'
| 'aria-describedby'
| 'onClick'
| 'onKeyUp'
| 'onKeyPress'
Expand Down Expand Up @@ -129,6 +143,7 @@ export function Switch<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
className: resolvePropValue(className, propsBag),
'aria-checked': checked,
'aria-labelledby': groupContext?.label?.id,
'aria-describedby': groupContext?.description?.id,
onClick: handleClick,
onKeyUp: handleKeyUp,
onKeyPress: handleKeyPress,
Expand Down Expand Up @@ -165,5 +180,22 @@ function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(

// ---

let DEFAULT_DESCRIPTIONL_TAG = 'p' as const
interface DescriptionRenderPropArg {}
type DescriptionPropsWeControl = 'id' | 'ref'

function Description<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, DescriptionRenderPropArg, DescriptionPropsWeControl>
) {
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
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 @@ -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 {
Expand Down

0 comments on commit 3dd74c1

Please sign in to comment.