diff --git a/.changeset/dry-foxes-melt.md b/.changeset/dry-foxes-melt.md
new file mode 100644
index 0000000000..637b9c9863
--- /dev/null
+++ b/.changeset/dry-foxes-melt.md
@@ -0,0 +1,5 @@
+---
+"@nextui-org/radio": patch
+---
+
+Fix ensure radio input correctly references description (#2932)
diff --git a/packages/components/radio/__tests__/radio.test.tsx b/packages/components/radio/__tests__/radio.test.tsx
index 4322e60dd5..cd5b51302d 100644
--- a/packages/components/radio/__tests__/radio.test.tsx
+++ b/packages/components/radio/__tests__/radio.test.tsx
@@ -213,6 +213,56 @@ describe("Radio", () => {
expect(radio2).toBeChecked();
});
+
+ it("should support help text description", () => {
+ const {getByRole} = render(
+
+ Option 1
+ ,
+ );
+
+ 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(
+
+
+ Option 1
+
+
+ Option 2
+
+ ,
+ );
+
+ 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", () => {
diff --git a/packages/components/radio/src/radio.tsx b/packages/components/radio/src/radio.tsx
index a72bc656dd..85535004f0 100644
--- a/packages/components/radio/src/radio.tsx
+++ b/packages/components/radio/src/radio.tsx
@@ -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) => {
{children && {children}}
- {description && (
- {description}
- )}
+ {description && {description}}
);
diff --git a/packages/components/radio/src/use-radio.ts b/packages/components/radio/src/use-radio.ts
index b401b76373..6e9c20593b 100644
--- a/packages/components/radio/src/use-radio.ts
+++ b/packages/components/radio/src/use-radio.ts
@@ -87,27 +87,33 @@ export function useRadio(props: UseRadioProps) {
const inputRef = useRef(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,
};
}