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( ,