From b1f069eb3bef0df4d8624f65e3cb860db1ba9d1c Mon Sep 17 00:00:00 2001 From: Martijn Russchen Date: Wed, 26 Nov 2025 12:23:24 +0100 Subject: [PATCH] Support standard HTML aria attribute names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for standard HTML aria attribute names (aria-describedby, aria-invalid, aria-labelledby, aria-required) in addition to the existing camelCase prop names (ariaDescribedBy, ariaInvalid, ariaLabelledBy, ariaRequired). The implementation only includes aria props in the cloneElement call when they have actual values. This prevents overwriting aria attributes that may already be defined on a custom input component when the DatePicker doesn't explicitly set them. Standard HTML attribute names take precedence when both are provided. Fixes #5580 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/index.tsx | 25 +++++- src/test/datepicker_test.test.tsx | 123 ++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 8f1ddf89c..5ea00fd14 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -194,6 +194,10 @@ export type DatePickerProps = OmitUnion< ariaInvalid?: string; ariaLabelledBy?: string; ariaRequired?: string; + "aria-describedby"?: string; + "aria-invalid"?: string; + "aria-labelledby"?: string; + "aria-required"?: string; rangeSeparator?: string; onChangeRaw?: ( event?: React.MouseEvent | React.KeyboardEvent, @@ -1574,6 +1578,22 @@ export class DatePicker extends Component { const customInput = this.props.customInput || ; const customInputRef = this.props.customInputRef || "ref"; + // Build aria props object, only including defined values to avoid + // overwriting aria attributes that may be set on the custom input + const ariaProps: Record = {}; + const ariaDescribedBy = + this.props["aria-describedby"] ?? this.props.ariaDescribedBy; + const ariaInvalid = this.props["aria-invalid"] ?? this.props.ariaInvalid; + const ariaLabelledBy = + this.props["aria-labelledby"] ?? this.props.ariaLabelledBy; + const ariaRequired = this.props["aria-required"] ?? this.props.ariaRequired; + + if (ariaDescribedBy != null) + ariaProps["aria-describedby"] = ariaDescribedBy; + if (ariaInvalid != null) ariaProps["aria-invalid"] = ariaInvalid; + if (ariaLabelledBy != null) ariaProps["aria-labelledby"] = ariaLabelledBy; + if (ariaRequired != null) ariaProps["aria-required"] = ariaRequired; + return cloneElement(customInput, { [customInputRef]: (input: HTMLElement | null) => { this.input = input; @@ -1596,10 +1616,7 @@ export class DatePicker extends Component { readOnly: this.props.readOnly, required: this.props.required, tabIndex: this.props.tabIndex, - "aria-describedby": this.props.ariaDescribedBy, - "aria-invalid": this.props.ariaInvalid, - "aria-labelledby": this.props.ariaLabelledBy, - "aria-required": this.props.ariaRequired, + ...ariaProps, }); }; diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index 43b5577e7..94629138d 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -4273,6 +4273,129 @@ describe("DatePicker", () => { }); }); + describe("aria attributes on input", () => { + it("should pass aria-describedby to the input using standard HTML attribute name", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-describedby")).toBe("description-id"); + }); + + it("should pass aria-describedby to the input using camelCase prop name", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-describedby")).toBe("description-id"); + }); + + it("should prefer standard HTML attribute name over camelCase for aria-describedby", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-describedby")).toBe("standard-id"); + }); + + it("should pass aria-invalid to the input using standard HTML attribute name", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-invalid")).toBe("true"); + }); + + it("should pass aria-invalid to the input using camelCase prop name", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-invalid")).toBe("true"); + }); + + it("should pass aria-labelledby to the input using standard HTML attribute name", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-labelledby")).toBe("label-id"); + }); + + it("should pass aria-labelledby to the input using camelCase prop name", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-labelledby")).toBe("label-id"); + }); + + it("should pass aria-required to the input using standard HTML attribute name", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-required")).toBe("true"); + }); + + it("should pass aria-required to the input using camelCase prop name", () => { + const { container } = render( + , + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-required")).toBe("true"); + }); + + it("should pass aria attributes to custom input using standard HTML attribute names", () => { + const { container } = render( + } + aria-describedby="desc-id" + aria-invalid="true" + aria-labelledby="label-id" + aria-required="true" + />, + ); + const input = safeQuerySelector(container, "input"); + expect(input.getAttribute("aria-describedby")).toBe("desc-id"); + expect(input.getAttribute("aria-invalid")).toBe("true"); + expect(input.getAttribute("aria-labelledby")).toBe("label-id"); + expect(input.getAttribute("aria-required")).toBe("true"); + }); + + it("should preserve custom input's own aria attributes when DatePicker does not specify them", () => { + // Custom input with its own aria attributes + const CustomInputWithAria = React.forwardRef< + HTMLInputElement, + React.InputHTMLAttributes + >((props, ref) => ( + + )); + CustomInputWithAria.displayName = "CustomInputWithAria"; + + const { container } = render( + } + />, + ); + const input = safeQuerySelector(container, "input"); + // Should preserve the custom input's aria attributes since DatePicker didn't specify any + expect(input.getAttribute("aria-describedby")).toBe("custom-desc"); + expect(input.getAttribute("aria-invalid")).toBe("false"); + }); + }); + it("should not customize the className attribute if showIcon is set to false", () => { const { container } = render( ,