Skip to content

Conversation

mahoneycm
Copy link
Contributor

@mahoneycm mahoneycm commented Feb 15, 2024

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 the hover 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 the renderCalendar() function or sets focus on the hovered dates.

  1. Follows expected mouseover behaviors.
  2. Does not affect date picker or date range usage.

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 altogether

Originally, 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

  • Date picker no longer updates focus on mouseover events.
  • Behavior changes help to meet WCAG AA requirement
  • Created a new setRangeDates() function to capture repeated behaviors to set range dates within the render calendar and handle mouseover functions.

The intent of this Success Criterion is to support people with low vision who need to enlarge text and read it in a single column. When the browser zoom is used to scale content to 400%, it reflows - i.e., it is presented in one column so that scrolling in more than one direction is not necessary. 1

Testing and review

  1. Visit date picker.
  2. Test mouse interactions and confirm they work as expected.
  3. Test keyboard interactions and confirm they work as expected.
    • Note: Ensure you can use keyboard navigation while the mouse is hovering a date
  4. Test VoiceOver and ensure callouts are expected.
  5. Zoom to 400% magnification and use the keyboard to interact with the date picker. There should be no errors navigating throughout the calendar.
  6. Repeat steps for all variants.
  7. Repeat steps date range picker and its variants.

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

  • Expected mouse behaviors work without error
  • Expected keyboard behaviors work without error
  • Screenreader behaviors work as expected without error
  • Component is usable at high magnification without error

Footnotes

  1. WCAG Reflow (Level AA) requirement

@mahoneycm

This comment was marked as outdated.

@mahoneycm mahoneycm closed this Feb 20, 2024
@mahoneycm mahoneycm reopened this Jun 28, 2024
@mahoneycm
Copy link
Contributor Author

mahoneycm commented Jul 22, 2024

Reopening this PR due to additional accessibility errors caused by the approach taken in #5778

@mahoneycm mahoneycm marked this pull request as ready for review July 22, 2024 17:47
@mahoneycm mahoneycm changed the title Cm date range remove mouseover USWDS - Date picker: Prevent focus on mouseover Jul 22, 2024
Comment on lines +482 to +488
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);
Copy link
Contributor Author

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;
Copy link
Contributor Author

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.

Comment on lines -2233 to -2238
[CALENDAR_MONTH]() {
handleMouseoverFromMonth(this);
},
[CALENDAR_YEAR]() {
handleMouseoverFromYear(this);
},
Copy link
Contributor Author

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.

Comment on lines -156 to -170

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",
);
});
Copy link
Contributor Author

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.

Comment on lines -67 to -92
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",
);
});

Copy link
Contributor Author

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.

Comment on lines -155 to -168
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",
);
});
Copy link
Contributor Author

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.

Comment on lines +1838 to +1854
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);
}
Copy link
Contributor Author

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.

@mahoneycm mahoneycm requested review from amyleadem and mejiaj July 22, 2024 20:33
Copy link
Contributor

@mejiaj mejiaj left a 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:

  1. Is the alternate approach still relevant? Could you add a brief note as to which you favor and why?
  2. 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.

Comment on lines +467 to +473
/**
* @typedef {Object} DateRangeContext
* @property {Date} rangeStartDate
* @property {Date} rangeEndDate
* @property {Date} withinRangeStartDate
* @property {Date} withinRangeEndDate
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thanks for adding this, I see the definitions on hover in VS Code.

image

Co-authored-by: James Mejia <james.mejia@gsa.gov>
@mahoneycm
Copy link
Contributor Author

@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 👍

@mahoneycm mahoneycm requested a review from mejiaj July 23, 2024 21:38
Copy link
Contributor

@mejiaj mejiaj left a 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!

Copy link
Contributor

@amyleadem amyleadem left a 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.

Copy link

@amycole501 amycole501 left a 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!

Copy link

@alex-hull alex-hull left a 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!

Copy link
Contributor

@thisisdano thisisdano left a 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. 🎉

@thisisdano thisisdano merged commit 2e398d2 into develop Oct 3, 2024
5 checks passed
@thisisdano thisisdano deleted the cm-date-range-remove-mouseover branch October 3, 2024 03:22
@thisisdano thisisdano mentioned this pull request Oct 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

USWDS - Bug: Date picker - mouse hover preventing keyboard interaction on high zoom magnification
6 participants