-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Some improvements for screen readers [a11y] #715
Conversation
> | ||
<strong>{monthTitle}</strong> | ||
</div> | ||
<table role="presentation"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the role of presentation here to stop the screen reader from reading all of the details of the columns and rows of the table WAI-ARIA use of presentation role.
className="CalendarMonth__caption js-CalendarMonth__caption" | ||
> | ||
<strong>{monthTitle}</strong> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed <caption>
to <div>
as apparently this is a structural type element that cues screen readers that this is a data vs layout type table (see NOTE on "layout tables" and the caption element here: H39: Using caption elements to associate data table captions with data tables).
See in DayPicker.jsx that it now points here for the aria-describedby
and thus continues to read the title as it was before with the <caption>
.
@@ -825,6 +825,9 @@ export default class DayPicker extends React.Component { | |||
<div | |||
className={dayPickerClassNames} | |||
style={dayPickerStyle} | |||
role="application" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a role of application here as it seemed to match up with the WAI-ARIA example shown here: For application landmarks with static prose.
|
||
// initialize phrases | ||
// set the appropriate CalendarDay phrase based on focusedInput | ||
const chooseAvailableDate = getChooseAvailableDatePhrase(phrases, focusedInput); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seemed like the chooseAvailableDate phrase was only getting set on update of the focus or the phrases so I thought adding the setting of it in componentDidMount
would address initialization.
@@ -21,41 +21,45 @@ describe('SingleDatePickerInput', () => { | |||
describe('clear date', () => { | |||
describe('props.showClearDate is falsey', () => { | |||
it('does not have .SingleDatePickerInput__clear-date class', () => { | |||
const wrapper = shallow(<SingleDatePickerInput showClearDate={false} />); | |||
const wrapper = shallow(<SingleDatePickerInput id="date" showClearDate={false} />); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the changes here were just to clean up some prop type warnings that were bugging me. Hope that's ok!
This is awesome! I will take a look at the code today. :) Thanks! FYI @backwardok |
src/components/DayPicker.jsx
Outdated
@@ -825,6 +825,9 @@ export default class DayPicker extends React.Component { | |||
<div | |||
className={dayPickerClassNames} | |||
style={dayPickerStyle} | |||
role="application" | |||
aria-label="Calendar" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should make sure that this is something that can be localized
src/components/DayPicker.jsx
Outdated
@@ -825,6 +825,9 @@ export default class DayPicker extends React.Component { | |||
<div | |||
className={dayPickerClassNames} | |||
style={dayPickerStyle} | |||
role="application" | |||
aria-label="Calendar" | |||
aria-describedby="CalendarMonth__caption" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious if this could be confusing - since it will be most likely just read when the user comes to the calendar the first time, the description will always be the first month. They will come to an area that will read something like, "Calendar, application, September". Seems like a more appropriate description would be something like "Date picker"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm well, going back and testing this again to be sure, it seems as if it's not even reading the aria-describedby. I must have assumed that when I heard "September 2017" it was the aria-describedby but really it was just the reading of the selected CalendarDay. Upon changing the text it's now more obvious that it's not being read. Perhaps it's just not supported by the browser screen reader combinations I'm testing on (Mac with VoiceOver on Chrome, Firefox, Safari)? I so far can't find a good reason for why it's not being read...
I wonder if it's really necessary or if the aria-label here and the aria-describedby on the input telling the user how to interact with the calendar is enough information? If you think a describedby is crucial here let me know and I'll keep working on why it's not being read, otherwise I can just remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@majapw and @backwardok, I just wanted to bump this and ask again, what are your thoughts about whether to skip adding an aria-describedby on the role="application" div or to figure out why it's not being read and fix it. If the answer is to add it and fix it, what should the text be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay! I think it would be fine to bypass adding the aria-describedby here. If we were to have some sort of visual text that also explained the calendar, or there was text that explained some calendar instructions specifically for screen reader users, then that might be useful, but I think it's probably ok without it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries! I have added the aria-label text to the localizable phrases and removed the aria-describedby.
As for having something visual and read out loud explaining the calendar instructions, there already is the keyboard shortcuts modal which is opened by entering ? or clicking on the ? at the bottom of the calendar. The instructions for getting to the keyboard shortcuts modal is read via the aria-describedby on the DateInput:
Press the down arrow key to interact with the calendar and select a date. Press the question mark key to get the keyboard shortcuts for changing dates.
So the instructions seem pretty clear and complete from my testing as a visually able, keyboard only, and screen-reader user.
@erin-doyle would you mind rebasing on the command line and force pushing, so as to remove any merge commits? |
… to the table thus differentiating the table as being for layout purposes instead of data for the sake of screen readers.
…r with appropriate aria attributes to better assist screen readers.
…rase based on the focusedInput which previously was not being set unless the focusedInput or phrases had changed.
oh jeez I forget to rebase every time... Ok it should be cleaned up now. |
@@ -174,6 +183,22 @@ export default class DayPickerRangeController extends React.Component { | |||
this.setDayPickerRef = this.setDayPickerRef.bind(this); | |||
} | |||
|
|||
componentDidMount() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the determination of which phrase to show is based off of the props and doesn't rely on the actual state of the component (waiting for the input to have focus), you can do this logic in the constructor and set phrases
to the new object in this.state
.
If you put that logic in componentDidMount
, which gets called after render
, setting state
will trigger another render.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(it'd then also need the same logic in componentWillReceiveProps
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep that totally makes sense, I've moved that logic to the constructor and removed componentDidMount
. As for componentWillReceiveProps
that logic was already there. The bit I added was to make sure that phrase was being initialized at component load time instead of having to wait until props changed.
…nentDidMount() and into the constructor to prevent extraneous rendering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me!
@@ -132,6 +132,15 @@ const defaultProps = { | |||
isRTL: false, | |||
}; | |||
|
|||
const getChooseAvailableDatePhrase = (phrases, focusedInput) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slightly prefer:
function getChooseAvailableDatePhrase(phrases, focusedInput) {
...
}
over an arrow function but not a blocker
Doing some screen reader testing on this and identified some things that could make the reading a little more clear/helpful:
The table was being interpreted as a data table versus a layout table and was reading extraneous info about the number of columns and rows in the current cell happened to be. The cell already nicely reads the full date and day of the week and therefore there's no need for the screen reader to associate the column headers with the cell.
The DayPickerRangeController did not seem to be properly setting the chooseAvailableStartDate phrase after first mounting so only after picking a start date and moving to the end date would the reader nicely read "it's available" for the date options.
I did also notice Issue [a11y] Disabled dates aren't actually disabled in markup #713 where unavailable dates are not reading as "unavailable" but I haven't figured out what's causing that yet and since it's already a known issue wanted to move forward with these changes before moving on to trying to help out with that one.