Skip to content

Commit 3070e4a

Browse files
Merge pull request #3817 from SpaNb4/fix/selected-date-announce
Fix: Screen reader does not announce selected date, current month
2 parents 9f1aa6b + 0795e66 commit 3070e4a

File tree

5 files changed

+247
-1
lines changed

5 files changed

+247
-1
lines changed

src/calendar.jsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
isValid,
4242
getYearsPeriod,
4343
DEFAULT_YEAR_ITEM_NUMBER,
44+
getMonthInLocale,
4445
} from "./date_utils";
4546

4647
const DROPDOWN_FOCUS_CLASSNAMES = [
@@ -210,6 +211,7 @@ export default class Calendar extends React.Component {
210211
date: this.getDateInView(),
211212
selectingDate: null,
212213
monthContainer: null,
214+
isRenderAriaLiveMessage: false,
213215
};
214216
}
215217

@@ -311,6 +313,7 @@ export default class Calendar extends React.Component {
311313
handleYearChange = (date) => {
312314
if (this.props.onYearChange) {
313315
this.props.onYearChange(date);
316+
this.setState({ isRenderAriaLiveMessage: true });
314317
}
315318
if (this.props.adjustDateOnChange) {
316319
if (this.props.onSelect) {
@@ -327,6 +330,7 @@ export default class Calendar extends React.Component {
327330
handleMonthChange = (date) => {
328331
if (this.props.onMonthChange) {
329332
this.props.onMonthChange(date);
333+
this.setState({ isRenderAriaLiveMessage: true });
330334
}
331335
if (this.props.adjustDateOnChange) {
332336
if (this.props.onSelect) {
@@ -983,6 +987,38 @@ export default class Calendar extends React.Component {
983987
}
984988
};
985989

990+
renderAriaLiveRegion = () => {
991+
const { startPeriod, endPeriod } = getYearsPeriod(
992+
this.state.date,
993+
this.props.yearItemNumber
994+
);
995+
let ariaLiveMessage;
996+
997+
if (this.props.showYearPicker) {
998+
ariaLiveMessage = `${startPeriod} - ${endPeriod}`;
999+
} else if (
1000+
this.props.showMonthYearPicker ||
1001+
this.props.showQuarterYearPicker
1002+
) {
1003+
ariaLiveMessage = getYear(this.state.date);
1004+
} else {
1005+
ariaLiveMessage = `${getMonthInLocale(
1006+
getMonth(this.state.date),
1007+
this.props.locale
1008+
)} ${getYear(this.state.date)}`;
1009+
}
1010+
1011+
return (
1012+
<span
1013+
role="alert"
1014+
aria-live="polite"
1015+
className="react-datepicker__aria-live"
1016+
>
1017+
{this.state.isRenderAriaLiveMessage && ariaLiveMessage}
1018+
</span>
1019+
);
1020+
};
1021+
9861022
renderChildren = () => {
9871023
if (this.props.children) {
9881024
return (
@@ -1004,6 +1040,7 @@ export default class Calendar extends React.Component {
10041040
showPopperArrow={this.props.showPopperArrow}
10051041
arrowProps={this.props.arrowProps}
10061042
>
1043+
{this.renderAriaLiveRegion()}
10071044
{this.renderPreviousButton()}
10081045
{this.renderNextButton()}
10091046
{this.renderMonths()}

src/index.jsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ export default class DatePicker extends React.Component {
376376
// used to focus day in inline version after month has changed, but not on
377377
// initial render
378378
shouldFocusDayInline: false,
379+
isRenderAriaLiveMessage: false,
379380
};
380381
};
381382

@@ -527,6 +528,7 @@ export default class DatePicker extends React.Component {
527528
this.props.onChangeRaw(event);
528529
}
529530
this.setSelected(date, event, false, monthSelectedIn);
531+
this.setState({ isRenderAriaLiveMessage: true });
530532
if (!this.props.shouldCloseOnSelect || this.props.showTimeSelect) {
531533
this.setPreSelection(date);
532534
} else if (!this.props.inline) {
@@ -670,6 +672,9 @@ export default class DatePicker extends React.Component {
670672
if (this.props.showTimeInput) {
671673
this.setOpen(true);
672674
}
675+
if (this.props.showTimeSelectOnly || this.props.showTimeSelect) {
676+
this.setState({ isRenderAriaLiveMessage: true });
677+
}
673678
this.setState({ inputValue: null });
674679
};
675680

@@ -1015,6 +1020,75 @@ export default class DatePicker extends React.Component {
10151020
);
10161021
};
10171022

1023+
renderAriaLiveRegion = () => {
1024+
const { dateFormat, locale } = this.props;
1025+
const isContainsTime =
1026+
this.props.showTimeInput || this.props.showTimeSelect;
1027+
const longDateFormat = isContainsTime ? "PPPPp" : "PPPP";
1028+
let ariaLiveMessage;
1029+
1030+
if (this.props.selectsRange) {
1031+
ariaLiveMessage = `Selected start date: ${safeDateFormat(
1032+
this.props.startDate,
1033+
{
1034+
dateFormat: longDateFormat,
1035+
locale,
1036+
}
1037+
)}. ${
1038+
this.props.endDate
1039+
? "End date: " +
1040+
safeDateFormat(this.props.endDate, {
1041+
dateFormat: longDateFormat,
1042+
locale,
1043+
})
1044+
: ""
1045+
}`;
1046+
} else {
1047+
if (this.props.showTimeSelectOnly) {
1048+
ariaLiveMessage = `Selected time: ${safeDateFormat(
1049+
this.props.selected,
1050+
{ dateFormat, locale }
1051+
)}`;
1052+
} else if (this.props.showYearPicker) {
1053+
ariaLiveMessage = `Selected year: ${safeDateFormat(
1054+
this.props.selected,
1055+
{ dateFormat: "yyyy", locale }
1056+
)}`;
1057+
} else if (this.props.showMonthYearPicker) {
1058+
ariaLiveMessage = `Selected month: ${safeDateFormat(
1059+
this.props.selected,
1060+
{ dateFormat: "MMMM yyyy", locale }
1061+
)}`;
1062+
} else if (this.props.showQuarterYearPicker) {
1063+
ariaLiveMessage = `Selected quarter: ${safeDateFormat(
1064+
this.props.selected,
1065+
{
1066+
dateFormat: "yyyy, QQQ",
1067+
locale,
1068+
}
1069+
)}`;
1070+
} else {
1071+
ariaLiveMessage = `Selected date: ${safeDateFormat(
1072+
this.props.selected,
1073+
{
1074+
dateFormat: longDateFormat,
1075+
locale,
1076+
}
1077+
)}`;
1078+
}
1079+
}
1080+
1081+
return (
1082+
<span
1083+
role="alert"
1084+
aria-live="polite"
1085+
className="react-datepicker__aria-live"
1086+
>
1087+
{this.state.isRenderAriaLiveMessage && ariaLiveMessage}
1088+
</span>
1089+
);
1090+
};
1091+
10181092
renderDateInput = () => {
10191093
const className = classnames(this.props.className, {
10201094
[outsideClickIgnoreClass]: this.state.open,
@@ -1096,6 +1170,7 @@ export default class DatePicker extends React.Component {
10961170
renderInputContainer() {
10971171
return (
10981172
<div className="react-datepicker__input-container">
1173+
{this.renderAriaLiveRegion()}
10991174
{this.renderDateInput()}
11001175
{this.renderClearButton()}
11011176
</div>

src/stylesheets/datepicker.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,3 +689,15 @@
689689
padding-left: 0.2rem;
690690
height: auto;
691691
}
692+
693+
.react-datepicker__aria-live {
694+
position: absolute;
695+
clip-path: circle(0);
696+
border: 0;
697+
height: 1px;
698+
margin: -1px;
699+
overflow: hidden;
700+
padding: 0;
701+
width: 1px;
702+
white-space: nowrap;
703+
}

test/calendar_test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,4 +1741,74 @@ describe("Calendar", function () {
17411741
expect(childrenContainer).to.have.length(0);
17421742
});
17431743
});
1744+
1745+
describe("should render aria live region after month/year change", () => {
1746+
it("should render aria live region after month change", () => {
1747+
const datePicker = TestUtils.renderIntoDocument(
1748+
<DatePicker selected={utils.newDate()} />
1749+
);
1750+
const dateInput = datePicker.input;
1751+
1752+
TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput));
1753+
1754+
const calendar = TestUtils.scryRenderedComponentsWithType(
1755+
datePicker.calendar,
1756+
Calendar
1757+
)[0];
1758+
const nextNavigationButton = TestUtils.findRenderedDOMComponentWithClass(
1759+
calendar,
1760+
"react-datepicker__navigation--next"
1761+
);
1762+
nextNavigationButton.click();
1763+
1764+
const currentMonthText = TestUtils.findRenderedDOMComponentWithClass(
1765+
calendar,
1766+
"react-datepicker__current-month"
1767+
).textContent;
1768+
1769+
const ariaLiveMessage = TestUtils.findRenderedDOMComponentWithClass(
1770+
calendar,
1771+
"react-datepicker__aria-live"
1772+
).textContent;
1773+
1774+
expect(currentMonthText).to.equal(ariaLiveMessage);
1775+
});
1776+
1777+
it("should render aria live region after year change", () => {
1778+
const datePicker = TestUtils.renderIntoDocument(
1779+
<DatePicker showYearDropdown selected={utils.newDate()} />
1780+
);
1781+
const dateInput = datePicker.input;
1782+
1783+
TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput));
1784+
1785+
const calendar = TestUtils.scryRenderedComponentsWithType(
1786+
datePicker.calendar,
1787+
Calendar
1788+
)[0];
1789+
const yearDropdown = TestUtils.findRenderedDOMComponentWithClass(
1790+
calendar,
1791+
"react-datepicker__year-read-view"
1792+
);
1793+
yearDropdown.click();
1794+
1795+
const option = TestUtils.scryRenderedDOMComponentsWithClass(
1796+
calendar,
1797+
"react-datepicker__year-option"
1798+
)[7];
1799+
option.click();
1800+
1801+
const ariaLiveMessage = TestUtils.findRenderedDOMComponentWithClass(
1802+
calendar,
1803+
"react-datepicker__aria-live"
1804+
).textContent;
1805+
1806+
expect(ariaLiveMessage).to.equal(
1807+
`${utils.getMonthInLocale(
1808+
utils.getMonth(calendar.state.date),
1809+
datePicker.props.locale
1810+
)} ${utils.getYear(calendar.state.date)}`
1811+
);
1812+
});
1813+
});
17441814
});

test/datepicker_test.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1984,7 +1984,7 @@ describe("DatePicker", () => {
19841984
expect(utils.getMinutes(date)).to.equal(22);
19851985
});
19861986
});
1987-
1987+
19881988
it("should selected month when specified minDate same month", () => {
19891989
const selected = utils.newDate("2023-01-09");
19901990
let date = null;
@@ -2040,4 +2040,56 @@ describe("DatePicker", () => {
20402040
});
20412041
expect(date.toString()).to.equal(utils.newDate("2022-01-01").toString());
20422042
});
2043+
2044+
describe("should render aria live region after date selection", () => {
2045+
it("should have correct format if datepicker does not contain time", () => {
2046+
const datePicker = TestUtils.renderIntoDocument(
2047+
<DatePicker selected={utils.newDate()} />
2048+
);
2049+
const dateInput = datePicker.input;
2050+
2051+
TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput));
2052+
TestUtils.Simulate.keyDown(
2053+
ReactDOM.findDOMNode(dateInput),
2054+
getKey("Enter")
2055+
);
2056+
2057+
const ariaLiveMessage = TestUtils.findRenderedDOMComponentWithClass(
2058+
datePicker,
2059+
"react-datepicker__aria-live"
2060+
).textContent;
2061+
2062+
expect(ariaLiveMessage).to.equal(
2063+
`Selected date: ${utils.safeDateFormat(datePicker.props.selected, {
2064+
dateFormat: "PPPP",
2065+
locale: datePicker.props.locale,
2066+
})}`
2067+
);
2068+
});
2069+
2070+
it("should have correct format if datepicker contains time", () => {
2071+
const datePicker = TestUtils.renderIntoDocument(
2072+
<DatePicker showTimeInput selected={utils.newDate()} />
2073+
);
2074+
const dateInput = datePicker.input;
2075+
2076+
TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput));
2077+
TestUtils.Simulate.keyDown(
2078+
ReactDOM.findDOMNode(dateInput),
2079+
getKey("Enter")
2080+
);
2081+
2082+
const ariaLiveMessage = TestUtils.findRenderedDOMComponentWithClass(
2083+
datePicker,
2084+
"react-datepicker__aria-live"
2085+
).textContent;
2086+
2087+
expect(ariaLiveMessage).to.equal(
2088+
`Selected date: ${utils.safeDateFormat(datePicker.props.selected, {
2089+
dateFormat: "PPPPp",
2090+
locale: datePicker.props.locale,
2091+
})}`
2092+
);
2093+
});
2094+
});
20432095
});

0 commit comments

Comments
 (0)