-
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
9 changed files
with
538 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,117 @@ | ||
import classnames from 'classnames'; | ||
import { useRef } from 'preact/hooks'; | ||
|
||
import { useArrowKeyNavigation } from '../../hooks/use-arrow-key-navigation'; | ||
import { RadioCheckedIcon, RadioIcon } from '../icons'; | ||
|
||
export type RadioInput<T extends string | number> = { | ||
value: T; | ||
label: string; | ||
subtitle?: string; | ||
disabled?: boolean; | ||
}; | ||
|
||
export type RadioGroup<T extends string | number> = { | ||
inputs: RadioInput<T>[]; | ||
selected?: T; | ||
onChange: (newSelected: T) => void; | ||
|
||
/** Determines the direction in which radios are stacked */ | ||
direction?: 'vertical' | 'horizontal'; | ||
|
||
disabled?: boolean; | ||
'aria-label'?: string; | ||
'aria-labelledby'?: string; | ||
|
||
/** It adds an actual input if provided */ | ||
name?: string; | ||
}; | ||
|
||
function Radio<T extends string | number>({ | ||
input: { value, label, subtitle, disabled }, | ||
isSelected, | ||
onChange, | ||
}: { | ||
input: RadioInput<T>; | ||
isSelected: boolean; | ||
onChange: (newSelected: T) => void; | ||
}) { | ||
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, | ||
})} | ||
onClick={() => !disabled && onChange(value)} | ||
onKeyDown={e => { | ||
if (!disabled && ['Enter', ' '].includes(e.key)) { | ||
e.preventDefault(); | ||
onChange(value); | ||
} | ||
}} | ||
tabIndex={-1} | ||
> | ||
<div className="flex items-center gap-x-1.5"> | ||
{isSelected ? <RadioCheckedIcon /> : <RadioIcon />} | ||
{label} | ||
</div> | ||
{subtitle && ( | ||
<div className="pl-4 ml-1.5 mt-1 text-grey-6 text-sm">{subtitle}</div> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
export default function RadioGroup<T extends string | number>({ | ||
direction = 'horizontal', | ||
inputs, | ||
selected, | ||
onChange, | ||
disabled, | ||
'aria-label': label, | ||
'aria-labelledby': labelledBy, | ||
name, | ||
}: RadioGroup<T>) { | ||
const containerRef = useRef<HTMLDivElement | null>(null); | ||
|
||
useArrowKeyNavigation(containerRef, { | ||
horizontal: direction === 'horizontal', | ||
vertical: direction === 'vertical', | ||
loop: false, | ||
selector: '[role="radio"]:not([aria-disabled="true"])', | ||
}); | ||
|
||
return ( | ||
<> | ||
<div | ||
aria-label={label} | ||
aria-labelledby={labelledBy} | ||
ref={containerRef} | ||
role="radiogroup" | ||
className={classnames('w-full flex gap-1.5', { | ||
'flex-col': direction === 'vertical', | ||
})} | ||
> | ||
{inputs.map((input, index) => { | ||
const isDisabled = disabled ?? input.disabled; | ||
return ( | ||
<Radio | ||
key={`${input.value}${index}`} | ||
input={{ ...input, disabled: isDisabled }} | ||
isSelected={!isDisabled && input.value === selected} | ||
onChange={onChange} | ||
/> | ||
); | ||
})} | ||
</div> | ||
{name && ( | ||
<input type="hidden" name={name} value={selected} disabled={disabled} /> | ||
)} | ||
</> | ||
); | ||
} |
189 changes: 189 additions & 0 deletions
189
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,189 @@ | ||
import Library from '../../Library'; | ||
|
||
export default function RadioGroupPage() { | ||
return ( | ||
<Library.Page | ||
title="Radio group" | ||
intro={ | ||
<p> | ||
<code>RadioGroup</code> is a <code>radiogroup</code> composite | ||
component that includes a list of <code>radio</code>s in it. | ||
</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="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="inputs"> | ||
<Library.Info> | ||
<Library.InfoItem label="description"> | ||
The list of inputs to render. | ||
</Library.InfoItem> | ||
<Library.InfoItem label="type"> | ||
<code> | ||
Array{'<{'} value: T; label: string; subtitle?: string; | ||
disabled?: boolean {'}>'} | ||
</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.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="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. | ||
</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="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="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.Pattern> | ||
</Library.Page> | ||
); | ||
} |
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,29 @@ | ||
import { useState } from 'preact/hooks'; | ||
|
||
import RadioGroup from '../../components/input/RadioGroup'; | ||
|
||
export default function App() { | ||
const [value, setValue] = useState<'one' | 'two'>('one'); | ||
|
||
return ( | ||
<div className="w-full"> | ||
<RadioGroup | ||
aria-label="Items labelled with aria-label" | ||
selected={value} | ||
onChange={setValue} | ||
inputs={[ | ||
{ | ||
value: 'one', | ||
label: 'First', | ||
subtitle: 'This is the first item', | ||
}, | ||
{ | ||
value: 'two', | ||
label: 'Second', | ||
subtitle: 'This is the second item', | ||
}, | ||
]} | ||
/> | ||
</div> | ||
); | ||
} |
31 changes: 31 additions & 0 deletions
31
src/pattern-library/examples/radio-group-aria-labelledby.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,31 @@ | ||
import { useId, useState } from 'preact/hooks'; | ||
|
||
import RadioGroup from '../../components/input/RadioGroup'; | ||
|
||
export default function App() { | ||
const [value, setValue] = useState<'one' | 'two'>('one'); | ||
const labelId = useId(); | ||
|
||
return ( | ||
<div className="w-full flex flex-col gap-2"> | ||
<p id={labelId}>Items labelled with aria-labelledby</p> | ||
<RadioGroup | ||
aria-labelledby={labelId} | ||
selected={value} | ||
onChange={setValue} | ||
inputs={[ | ||
{ | ||
value: 'one', | ||
label: 'First', | ||
subtitle: 'This is the first item', | ||
}, | ||
{ | ||
value: 'two', | ||
label: 'Second', | ||
subtitle: 'This is the second item', | ||
}, | ||
]} | ||
/> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.