fix(radio): ensure radio input correctly references description (#3301)

* fix(radio): ensure radio input correctly references description

* refactor: tweak test
This commit is contained in:
Ryo Matsukawa 2024-06-23 22:01:11 +09:00 committed by GitHub
parent edd48a09cc
commit 42183353a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 82 additions and 17 deletions

View File

@ -0,0 +1,5 @@
---
"@nextui-org/radio": patch
---
Fix ensure radio input correctly references description (#2932)

View File

@ -213,6 +213,56 @@ describe("Radio", () => {
expect(radio2).toBeChecked();
});
it("should support help text description", () => {
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", () => {
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", () => {

View File

@ -9,8 +9,6 @@ const Radio = forwardRef<"input", RadioProps>((props, ref) => {
const {
Component,
children,
slots,
classNames,
description,
getBaseProps,
getWrapperProps,
@ -18,6 +16,7 @@ const Radio = forwardRef<"input", RadioProps>((props, ref) => {
getLabelProps,
getLabelWrapperProps,
getControlProps,
getDescriptionProps,
} = useRadio({...props, ref});
return (
@ -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>
);

View File

@ -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,
@ -117,8 +123,7 @@ export function useRadio(props: UseRadioProps) {
} = useReactAriaRadio(
{
value,
children,
...groupContext,
children: typeof children === "function" ? true : children,
...ariaRadioProps,
},
groupContext.groupState,
@ -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,
};
}