Skip to content

Commit

Permalink
Merge pull request #28 from receter/radio-and-checkbox-group
Browse files Browse the repository at this point in the history
Radio and checkbox group
  • Loading branch information
receter authored Jan 14, 2025
2 parents 00431b3 + 5d7ecbc commit 1c125d3
Show file tree
Hide file tree
Showing 36 changed files with 956 additions and 7 deletions.
158 changes: 158 additions & 0 deletions packages/ui/RadioAndCheckboxGroups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
## Radio Group

In `HTML`, you can group radio buttons by using the `name` attribute. You can do the same with the `Radio` component in sys42. The simplest way to create a group of radio buttons is as follows:

```jsx
<>
<Radio name="myGroup" value="option1" onChange={handleChangeGroup} /> Option 1
<br />
<Radio name="myGroup" value="option2" onChange={handleChangeGroup} /> Option 2
</>
```

While this works, the labels are not associated with the radio buttons. To associate a label with a radio button, you can either associate a label or use the `LabeledControl` component:

```jsx
<>
<LabeledControl
control={
<Radio name="myGroup" value="option1" onChange={handleChangeGroup} />
}
>
Option 1
</LabeledControl>
<LabeledControl
control={
<Radio name="myGroup" value="option2" onChange={handleChangeGroup} />
}
>
Option 2
</LabeledControl>
</>
```

This will render `label` elements that are properly associated with the radio buttons:

```html
<label>
<input name="myGroup" type="radio" value="option1" />
Option 1
</label>
```

### Group Labeling for Accessibility (a11y)

For accessibility reasons, it is recommended to provide a label for the group of radio buttons. In HTML, one common method is to use a `fieldset` and `legend`:

```html
<fieldset>
<legend>Choose an option</legend>
<label>
<input name="myGroup" type="radio" value="option1" />
Option 1
</label>
<label>
<input name="myGroup" type="radio" value="option2" />
Option 2
</label>
</fieldset>
```

Another method is to use `role="radiogroup"` along with `aria-labelledby` or `aria-label` attributes:

```html
<div role="radiogroup" aria-label="Choose an option">
<label>
<input name="myGroup" type="radio" value="option1" />
Option 1
</label>
<label>
<input name="myGroup" type="radio" value="option2" />
Option 2
</label>
</div>
```

### Using the `RadioGroup` Component

You can achieve the same output with the `RadioGroup` component:

```jsx
<RadioGroup
value={selected}
onChangeValue={handleChangeValue}
name="myGroup"
aria-label="Choose an option"
>
<RadioGroupItem value="option1">Option 1</RadioGroupItem>
<RadioGroupItem value="option2">Option 2</RadioGroupItem>
</RadioGroup>
```

If you place the `RadioGroup` inside a `fieldset` or the `FormFieldSet` component, you can omit the `aria-label` in this case, as the `fieldset` provides sufficient semantic structure.

The `name` attribute is not stricly required because the checked state is controlled by React. But a `name` can be set in order to make sure all input elements have the name attribute set.

```jsx
<FormFieldSet label="Choose an option">
<RadioGroup value={selected} onChangeValue={handleChangeValue}>
<RadioGroupItem value="option1">Option 1</RadioGroupItem>
<RadioGroupItem value="option2">Option 2</RadioGroupItem>
</RadioGroup>
</FormFieldSet>
```

This approach eliminates the need for additional `aria` attributes because the semantics of `fieldset` and `legend` are sufficient. The rendered HTML would look like this:

```html
<fieldset>
<legend>Choose an option</legend>
<div role="radiogroup">
<label>
<input type="radio" value="option1" />
Option 1
</label>
<label>
<input type="radio" value="option2" />
Option 2
</label>
</div>
</fieldset>
```

## Checkbox Group

The `CheckboxGroup` component is similar to the `RadioGroup` component. Here is an example of how to use the `CheckboxGroup` component:

```jsx
<FormFieldSet label="Choose options">
<CheckboxGroup
value={checkboxGroupValue}
onChangeValue={handleChangeValue}
onChange={handleChange}
name="myGroup"
>
<CheckboxGroupItem value="option1">Option 1</CheckboxGroupItem>
<CheckboxGroupItem value="option2">Option 2</CheckboxGroupItem>
<CheckboxGroupItem value="option3">Option 3</CheckboxGroupItem>
</CheckboxGroup>
</FormFieldSet>
```

This will render a group of checkboxes with a label:

```html
<fieldset>
<legend>Choose an option</legend>
<div role="group">
<label>
<input name="myGroup" type="radio" value="option1" />
Option 1
</label>
<label>
<input name="myGroup" type="radio" value="option2" />
Option 2
</label>
</div>
</fieldset>
```
87 changes: 87 additions & 0 deletions packages/ui/fixtures/CheckboxGroup.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useValue } from "react-cosmos/client";

import {
CheckboxGroup,
CheckboxGroupItem,
FormFieldSet,
Stack,
} from "../lib/main";

export default function CheckboxGroupFixture() {
const [option1Checked, setOption1Checked] = useValue("option1", {
defaultValue: false,
});
const [option2Checked, setOption2Checked] = useValue("option2", {
defaultValue: false,
});
const [option3Checked, setOption3Checked] = useValue("option3", {
defaultValue: false,
});

const checkboxGroupValue: string[] = [];
if (option1Checked) {
checkboxGroupValue.push("option1");
}
if (option2Checked) {
checkboxGroupValue.push("option2");
}
if (option3Checked) {
checkboxGroupValue.push("option3");
}

function handleChangeValue(values: string[]) {
setOption1Checked(values.includes("option1"));
setOption2Checked(values.includes("option2"));
setOption3Checked(values.includes("option3"));
}

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e);
}

