Skip to content

Commit

Permalink
RadioButton/Checkbox: Add describedby as a prop (#411)
Browse files Browse the repository at this point in the history
* ariaprops poc

* add aria props poc

* add aria-describedby to checkbox + radio

* changeset
  • Loading branch information
somethiiing committed May 13, 2024
1 parent f94fd5c commit df4e035
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-shrimps-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cambly/syntax-core": minor
---

RadioButton/Checkbox: Add aria-describedby as a prop on both of those components for accessibility.
36 changes: 35 additions & 1 deletion packages/syntax-core/src/Checkbox/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type StoryObj, type Meta } from "@storybook/react";
import Checkbox from "./Checkbox";
import React, { useState } from "react";
import Box from "../Box/Box";
import Typography from "../Typography/Typography";

export default {
title: "Components/Checkbox",
Expand All @@ -19,6 +20,7 @@ export default {
error: false,
size: "md",
"data-testid": "",
"aria-describedby": "",
},
argTypes: {
checked: {
Expand All @@ -42,14 +44,23 @@ export const Default: StoryObj<typeof Checkbox> = {};

const CheckboxInteractive = ({
label = "checkbox label",
"aria-describedby": ariaDescribedby,
}: {
label?: string;
"aria-describedby"?: string;
}) => {
const [isChecked, setIsChecked] = useState(false);
const handleChange = () => {
setIsChecked(!isChecked);
};
return <Checkbox checked={isChecked} onChange={handleChange} label={label} />;
return (
<Checkbox
aria-describedby={ariaDescribedby}
checked={isChecked}
onChange={handleChange}
label={label}
/>
);
};

export const Interactive: StoryObj<typeof Checkbox> = {
Expand Down Expand Up @@ -90,3 +101,26 @@ export const FixDoesNotBlowoutHeight: StoryObj<typeof Checkbox> = {
</Box>
),
};

export const Accessibility: StoryObj<typeof Checkbox> = {
render: () => (
<Box
display="flex"
direction="column"
height="100px"
overflowY="auto"
padding={3}
gap={3}
>
<CheckboxInteractive aria-describedby="checkboxes" />
<CheckboxInteractive aria-describedby="checkboxes" />
<CheckboxInteractive aria-describedby="checkboxes" />
<CheckboxInteractive aria-describedby="checkboxes" />
<CheckboxInteractive aria-describedby="checkboxes" />

<Box id="checkboxes">
<Typography>This is text that describes the checkbox above!</Typography>
</Box>
</Box>
),
};
8 changes: 8 additions & 0 deletions packages/syntax-core/src/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Checkbox = ({
label,
error = false,
onChange,
"aria-describedby": ariaDescribedby,
}: {
/**
* Whether or not the box has been clicked
Expand Down Expand Up @@ -68,6 +69,12 @@ const Checkbox = ({
* @defaultValue false
*/
error?: boolean;
/**
* The aria-describedby attribute for the checkbox. This aria- prop identifies the element that describes the element on which the attribute is set.
*
* @defaultValue undefined
*/
"aria-describedby"?: string;
}): ReactElement => {
const isHydrated = useIsHydrated();
const disabled = !isHydrated || disabledProp;
Expand Down Expand Up @@ -108,6 +115,7 @@ const Checkbox = ({
</div>
<input
data-testid={dataTestId}
aria-describedby={ariaDescribedby}
type="checkbox"
className={classNames(
styles.inputOverlay,
Expand Down
24 changes: 23 additions & 1 deletion packages/syntax-core/src/RadioButton/RadioButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type StoryObj, type Meta } from "@storybook/react";
import RadioButton from "./RadioButton";
import React, { useState } from "react";
import Box from "../Box/Box";
import Typography from "../Typography/Typography";

export default {
title: "Components/RadioButton",
Expand All @@ -23,6 +24,7 @@ export default {
value: "value",
id: "",
dangerouslyForceFocusStyles: false,
"aria-describedby": "",
},
argTypes: {
checked: {
Expand All @@ -43,7 +45,11 @@ export default {

export const Default: StoryObj<typeof RadioButton> = {};

const RadioButtonInteractive = () => {
const RadioButtonInteractive = ({
"aria-describedby": ariaDescribedby,
}: {
"aria-describedby"?: string;
}) => {
const [selectedOption, setSelectedOption] = useState("");
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedOption(event.target.value);
Expand All @@ -52,20 +58,23 @@ const RadioButtonInteractive = () => {
<form>
<Box display="flex" direction="column" gap={3}>
<RadioButton
aria-describedby={ariaDescribedby}
checked={selectedOption === "mage"}
value="mage"
onChange={handleChange}
name="rpg_class"
label="Label with a lot of text to test the responsive behavior of the component"
/>
<RadioButton
aria-describedby={ariaDescribedby}
checked={selectedOption === "warrior"}
value="warrior"
onChange={handleChange}
name="rpg_class"
label="Warrior"
/>
<RadioButton
aria-describedby={ariaDescribedby}
checked={selectedOption === "archer"}
value="archer"
onChange={handleChange}
Expand All @@ -80,3 +89,16 @@ const RadioButtonInteractive = () => {
export const Interactive: StoryObj<typeof RadioButton> = {
render: () => <RadioButtonInteractive />,
};

export const Accessibility: StoryObj<typeof RadioButton> = {
render: () => (
<Box display="flex" direction="column" gap={4}>
<RadioButtonInteractive aria-describedby="radio-buttons" />
<Box id="radio-buttons">
<Typography>
This is a description of the radio buttons above.
</Typography>
</Box>
</Box>
),
};
8 changes: 8 additions & 0 deletions packages/syntax-core/src/RadioButton/RadioButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const RadioButton = ({
onChange,
size = "md",
value,
"aria-describedby": ariaDescribedby,
}: {
/**
* Whether or not the radio button is checked
Expand Down Expand Up @@ -78,6 +79,12 @@ const RadioButton = ({
value: string | number;
/** forces focus ring styling */
dangerouslyForceFocusStyles?: boolean;
/**
* The aria-describedby attribute for the RadioButton. This aria- prop identifies the element that describes the element on which the attribute is set.
*
* @defaultValue undefined
*/
"aria-describedby"?: string;
}): ReactElement => {
const isHydrated = useIsHydrated();
const disabled = !isHydrated || disabledProp;
Expand Down Expand Up @@ -122,6 +129,7 @@ const RadioButton = ({
/>
)}
<input
aria-describedby={ariaDescribedby}
data-testid={dataTestId}
type="radio"
id={id}
Expand Down

0 comments on commit df4e035

Please sign in to comment.