Skip to content

Commit b1f069e

Browse files
Support standard HTML aria attribute names
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 <[email protected]>
1 parent 43acb66 commit b1f069e

File tree

2 files changed

+144
-4
lines changed

2 files changed

+144
-4
lines changed

src/index.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ export type DatePickerProps = OmitUnion<
194194
ariaInvalid?: string;
195195
ariaLabelledBy?: string;
196196
ariaRequired?: string;
197+
"aria-describedby"?: string;
198+
"aria-invalid"?: string;
199+
"aria-labelledby"?: string;
200+
"aria-required"?: string;
197201
rangeSeparator?: string;
198202
onChangeRaw?: (
199203
event?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
@@ -1574,6 +1578,22 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
15741578
const customInput = this.props.customInput || <input type="text" />;
15751579
const customInputRef = this.props.customInputRef || "ref";
15761580

1581+
// Build aria props object, only including defined values to avoid
1582+
// overwriting aria attributes that may be set on the custom input
1583+
const ariaProps: Record<string, string> = {};
1584+
const ariaDescribedBy =
1585+
this.props["aria-describedby"] ?? this.props.ariaDescribedBy;
1586+
const ariaInvalid = this.props["aria-invalid"] ?? this.props.ariaInvalid;
1587+
const ariaLabelledBy =
1588+
this.props["aria-labelledby"] ?? this.props.ariaLabelledBy;
1589+
const ariaRequired = this.props["aria-required"] ?? this.props.ariaRequired;
1590+
1591+
if (ariaDescribedBy != null)
1592+
ariaProps["aria-describedby"] = ariaDescribedBy;
1593+
if (ariaInvalid != null) ariaProps["aria-invalid"] = ariaInvalid;
1594+
if (ariaLabelledBy != null) ariaProps["aria-labelledby"] = ariaLabelledBy;
1595+
if (ariaRequired != null) ariaProps["aria-required"] = ariaRequired;
1596+
15771597
return cloneElement(customInput, {
15781598
[customInputRef]: (input: HTMLElement | null) => {
15791599
this.input = input;
@@ -1596,10 +1616,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
15961616
readOnly: this.props.readOnly,
15971617
required: this.props.required,
15981618
tabIndex: this.props.tabIndex,
1599-
"aria-describedby": this.props.ariaDescribedBy,
1600-
"aria-invalid": this.props.ariaInvalid,
1601-
"aria-labelledby": this.props.ariaLabelledBy,
1602-
"aria-required": this.props.ariaRequired,
1619+
...ariaProps,
16031620
});
16041621
};
16051622

src/test/datepicker_test.test.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4273,6 +4273,129 @@ describe("DatePicker", () => {
42734273
});
42744274
});
42754275

