Skip to content

Commit

Permalink
Create RadioGroup component
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Aug 28, 2024
1 parent eb2270f commit 694a9f5
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 0 deletions.
101 changes: 101 additions & 0 deletions src/components/input/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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;
direction?: 'vertical' | 'horizontal';

/** It adds an actual input if provided */
name?: string;
};

function RadioGroupInput<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,
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
ref={containerRef}
role="radiogroup"
className={classnames('w-full flex gap-1.5', {
'flex-col': direction === 'vertical',
})}
>
{inputs.map((input, index) => (
<RadioGroupInput
key={`${input.value}${index}`}
input={input}
isSelected={input.value === selected}
onChange={onChange}
/>
))}
</div>
{name && <input type="hidden" name={name} value={selected} />}
</>
);
}
34 changes: 34 additions & 0 deletions src/pattern-library/components/patterns/input/RadioGroupPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 radios.
</p>
}
>
<Library.Pattern>
<Library.Usage componentName="RadioGroup" />
<Library.Example>
<Library.Demo
title="Basic RadioGroup"
withSource
exampleFile="radio-button-group"
/>
</Library.Example>
</Library.Pattern>

<Library.Pattern title="Working with RadioGroups">
<Library.Example title="TODO" />
</Library.Pattern>

<Library.Pattern title="Component API">
<code>TODO</code>
</Library.Pattern>
</Library.Page>
);
}
69 changes: 69 additions & 0 deletions src/pattern-library/examples/radio-button-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useState } from 'preact/hooks';

import RadioGroup from '../../components/input/RadioGroup';

export default function App() {
const [horizontalValue, setHorizontalValue] = useState<
'one' | 'two' | 'three'
>('one');
const [verticalValue, setVerticalValue] = useState<
'one' | 'two' | 'three' | 'four'
>('two');

return (
<div className="w-full">
<p className="font-bold">Grouped horizontally:</p>
<RadioGroup
selected={horizontalValue}
onChange={setHorizontalValue}
inputs={[
{
value: 'one',
label: 'First',
subtitle: 'This is the first item',
},
{
value: 'two',
label: 'Second',
subtitle: 'This is the second item',
},
{
value: 'three',
label: 'Third',
subtitle: 'This is the third item',
},
]}
/>

<p className="font-bold mt-5">Grouped vertically:</p>
<RadioGroup
selected={verticalValue}
onChange={setVerticalValue}
direction="vertical"
inputs={[
{
value: 'one',
label: 'First',
subtitle: 'This is the first item',
},
{
value: 'two',
label: 'Second',
subtitle: 'This is the second item',
},
{
value: 'three',
label: 'Third',
subtitle: 'This item is disabled',
disabled: true,
},
{
value: 'four',
label: 'Four',
subtitle: 'This is the fourth item',
},
]}
/>
</div>
);
}
7 changes: 7 additions & 0 deletions src/pattern-library/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import InputGroupPage from './components/patterns/input/InputGroupPage';
import InputPage from './components/patterns/input/InputPage';
import OptionButtonPage from './components/patterns/input/OptionButtonPage';
import RadioButtonPage from './components/patterns/input/RadioButtonPage';
import RadioGroupPage from './components/patterns/input/RadioGroupPage';
import TextareaPage from './components/patterns/input/TextareaPage';
import CardPage from './components/patterns/layout/CardPage';
import OverlayPage from './components/patterns/layout/OverlayPage';
Expand Down Expand Up @@ -202,6 +203,12 @@ const routes: PlaygroundRoute[] = [
component: RadioButtonPage,
route: '/input-radio-button',
},
{
title: 'RadioGroup',
group: 'input',
component: RadioGroupPage,
route: '/input-radio-group',
},
{
title: 'Selects',
group: 'input',
Expand Down

0 comments on commit 694a9f5

Please sign in to comment.