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, }; }