return (
<Stack>
<div>
<h2>Checkbox Group</h2>
<CheckboxGroup
value={checkboxGroupValue}
onChangeValue={handleChangeValue}
onChange={handleChange}
aria-label="Choose an option"
>
<CheckboxGroupItem value="option1">Option 1</CheckboxGroupItem>
<CheckboxGroupItem value="option2">Option 2</CheckboxGroupItem>
<CheckboxGroupItem value="option3">Option 3</CheckboxGroupItem>
</CheckboxGroup>
</div>
<div>
<h2>Checkbox Group in Field Set</h2>
<FormFieldSet label="Choose an option">
<CheckboxGroup
value={checkboxGroupValue}
onChangeValue={handleChangeValue}
onChange={handleChange}
>
<CheckboxGroupItem value="option1">Option 1</CheckboxGroupItem>
<CheckboxGroupItem value="option2">Option 2</CheckboxGroupItem>
<CheckboxGroupItem value="option3">Option 3</CheckboxGroupItem>
</CheckboxGroup>
</FormFieldSet>
</div>
<div>
<h2>Checkbox Group with Text in Between</h2>
<CheckboxGroup
value={checkboxGroupValue}
onChangeValue={handleChangeValue}
onChange={handleChange}
aria-label="Choose an option"
>
<CheckboxGroupItem value="option1">Option 1</CheckboxGroupItem>
<CheckboxGroupItem value="option2">Option 2</CheckboxGroupItem>
<div>Text</div>
<CheckboxGroupItem value="option3">Option 3</CheckboxGroupItem>
</CheckboxGroup>
</div>
</Stack>
);
}
64 changes: 64 additions & 0 deletions packages/ui/fixtures/RadioGroup.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useFixtureSelect } from "react-cosmos/client";

import { FormFieldSet, RadioGroup, RadioGroupItem, Stack } from "../lib/main";

export default function RadioGroupFixture() {
const [selected, setSelected] = useFixtureSelect("Selected", {
options: ["option1", "option2", "option3"],
defaultValue: "default",
});

function handleChangeValue(value: string) {
setSelected(value as "option1" | "option2" | "option3");
}

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e);
}

return (
<Stack>
<div>
<h2>Radio Group</h2>
<RadioGroup
value={selected}
onChangeValue={handleChangeValue}
onChange={handleChange}
aria-label="Choose an option"
>
<RadioGroupItem value="option1">Option 1</RadioGroupItem>
<RadioGroupItem value="option2">Option 2</RadioGroupItem>
<RadioGroupItem value="option3">Option 3</RadioGroupItem>
</RadioGroup>
</div>
<div>
<h2>Radio Group in Field Set</h2>
<FormFieldSet label="Choose an option">
<RadioGroup
value={selected}
onChangeValue={handleChangeValue}
onChange={handleChange}
>
<RadioGroupItem value="option1">Option 1</RadioGroupItem>
<RadioGroupItem value="option2">Option 2</RadioGroupItem>
<RadioGroupItem value="option3">Option 3</RadioGroupItem>
</RadioGroup>
</FormFieldSet>
</div>
<div>
<h2>Radio Group with Text in Between</h2>
<RadioGroup
value={selected}
onChangeValue={handleChangeValue}
onChange={handleChange}
aria-label="Choose an option"
>
<RadioGroupItem value="option1">Option 1</RadioGroupItem>
<RadioGroupItem value="option2">Option 2</RadioGroupItem>
<div>Text</div>
<RadioGroupItem value="option3">Option 3</RadioGroupItem>
</RadioGroup>
</div>
</Stack>
);
}
17 changes: 17 additions & 0 deletions packages/ui/lib/CheckboxGroup/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createComponent } from "../helpers";

import { renderCheckboxGroup } from "./render";
import { CheckboxGroupProps, useCheckboxGroup } from "./useCheckboxGroup";

export const CheckboxGroup = createComponent<CheckboxGroupProps, "div">(
"div",
(hookOptions) => {
const { elementProps, elementRef, renderArgs } =
useCheckboxGroup(hookOptions);
return (
<div {...elementProps} ref={elementRef}>
{renderCheckboxGroup(renderArgs)}
</div>
);
},
);
11 changes: 11 additions & 0 deletions packages/ui/lib/CheckboxGroup/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext } from "react";

export type CheckboxGroupContextType = {
value: string[];
name?: string;
onChangeCheckbox: (event: React.ChangeEvent<HTMLInputElement>) => void;
};

export const CheckboxGroupContext = createContext<
CheckboxGroupContextType | undefined
>(undefined);
17 changes: 17 additions & 0 deletions packages/ui/lib/CheckboxGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type {
BaseCheckboxGroupProps,
BaseCheckboxGroupRenderArgs,
} from "./useBaseCheckboxGroup";
import type { CheckboxGroupProps } from "./useCheckboxGroup";

export { CheckboxGroup } from "./CheckboxGroup";
export { useCheckboxGroup } from "./useCheckboxGroup";
export { useBaseCheckboxGroup } from "./useBaseCheckboxGroup";
export { renderCheckboxGroup } from "./render";
export { CheckboxGroupContext } from "./context";

export type {
BaseCheckboxGroupProps,
BaseCheckboxGroupRenderArgs,
CheckboxGroupProps,
};
12 changes: 12 additions & 0 deletions packages/ui/lib/CheckboxGroup/render.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CheckboxGroupContext } from "./context";
import { BaseCheckboxGroupRenderArgs } from "./useBaseCheckboxGroup";

export function renderCheckboxGroup(args: BaseCheckboxGroupRenderArgs) {
const { children, ctx } = args;

return (
<CheckboxGroupContext.Provider value={ctx}>
{children}
</CheckboxGroupContext.Provider>
);
}
Loading

0 comments on commit 1c125d3

Please sign in to comment.