Skip to content

Commit

Permalink
Merge pull request #3817 from SpaNb4/fix/selected-date-announce
Browse files Browse the repository at this point in the history
Fix: Screen reader does not announce selected date, current month
  • Loading branch information
martijnrusschen authored Jan 30, 2023
2 parents 9f1aa6b + 0795e66 commit 3070e4a
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 1 deletion.
37 changes: 37 additions & 0 deletions src/calendar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
isValid,
getYearsPeriod,
DEFAULT_YEAR_ITEM_NUMBER,
getMonthInLocale,
} from "./date_utils";

const DROPDOWN_FOCUS_CLASSNAMES = [
Expand Down Expand Up @@ -210,6 +211,7 @@ export default class Calendar extends React.Component {
date: this.getDateInView(),
selectingDate: null,
monthContainer: null,
isRenderAriaLiveMessage: false,
};
}

Expand Down Expand Up @@ -311,6 +313,7 @@ export default class Calendar extends React.Component {
handleYearChange = (date) => {
if (this.props.onYearChange) {
this.props.onYearChange(date);
this.setState({ isRenderAriaLiveMessage: true });
}
if (this.props.adjustDateOnChange) {
if (this.props.onSelect) {
Expand All @@ -327,6 +330,7 @@ export default class Calendar extends React.Component {
handleMonthChange = (date) => {
if (this.props.onMonthChange) {
this.props.onMonthChange(date);
this.setState({ isRenderAriaLiveMessage: true });
}
if (this.props.adjustDateOnChange) {
if (this.props.onSelect) {
Expand Down Expand Up @@ -983,6 +987,38 @@ export default class Calendar extends React.Component {
}
};

renderAriaLiveRegion = () => {
const { startPeriod, endPeriod } = getYearsPeriod(
this.state.date,
this.props.yearItemNumber
);
let ariaLiveMessage;

if (this.props.showYearPicker) {
ariaLiveMessage = `${startPeriod} - ${endPeriod}`;
} else if (
this.props.showMonthYearPicker ||
this.props.showQuarterYearPicker
) {
ariaLiveMessage = getYear(this.state.date);
} else {
ariaLiveMessage = `${getMonthInLocale(
getMonth(this.state.date),
this.props.locale
)} ${getYear(this.state.date)}`;
}

return (
<span
role="alert"
aria-live="polite"
className="react-datepicker__aria-live"
>
{this.state.isRenderAriaLiveMessage && ariaLiveMessage}
</span>
);
};

renderChildren = () => {
if (this.props.children) {
return (
Expand All @@ -1004,6 +1040,7 @@ export default class Calendar extends React.Component {
showPopperArrow={this.props.showPopperArrow}
arrowProps={this.props.arrowProps}
>
{this.renderAriaLiveRegion()}
{this.renderPreviousButton()}
{this.renderNextButton()}
{this.renderMonths()}
Expand Down
75 changes: 75 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export default class DatePicker extends React.Component {
// used to focus day in inline version after month has changed, but not on
// initial render
shouldFocusDayInline: false,
isRenderAriaLiveMessage: false,
};
};

Expand Down Expand Up @@ -527,6 +528,7 @@ export default class DatePicker extends React.Component {
this.props.onChangeRaw(event);
}
this.setSelected(date, event, false, monthSelectedIn);
this.setState({ isRenderAriaLiveMessage: true });
if (!this.props.shouldCloseOnSelect || this.props.showTimeSelect) {
this.setPreSelection(date);
} else if (!this.props.inline) {
Expand Down Expand Up @@ -670,6 +672,9 @@ export default class DatePicker extends React.Component {
if (this.props.showTimeInput) {
this.setOpen(true);
}
if (this.props.showTimeSelectOnly || this.props.showTimeSelect) {
this.setState({ isRenderAriaLiveMessage: true });
}
this.setState({ inputValue: null });
};

Expand Down Expand Up @@ -1015,6 +1020,75 @@ export default class DatePicker extends React.Component {
);
};

renderAriaLiveRegion = () => {
const { dateFormat, locale } = this.props;
const isContainsTime =
this.props.showTimeInput || this.props.showTimeSelect;
const longDateFormat = isContainsTime ? "PPPPp" : "PPPP";
let ariaLiveMessage;

if (this.props.selectsRange) {
ariaLiveMessage = `Selected start date: ${safeDateFormat(
this.props.startDate,
{
dateFormat: longDateFormat,
locale,
}
)}. ${
this.props.endDate
? "End date: " +
safeDateFormat(this.props.endDate, {
dateFormat: longDateFormat,
locale,
})
: ""
}`;
} else {
if (this.props.showTimeSelectOnly) {
ariaLiveMessage = `Selected time: ${safeDateFormat(
this.props.selected,
{ dateFormat, locale }
)}`;
} else if (this.props.showYearPicker) {
ariaLiveMessage = `Selected year: ${safeDateFormat(
this.props.selected,
{ dateFormat: "yyyy", locale }
)}`;
} else if (this.props.showMonthYearPicker) {
ariaLiveMessage = `Selected month: ${safeDateFormat(
this.props.selected,
{ dateFormat: "MMMM yyyy", locale }
)}`;
} else if (this.props.showQuarterYearPicker) {
ariaLiveMessage = `Selected quarter: ${safeDateFormat(
this.props.selected,
{
dateFormat: "yyyy, QQQ",
locale,
}
)}`;
} else {
ariaLiveMessage = `Selected date: ${safeDateFormat(
this.props.selected,
{
dateFormat: longDateFormat,
locale,
}
)}`;
}
}

return (
<span
role="alert"
aria-live="polite"
className="react-datepicker__aria-live"
>
{this.state.isRenderAriaLiveMessage && ariaLiveMessage}
</span>
);
};

renderDateInput = () => {
const className = classnames(this.props.className, {
[outsideClickIgnoreClass]: this.state.open,
Expand Down Expand Up @@ -1096,6 +1170,7 @@ export default class DatePicker extends React.Component {
renderInputContainer() {
return (
<div className="react-datepicker__input-container">
{this.renderAriaLiveRegion()}
{this.renderDateInput()}
{this.renderClearButton()}
</div>
Expand Down
12 changes: 12 additions & 0 deletions src/stylesheets/datepicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,15 @@
padding-left: 0.2rem;
height: auto;
}

.react-datepicker__aria-live {
position: absolute;
clip-path: circle(0);
border: 0;
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
width: 1px;
white-space: nowrap;
}
70 changes: 70 additions & 0 deletions test/calendar_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1741,4 +1741,74 @@ describe("Calendar", function () {
expect(childrenContainer).to.have.length(0);
});
});

describe("should render aria live region after month/year change", () => {
it("should render aria live region after month change", () => {
const datePicker = TestUtils.renderIntoDocument(
<DatePicker selected={utils.newDate()} />
);
const dateInput = datePicker.input;

TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput));

const calendar = TestUtils.scryRenderedComponentsWithType(
datePicker.calendar,
Calendar
)[0];
const nextNavigationButton = TestUtils.findRenderedDOMComponentWithClass(
calendar,
"react-datepicker__navigation--next"
);
nextNavigationButton.click();

const currentMonthText = TestUtils.findRenderedDOMComponentWithClass(
calendar,
"react-datepicker__current-month"
).textContent;

const ariaLiveMessage = TestUtils.findRenderedDOMComponentWithClass(
calendar,
"react-datepicker__aria-live"
).textContent;

expect(currentMonthText).to.equal(ariaLiveMessage);
});

it("should render aria live region after year change", () => {
const datePicker = TestUtils.renderIntoDocument(
<DatePicker showYearDropdown selected={utils.newDate()} />
);
const dateInput = datePicker.input;

TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput));

const calendar = TestUtils.scryRenderedComponentsWithType(
datePicker.calendar,
Calendar
)[0];
const yearDropdown = TestUtils.findRenderedDOMComponentWithClass(
calendar,
"react-datepicker__year-read-view"
);
yearDropdown.click();

const option = TestUtils.scryRenderedDOMComponentsWithClass(
calendar,
"react-datepicker__year-option"
)[7];
option.click();

const ariaLiveMessage = TestUtils.findRenderedDOMComponentWithClass(
calendar,
"react-datepicker__aria-live"
).textContent;

expect(ariaLiveMessage).to.equal(
`${utils.getMonthInLocale(
utils.getMonth(calendar.state.date),
datePicker.props.locale
)} ${utils.getYear(calendar.state.date)}`
);
});
});
});
54 changes: 53 additions & 1 deletion test/datepicker_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1984,7 +1984,7 @@ describe("DatePicker", () => {
expect(utils.getMinutes(date)).to.equal(22);
});
});

it("should selected month when specified minDate same month", () => {
const selected = utils.newDate("2023-01-09");
let date = null;
Expand Down Expand Up @@ -2040,4 +2040,56 @@ describe("DatePicker", () => {
});
expect(date.toString()).to.equal(utils.newDate("2022-01-01").toString());
});

describe("should render aria live region after date selection", () => {
it("should have correct format if datepicker does not contain time", () => {
const datePicker = TestUtils.renderIntoDocument(
<DatePicker selected={utils.newDate()} />
);
const dateInput = datePicker.input;

TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput));
TestUtils.Simulate.keyDown(
ReactDOM.findDOMNode(dateInput),
getKey("Enter")
);

const ariaLiveMessage = TestUtils.findRenderedDOMComponentWithClass(
datePicker,
"react-datepicker__aria-live"
).textContent;

expect(ariaLiveMessage).to.equal(
`Selected date: ${utils.safeDateFormat(datePicker.props.selected, {
dateFormat: "PPPP",
locale: datePicker.props.locale,
})}`
);
});

it("should have correct format if datepicker contains time", () => {
const datePicker = TestUtils.renderIntoDocument(
<DatePicker showTimeInput selected={utils.newDate()} />
);
const dateInput = datePicker.input;

TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput));
TestUtils.Simulate.keyDown(
ReactDOM.findDOMNode(dateInput),
getKey("Enter")
);

const ariaLiveMessage = TestUtils.findRenderedDOMComponentWithClass(
datePicker,
"react-datepicker__aria-live"
).textContent;

expect(ariaLiveMessage).to.equal(
`Selected date: ${utils.safeDateFormat(datePicker.props.selected, {
dateFormat: "PPPPp",
locale: datePicker.props.locale,
})}`
);
});
});
});

0 comments on commit 3070e4a

Please sign in to comment.