Skip to content

Commit c261620

Browse files
Merge pull request #6056 from Hacker0x01/fix/aria-attributes-standard-names
Support standard HTML aria attribute names
2 parents 43acb66 + b1f069e commit c261620

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)