4276+
describe("aria attributes on input", () => {
4277+
it("should pass aria-describedby to the input using standard HTML attribute name", () => {
4278+
const { container } = render(
4279+
<DatePicker selected={newDate()} aria-describedby="description-id" />,
4280+
);
4281+
const input = safeQuerySelector(container, "input");
4282+
expect(input.getAttribute("aria-describedby")).toBe("description-id");
4283+
});
4284+
4285+
it("should pass aria-describedby to the input using camelCase prop name", () => {
4286+
const { container } = render(
4287+
<DatePicker selected={newDate()} ariaDescribedBy="description-id" />,
4288+
);
4289+
const input = safeQuerySelector(container, "input");
4290+
expect(input.getAttribute("aria-describedby")).toBe("description-id");
4291+
});
4292+
4293+
it("should prefer standard HTML attribute name over camelCase for aria-describedby", () => {
4294+
const { container } = render(
4295+
<DatePicker
4296+
selected={newDate()}
4297+
aria-describedby="standard-id"
4298+
ariaDescribedBy="camelcase-id"
4299+
/>,
4300+
);
4301+
const input = safeQuerySelector(container, "input");
4302+
expect(input.getAttribute("aria-describedby")).toBe("standard-id");
4303+
});
4304+
4305+
it("should pass aria-invalid to the input using standard HTML attribute name", () => {
4306+
const { container } = render(
4307+
<DatePicker selected={newDate()} aria-invalid="true" />,
4308+
);
4309+
const input = safeQuerySelector(container, "input");
4310+
expect(input.getAttribute("aria-invalid")).toBe("true");
4311+
});
4312+
4313+
it("should pass aria-invalid to the input using camelCase prop name", () => {
4314+
const { container } = render(
4315+
<DatePicker selected={newDate()} ariaInvalid="true" />,
4316+
);
4317+
const input = safeQuerySelector(container, "input");
4318+
expect(input.getAttribute("aria-invalid")).toBe("true");
4319+
});
4320+
4321+
it("should pass aria-labelledby to the input using standard HTML attribute name", () => {
4322+
const { container } = render(
4323+
<DatePicker selected={newDate()} aria-labelledby="label-id" />,
4324+
);
4325+
const input = safeQuerySelector(container, "input");
4326+
expect(input.getAttribute("aria-labelledby")).toBe("label-id");
4327+
});
4328+
4329+
it("should pass aria-labelledby to the input using camelCase prop name", () => {
4330+
const { container } = render(
4331+
<DatePicker selected={newDate()} ariaLabelledBy="label-id" />,
4332+
);
4333+
const input = safeQuerySelector(container, "input");
4334+
expect(input.getAttribute("aria-labelledby")).toBe("label-id");
4335+
});
4336+
4337+
it("should pass aria-required to the input using standard HTML attribute name", () => {
4338+
const { container } = render(
4339+
<DatePicker selected={newDate()} aria-required="true" />,
4340+
);
4341+
const input = safeQuerySelector(container, "input");
4342+
expect(input.getAttribute("aria-required")).toBe("true");
4343+
});
4344+
4345+
it("should pass aria-required to the input using camelCase prop name", () => {
4346+
const { container } = render(
4347+
<DatePicker selected={newDate()} ariaRequired="true" />,
4348+
);
4349+
const input = safeQuerySelector(container, "input");
4350+
expect(input.getAttribute("aria-required")).toBe("true");
4351+
});
4352+
4353+
it("should pass aria attributes to custom input using standard HTML attribute names", () => {
4354+
const { container } = render(
4355+
<DatePicker
4356+
selected={newDate()}
4357+
customInput={<CustomInput />}
4358+
aria-describedby="desc-id"
4359+
aria-invalid="true"
4360+
aria-labelledby="label-id"
4361+
aria-required="true"
4362+
/>,
4363+
);
4364+
const input = safeQuerySelector(container, "input");
4365+
expect(input.getAttribute("aria-describedby")).toBe("desc-id");
4366+
expect(input.getAttribute("aria-invalid")).toBe("true");
4367+
expect(input.getAttribute("aria-labelledby")).toBe("label-id");
4368+
expect(input.getAttribute("aria-required")).toBe("true");
4369+
});
4370+
4371+
it("should preserve custom input's own aria attributes when DatePicker does not specify them", () => {
4372+
// Custom input with its own aria attributes
4373+
const CustomInputWithAria = React.forwardRef<
4374+
HTMLInputElement,
4375+
React.InputHTMLAttributes<HTMLInputElement>
4376+
>((props, ref) => (
4377+
<input
4378+
ref={ref}
4379+
{...props}
4380+
aria-describedby="custom-desc"
4381+
aria-invalid="false"
4382+
/>
4383+
));
4384+
CustomInputWithAria.displayName = "CustomInputWithAria";
4385+
4386+
const { container } = render(
4387+
<DatePicker
4388+
selected={newDate()}
4389+
customInput={<CustomInputWithAria />}
4390+
/>,
4391+
);
4392+
const input = safeQuerySelector(container, "input");
4393+
// Should preserve the custom input's aria attributes since DatePicker didn't specify any
4394+
expect(input.getAttribute("aria-describedby")).toBe("custom-desc");
4395+
expect(input.getAttribute("aria-invalid")).toBe("false");
4396+
});
4397+
});
4398+
42764399
it("should not customize the className attribute if showIcon is set to false", () => {
42774400
const { container } = render(
42784401
<DatePicker selected={newDate("2021-04-15")} />,

0 commit comments

Comments
 (0)