-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
592 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import classnames from 'classnames'; | ||
import type { ComponentChildren } from 'preact'; | ||
import { useContext, useRef } from 'preact/hooks'; | ||
|
||
import { useArrowKeyNavigation } from '../../hooks/use-arrow-key-navigation'; | ||
import { RadioCheckedIcon, RadioIcon } from '../icons'; | ||
import RadioGroupContext from './RadioGroupContext'; | ||
|
||
type RadioValue = string | number; | ||
|
||
export type RadioProps<T extends RadioValue> = { | ||
value: T; | ||
/** Content provided as children is vertically aligned with the radio icon */ | ||
children: ComponentChildren; | ||
|
||
/** | ||
* Allows to provide extra content to be displayed under the children, in a | ||
* smaller and more subtle font color. | ||
*/ | ||
subtitle?: ComponentChildren; | ||
|
||
disabled?: boolean; | ||
}; | ||
|
||
function Radio<T extends RadioValue>({ | ||
value, | ||
children, | ||
subtitle, | ||
disabled: radioDisabled, | ||
}: RadioProps<T>) { | ||
const radioGroupContext = useContext(RadioGroupContext); | ||
if (!radioGroupContext) { | ||
throw new Error('RadioGroup.Radio can only be used as RadioGroup child'); | ||
} | ||
|
||
const { selected, disabled = radioDisabled, onChange } = radioGroupContext; | ||
const isSelected = !disabled && selected === value; | ||
|
||
return ( | ||
<div | ||
role="radio" | ||
aria-checked={isSelected} | ||
aria-disabled={disabled} | ||
className={classnames('focus-visible-ring rounded-lg px-3 py-2 grow', { | ||
'bg-grey-2': isSelected, | ||
'hover:bg-grey-1': !isSelected && !disabled, | ||
'opacity-70': disabled, | ||
'cursor-pointer': !disabled, | ||
})} | ||
data-value={value} | ||
onClick={!disabled ? () => onChange(value) : undefined} | ||
onKeyDown={ | ||
disabled | ||
? undefined | ||
: e => { | ||
if (['Enter', ' '].includes(e.key)) { | ||
e.preventDefault(); | ||
onChange(value); | ||
} | ||
} | ||
} | ||
tabIndex={-1} | ||
> | ||
<div className="flex items-center gap-x-1.5"> | ||
{isSelected ? <RadioCheckedIcon /> : <RadioIcon />} | ||
{children} | ||
</div> | ||
{subtitle && ( | ||
<div className="pl-4 ml-1.5 mt-1 text-grey-6 text-sm">{subtitle}</div> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
Radio.displayName = 'RadioGroup.Radio'; | ||
|
||
export type RadioGroup<T extends RadioValue> = { | ||
children: ComponentChildren; | ||
selected?: T; | ||
onChange: (newSelected: T) => void; | ||
|
||
/** | ||
* Determines the direction in which radios are stacked. | ||
* Defaults to 'horizontal'. | ||
*/ | ||
direction?: 'vertical' | 'horizontal'; | ||
|
||
disabled?: boolean; | ||
'aria-label'?: string; | ||
'aria-labelledby'?: string; | ||
|
||
/** | ||
* If provided, adds a hidden form control with the given name and the value | ||
* set to the selected radio's value, for use in form submissions. | ||
*/ | ||
name?: string; | ||
}; | ||
|
||
function RadioGroupMain<T extends RadioValue>({ | ||
direction = 'horizontal', | ||
children, | ||
selected, | ||
onChange, | ||
disabled, | ||
'aria-label': label, | ||
'aria-labelledby': labelledBy, | ||
name, | ||
}: RadioGroup<T>) { | ||
const containerRef = useRef<HTMLDivElement | null>(null); | ||
|
||
useArrowKeyNavigation(containerRef, { | ||
loop: false, | ||
selector: '[role="radio"]:not([aria-disabled="true"])', | ||
focusElement: el => { | ||
onChange(el.dataset.value as T); | ||
el.focus(); | ||
}, | ||
}); | ||
|
||
return ( | ||
<RadioGroupContext.Provider | ||
value={{ selected, disabled, onChange: onChange as any }} | ||
> | ||
<div | ||
aria-label={label} | ||
aria-labelledby={labelledBy} | ||
ref={containerRef} | ||
role="radiogroup" | ||
className={classnames('w-full flex gap-1.5', { | ||
'flex-col': direction === 'vertical', | ||
})} | ||
> | ||
{children} | ||
</div> | ||
{name && ( | ||
<input | ||
type="hidden" | ||
data-testid="hidden-input" | ||
name={name} | ||
value={selected} | ||
disabled={disabled} | ||
/> | ||
)} | ||
</RadioGroupContext.Provider> | ||
); | ||
} | ||
|
||
const RadioGroup = Object.assign(RadioGroupMain, { | ||
Radio, | ||
displayName: 'RadioGroup', | ||
}); | ||
|
||
export default RadioGroup; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { createContext } from 'preact'; | ||
|
||
export type RadioGroupContextType<T = unknown> = { | ||
selected: T | undefined; | ||
disabled?: boolean; | ||
onChange: (newSelected: T) => void; | ||
}; | ||
|
||
const RadioGroupContext = createContext<RadioGroupContextType | null>(null); | ||
|
||
export default RadioGroupContext; |
213 changes: 213 additions & 0 deletions
213
src/pattern-library/components/patterns/input/RadioGroupPage.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
import { Link } from '../../../../components/navigation'; | ||
import Library from '../../Library'; | ||
|
||
export default function RadioGroupPage() { | ||
return ( | ||
<Library.Page | ||
title="Radio group" | ||
intro={ | ||
<p> | ||
<code>RadioGroup</code> is a component implementing the{' '} | ||
<Link | ||
href="https://www.w3.org/WAI/ARIA/apg/patterns/radio/" | ||
target="_blank" | ||
> | ||
Radio Group Pattern | ||
</Link> | ||
. | ||
</p> | ||
} | ||
> | ||
<Library.Pattern> | ||
<Library.Usage componentName="RadioGroup" /> | ||
<Library.Example> | ||
<Library.Demo | ||
title="Basic RadioGroup" | ||
withSource | ||
exampleFile="radio-group-horizontal" | ||
/> | ||
</Library.Example> | ||
</Library.Pattern> | ||
|
||
<Library.Pattern title="Working with RadioGroups"> | ||
<p> | ||
<code>RadioGroup</code> can render a list of radios arranged | ||
horizontally or vertically. Each radio can optionally render a | ||
subtitle, and be individually disabled. | ||
</p> | ||
<p> | ||
Radios can be focused via arrow keys. Right/Left for horizontal{' '} | ||
<code>RadioGroup</code>s, and Up/Down for vertical ones. | ||
</p> | ||
|
||
<Library.Example title="RadioGroup direction"> | ||
<Library.Demo | ||
title="Vertical RadioGroup" | ||
withSource | ||
exampleFile="radio-group-vertical" | ||
/> | ||
</Library.Example> | ||
|
||
<Library.Example title="RadioGroup with complex layout"> | ||
<p> | ||
If the <code>RadioGroup</code> <code>direction</code> does not fit | ||
your needs, you can provide a more complex container around radios, | ||
like a responsive set of columns or a grid layout. | ||
</p> | ||
|
||
<Library.Demo | ||
title="RadioGroup grid layout" | ||
withSource | ||
exampleFile="radio-group-grid-layout" | ||
/> | ||
</Library.Example> | ||
|
||
<Library.Example title="Labelling RadioGroups"> | ||
<p> | ||
There are two ways to label a <code>RadioGroup</code>. Make sure to | ||
use at least one of them. | ||
</p> | ||
|
||
<Library.Demo | ||
title="Via aria-label" | ||
withSource | ||
exampleFile="radio-group-aria-label" | ||
/> | ||
<Library.Demo | ||
title="Via aria-labelledby" | ||
withSource | ||
exampleFile="radio-group-aria-labelledby" | ||
/> | ||
</Library.Example> | ||
</Library.Pattern> | ||
|
||
<Library.Pattern title="Component API"> | ||
<Library.Example title="aria-label"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
Sets the <code>aria-label</code> attribute in the{' '} | ||
<code>RadioGroup</code>. Make sure either this or{' '} | ||
<code>aria-labelledby</code> is used. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code>string | undefined</code> | ||
</Library.InfoItem> | ||
<Library.InfoItem label="default"> | ||
<code>undefined</code> | ||
</Library.InfoItem> | ||
</Library.Info> | ||
</Library.Example> | ||
<Library.Example title="aria-labelledby"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
Sets the <code>aria-labelledby</code> attribute in the{' '} | ||
<code>RadioGroup</code>. Make sure either this or{' '} | ||
<code>aria-label</code> is used. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code>string | undefined</code> | ||
</Library.InfoItem> | ||
<Library.InfoItem label="default"> | ||
<code>undefined</code> | ||
</Library.InfoItem> | ||
</Library.Info> | ||
</Library.Example> | ||
<Library.Example title="children"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
The content to render inside the <code>RadioGroup</code>, | ||
typically a list of <code>RadioGroup.Radio</code> components. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code>ComponentChildren</code> | ||
</Library.InfoItem> | ||
<Library.InfoItem label="default"> | ||
<code>undefined</code> | ||
</Library.InfoItem> | ||
</Library.Info> | ||
</Library.Example> | ||
<Library.Example title="direction"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
Whether the radios should be stacked horizontally or vertically. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code> | ||
{"'"}vertical{"'"} | {"'"}horizontal{"'"} | ||
</code> | ||
</Library.InfoItem> | ||
<Library.InfoItem label="default"> | ||
<code> | ||
{"'"}horizontal{"'"} | ||
</code> | ||
</Library.InfoItem> | ||
</Library.Info> | ||
</Library.Example> | ||
<Library.Example title="disabled"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
If true, it will disable all radios, regardless of the value of | ||
their own <code>disabled</code> prop. | ||
<br /> | ||
Disabled radios are never marked as selected. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code>boolean</code> | ||
</Library.InfoItem> | ||
<Library.InfoItem label="default"> | ||
<code>false</code> | ||
</Library.InfoItem> | ||
</Library.Info> | ||
<Library.Demo | ||
title="Disabled RadioGroup" | ||
withSource | ||
exampleFile="radio-group-disabled" | ||
/> | ||
</Library.Example> | ||
<Library.Example title="name"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
When provided, a hidden <code>input</code> will be included, with | ||
this name and selected value, allowing <code>RadioGroup</code> to | ||
be used as a regular form control. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code>string | undefined</code> | ||
</Library.InfoItem> | ||
<Library.InfoItem label="default"> | ||
<code>undefined</code> | ||
</Library.InfoItem> | ||
</Library.Info> | ||
<Library.Demo | ||
title="Named RadioGroup in form" | ||
withSource | ||
exampleFile="radio-group-in-form" | ||
/> | ||
</Library.Example> | ||
<Library.Example title="onChange"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
A callback invoked when selected radio changes. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code>(newValue: T) {'=>'} void</code> | ||
</Library.InfoItem> | ||
</Library.Info> | ||
</Library.Example> | ||
<Library.Example title="selected"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
The value for currently selected radio. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code>T | undefined</code> | ||
</Library.InfoItem> | ||
<Library.InfoItem label="default"> | ||
<code>undefined</code> | ||
</Library.InfoItem> | ||
</Library.Info> | ||
</Library.Example> | ||
</Library.Pattern> | ||
</Library.Page> | ||
); | ||
} |
Oops, something went wrong.