-
Notifications
You must be signed in to change notification settings - Fork 1.1k
USWDS - Date picker: Prevent focus on mouseover #5774
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
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
Reopening this PR due to additional accessibility errors caused by the approach taken in #5778 |
const setRangeDates = (date, rangeDate) => { | ||
const rangeConclusionDate = date; | ||
const rangeStartDate = rangeDate && min(rangeConclusionDate, rangeDate); | ||
const rangeEndDate = rangeDate && max(rangeConclusionDate, rangeDate); | ||
|
||
const withinRangeStartDate = rangeDate && addDays(rangeStartDate, 1); | ||
const withinRangeEndDate = rangeDate && subDays(rangeEndDate, 1); |
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.
This code was used in the renderCalendar
function to set range dates. I moved it to its own function so I can use it to set range dates on mouseover.
|
||
const currentCalendarDate = calendarEl.dataset.value; | ||
const hoverDate = dateEl.dataset.value; | ||
if (selectedDate) return; |
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.
Returning early if there is a selected date. This allows us to match current date-range behaviors.
[CALENDAR_MONTH]() { | ||
handleMouseoverFromMonth(this); | ||
}, | ||
[CALENDAR_YEAR]() { | ||
handleMouseoverFromYear(this); | ||
}, |
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.
Removed mouseover from month and year functions because we do not want to re-render and focus these buttons on hover.
|
||
it("should update the focus when moving over a non-selected valid month", () => { | ||
EVENTS.mouseover( | ||
getCalendarEl().querySelector( | ||
'.usa-date-picker__calendar__month[data-label="October"]', | ||
), | ||
); | ||
|
||
assert.strictEqual( | ||
getCalendarEl(".usa-date-picker__calendar__month--focused").dataset | ||
.label, | ||
"October", | ||
"focuses correct month", | ||
); | ||
}); |
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.
Removed test because we are no longer setting focus when month buttons are hovered.
it("should update the focus when moving over a non-selected valid day", () => { | ||
root.dataset.minDate = "2020-06-01"; | ||
root.dataset.maxDate = "2020-06-24"; | ||
input.value = "6/20/2020"; | ||
EVENTS.click(button); | ||
assert.strictEqual( | ||
getCalendarEl(".usa-date-picker__calendar__date--focused").dataset | ||
.value, | ||
"2020-06-20", | ||
"focuses correct date", | ||
); | ||
|
||
EVENTS.mouseover( | ||
getCalendarEl().querySelector( | ||
'.usa-date-picker__calendar__date[data-day="19"]', | ||
), | ||
); | ||
|
||
assert.strictEqual( | ||
getCalendarEl(".usa-date-picker__calendar__date--focused").dataset | ||
.value, | ||
"2020-06-19", | ||
"focuses correct date", | ||
); | ||
}); | ||
|
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.
Removed test because we are no longer setting focus when date buttons are hovered.
it("should update the focus when moving over a non-selected valid year", () => { | ||
EVENTS.mouseover( | ||
getCalendarEl().querySelector( | ||
'.usa-date-picker__calendar__year[data-value="2022"]', | ||
), | ||
); | ||
|
||
assert.strictEqual( | ||
getCalendarEl(".usa-date-picker__calendar__year--focused").dataset | ||
.value, | ||
"2022", | ||
"focuses correct year", | ||
); | ||
}); |
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.
Removed test because we are no longer setting focus when year buttons are hovered.
const dateButtons = calendarEl.querySelectorAll( | ||
`.${CALENDAR_DATE_CURRENT_MONTH_CLASS}`, | ||
); | ||
|
||
dateButtons.forEach((button) => { | ||
const buttonDate = parseDateString(button.dataset.value); | ||
if ( | ||
isDateWithinMinAndMax( | ||
buttonDate, | ||
withinRangeStartDate, | ||
withinRangeEndDate, | ||
) | ||
) { | ||
button.classList.add(CALENDAR_DATE_WITHIN_RANGE_CLASS); | ||
} else { | ||
button.classList.remove(CALENDAR_DATE_WITHIN_RANGE_CLASS); | ||
} |
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.
For each date button in the current calendar month, check if it's value is within the date range.
If it is, add the within range
class. If itis not, 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.
Thanks for the update and such thorough documentation @mahoneycm! The alternatives considered section was really helpful.
A few notes on the PR description:
- Is the alternate approach still relevant? Could you add a brief note as to which you favor and why?
- Preview links send you to other PR's.
Tested
Component | Before | After |
---|---|---|
Datepicker | develop | current |
Date range | develop | current |
- No regressions in mouse interactions
- No regressions in keyboard interactions
- Unit tests pass via
npm run test:unit
- No regressions on 400% zoom
Tested in Chromium 126 & Samsung S23 Android 13 Chrome.
Minor comments below.
/** | ||
* @typedef {Object} DateRangeContext | ||
* @property {Date} rangeStartDate | ||
* @property {Date} rangeEndDate | ||
* @property {Date} withinRangeStartDate | ||
* @property {Date} withinRangeEndDate | ||
*/ |
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.
Co-authored-by: James Mejia <james.mejia@gsa.gov>
@mejiaj I updated the PR description with the appropriate testing links, and additional information on why the alternative solution failed. I also committed one of your suggestions, and replied to the other comments! Ready for your re-review 👍 |
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.
Thanks for the responses, @mahoneycm!
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.
@mahoneycm Looks good to me! The components behave as expected and I no longer experience the error described in the issue. Great work!
I have tagged @amycole501 and @alex-hull to review so that they can confirm the updated keyboard, zoom, and screen reader behavior in the components meets standards.
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.
Tested at standard view and 400% in both JAWS and NVDA. The only minor issue I found was NVDA kept wanting to announce the table ("application table") which was a little annoying but not a blocker. The mouseover issue seems to have been fixed!
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.
Agreed with Amy C. I think it looks good!
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.
Great documentation and thorough review. 🎉
Summary
Fixed a bug that caused
mouseover
events to focus buttons and prevent keyboard navigation. Now, hovering the mouse over date picker buttons will only cause thehover
state.Breaking change
This is not a breaking change
Related issue
Closes #5752
Related pull requests
Changelog →
An alternative approach to #5778. This approach introduces a separate accessibility issue where screen-readers read the focused date every time a date button is hovered. This includes when a new date is selected via the keyboard while the mouse is hovering over a date. This would introduce confusion for screen reader users.
Preview link
Date Picker →
Date Range Picker →
Problem statement
Hovering the mouse over date picker calendar buttons cause them to gain focus. This is unexpected behavior which also prevents keyboard navigation through calendar buttons.
Solution
Rework the
handleMouseoverFromDate()
function to target the date buttons of the current month and add the--within-range
class. The function no longer calls therenderCalendar()
function or sets focus on the hovered dates.mouseover
behaviors.Additionally, I removed the
mouseover
event listeners and functions from month and year buttons. They did not appear to add any functionality beyond focusing the button on hover, which we want to prevent.handleMousoverFromDate
is maintained in order to properly render date range previews on hover.Accessibility improvements
This solution improves accessibility interaction in two ways.
VoiceOver
In its current iteration, when using the date picker with a mouse and VoiceOver, it would rapidly call out dates as your mouse moved over them. This, in my opinion, is unexpected behavior. VoiceOver should not read out other buttons or table content upon
mouseover
events.With this update, VoiceOver will only call out the buttons when selected via the keyboard.
High zoom magnification behaviors
As depicted in #5752, using the date picker at a high magnification (400%, AA requirement) the mouse would sometimes jump to the selected button, causing a mouseover event, and locking the focus on that button. Keyboard interaction would be prevented by the hovered mouse. For users who rely on keyboard navigation, this would cause confusion as they would not move the mouse to identify the issue.
With this update, users will be able to use expected keyboard interactions without error.
Alternatives considered
blur
I tested calling
blur()
on the selected element during keyboard interactions, but this caused accessibility issues at high zoom (400%). The window would jump unexpectedly and cause the mouse to move, hovering a new button, which then stole focus once again.Reassigning focus on render
This was the approach taken in #5778.
I tested storing the focused button value and reapplying focus after the calendar re-rendered. Visually, this solution worked fine but caused the screen reader to call out the focused date every time the mouse hovered a new date. The date wouldn't match what is hovered and could be confusing for users if their mouse accidentally hovered over a date button.
Removing
mouseover
events altogetherOriginally, I had removed mouseover events altogether. This caused a visual regression when selecting a date range for the first time where the range preview would not show.
Additional concerns
Current date picker behaviors cause a new calendar to be rendered after every mouseover and keyboard event. It does so by cloning the existing calendar, setting appropriate classes and returning an HTML template literal. This pattern does not match modern USWDS practices and likely affects performance.
Major changes
mouseover
events.setRangeDates()
function to capture repeated behaviors to set range dates within the render calendar and handle mouseover functions.Testing and review
Tip
When using keyboard navigation with a screenreader, use the screenreader’s Table Navigation controls (varies by screenreader). This can help prevent unexpected interactions. Use default arrow keys when jumping months via dates.
Testing checklist
Footnotes
WCAG Reflow (Level AA) requirement ↩