Skip to content

Commit

Permalink
fix(radio): ensure radio input correctly references description
Browse files Browse the repository at this point in the history
  • Loading branch information
ryo-manba committed Jun 22, 2024
1 parent 6951b1f commit 3ae6284
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-foxes-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/radio": patch
---

Fix ensure radio input correctly references description (#2932)
50 changes: 50 additions & 0 deletions packages/components/radio/__tests__/radio.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,56 @@ describe("Radio", () => {

expect(radio2).toBeChecked();
});

it("should support help text description", function () {
const {getByRole} = render(
<RadioGroup description="Help text" label="Options">
<Radio value="1">Option 1</Radio>
</RadioGroup>,
);

const group = getByRole("radiogroup");

expect(group).toHaveAttribute("aria-describedby");

const groupDescriptionId = group.getAttribute("aria-describedby");
const groupDescriptionElement = document.getElementById(groupDescriptionId as string);

expect(groupDescriptionElement).toHaveTextContent("Help text");
});

it("should support help text description for the individual radios", function () {
const {getByLabelText} = render(
<RadioGroup description="Help text" label="Options">
<Radio description="Help text for option 1" value="1">
Option 1
</Radio>
<Radio description="Help text for option 2" value="2">
Option 2
</Radio>
</RadioGroup>,
);

const option1 = getByLabelText("Option 1");

expect(option1).toHaveAttribute("aria-describedby");
const option1Description = option1
.getAttribute("aria-describedby")
?.split(" ")
.map((d) => document.getElementById(d)?.textContent)
.join(" ");

expect(option1Description).toBe("Help text for option 1 Help text");

const option2 = getByLabelText("Option 2");
const option2Description = option2
.getAttribute("aria-describedby")
?.split(" ")
.map((d) => document.getElementById(d)?.textContent)
.join(" ");

expect(option2Description).toBe("Help text for option 2 Help text");
});
});

describe("validation", () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/components/radio/src/radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ const Radio = forwardRef<"input", RadioProps>((props, ref) => {
const {
Component,
children,
slots,
classNames,
description,
getBaseProps,
getWrapperProps,
getInputProps,
getLabelProps,
getLabelWrapperProps,
getControlProps,
getDescriptionProps,
} = useRadio({...props, ref});

return (
Expand All @@ -30,9 +29,7 @@ const Radio = forwardRef<"input", RadioProps>((props, ref) => {
</span>
<div {...getLabelWrapperProps()}>
{children && <span {...getLabelProps()}>{children}</span>}
{description && (
<span className={slots.description({class: classNames?.description})}>{description}</span>
)}
{description && <span {...getDescriptionProps()}>{description}</span>}
</div>
</Component>
);
Expand Down
37 changes: 25 additions & 12 deletions packages/components/radio/src/use-radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,27 +87,33 @@ export function useRadio(props: UseRadioProps) {
const inputRef = useRef<HTMLInputElement>(null);

const labelId = useId();
const descriptionId = useId();

const isRequired = useMemo(() => groupContext.isRequired ?? false, [groupContext.isRequired]);
const isInvalid = groupContext.isInvalid;

const ariaRadioProps = useMemo(() => {
const ariaLabel =
otherProps["aria-label"] || typeof children === "string" ? (children as string) : undefined;
const ariaDescribedBy =
otherProps["aria-describedby"] || typeof description === "string"
? (description as string)
: undefined;
[otherProps["aria-describedby"], descriptionId].filter(Boolean).join(" ") || undefined;

return {
id,
isRequired,
isDisabled: isDisabledProp,
"aria-label": ariaLabel,
"aria-label": otherProps["aria-label"],
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
"aria-describedby": ariaDescribedBy,
};
}, [labelId, id, isDisabledProp, isRequired]);
}, [
id,
isDisabledProp,
isRequired,
description,
otherProps["aria-label"],
otherProps["aria-labelledby"],
otherProps["aria-describedby"],
descriptionId,
]);

const {
inputProps,
Expand All @@ -117,8 +123,7 @@ export function useRadio(props: UseRadioProps) {
} = useReactAriaRadio(
{
value,
children,
...groupContext,
children: typeof children === "function" ? true : children,
...ariaRadioProps,
},
groupContext.groupState,
Expand Down Expand Up @@ -251,22 +256,30 @@ export function useRadio(props: UseRadioProps) {
[slots, classNames?.control],
);

const getDescriptionProps: PropGetter = useCallback(
(props = {}) => ({
...props,
id: descriptionId,
className: slots.description({class: classNames?.description}),
}),
[slots, classNames?.description],
);

return {
Component,
children,
slots,
classNames,
description,
isSelected,
isDisabled,
isInvalid,
isFocusVisible,
description,
getBaseProps,
getWrapperProps,
getInputProps,
getLabelProps,
getLabelWrapperProps,
getControlProps,
getDescriptionProps,
};
}

Expand Down

0 comments on commit 3ae6284

Please sign in to comment.