-
Notifications
You must be signed in to change notification settings - Fork 6.8k
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
fix(material/datepicker): update active date on focus #24172
Conversation
@@ -218,6 +218,21 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy { | |||
); | |||
} | |||
|
|||
_yearBecomesActive(event: MatCalendarUserEvent<number>) { |
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 is nearly identical to the _yearSelected
method above. I'm open to suggestions on how to refactor this, or if we should refactor it to share code.
@@ -198,6 +198,25 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy { | |||
); | |||
} | |||
|
|||
/** Handles when a new month becomes active. */ |
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 is nearly identical to the _monthSelected
method above. I'm open to suggestions on how to refactor this, or if we should refactor it to share code.
This is working and ready for review now. @jelbourn , is minor the correct target for this PR? It adds a public member to |
Deployed dev-app to: https://ng-comp-dev--pr-24172-194ec1b7aad7df4b3deed4820c983151-3fnykkj8.web.app |
Looks like the focus is lost when pressing Page Up in the month view. I'm looking into that... |
2d6d48e
to
906db9e
Compare
Fixed the losing focus problem. |
Everything works when I test manually, so it appears to be a problem with the tests. |
@crisbeto c0f8ecf After debugging this with Ensuring that change detection runs appears to be the fix here, but I'm curious why this wan't a problem until now. |
c0f8ecf
to
9dc7040
Compare
Note that this is not going to fix the problem with VoiceOver navigation not updating the focused date until #24171 lands. That's because VoiceOver does not seem to trigger the This PR and #24171 are safe to merge in either order, but #23483 will not be fixed until both PR's land. |
9dc7040
to
28d1cc0
Compare
@@ -329,6 +340,9 @@ export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy { | |||
this.activeDateChange.emit(this.activeDate); | |||
} | |||
|
|||
// Ensure the calendar body has the correct active cell. | |||
this._changeDetectorRef.detectChanges(); |
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.
Why is this necessary? This would invoke an entire extra change detection, which we generally want to avoid.
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.
Yeah, this seems a little iffy. I'm guessing that it might be flushing this subscription: https://github.com/angular/components/blob/master/src/material/datepicker/calendar-body.ts#L201
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.
After debugging this with console.log statements, I determined that the calendar body calls .focus on the active cell before receiving an updated value for activeCell. This causes it to focus on the old active cell, rather than the one set in the keydown handler.
Ensuring that change detection runs appears to be the fix here, but I'm curious why this wan't a problem until now.
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.
If it works in real life but not in tests, it most likely means the test needs a detectChanges
call and not the source code (which is very normal since in the test nothing is causing change dtections to happen autmatically)
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 just pushed a change to only call detectChanges
if the event is not trusted. This is only needed for the tests because fake keyboard events do not trigger change detection. I'm generally not a fan of having code that's only intended to run in the tests, and I'm curious how we usually handle this situation.
Calling detectChanges in the test code doesn't work because by that time the calendar body will have already triggered the activeDateChange
event with a stale value. This wasn't a problem until now, because before focusing on a stale date wouldn't trigger the activeDateChange
event to update the active date. Calling detectChanges in the test code would case the calendar body to focus again but on a updated value.
Here's a rough timeline of what happens in the unit tests if we don't call detectChanges
when handling the keydown
event.
0. (active Date is Jan 12 2021)
- trigger a fake keyboard event with the right arrow key
- MatMonthView sets
this.activeDate
to Jan 13 2021 - MatMonthView calls
_focusActiveDate
- MatCalendarBody calls .focus on Jan 12, 2021 date then triggers an
activeDateChange
Event with Jan 12, 2021 - test code calls
detectChanges
- month-view receives sets the active date to Jan 12, 2021
- (active date should be Jan 13, 2021).
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.
Is there a supported way to do this programmatically without dispatching fake events? If there isn't, teams will probably try dispatching keydown events manually to get this desired behavior. So if the support for this gets baked into the datepicker it will likely be depended on
Edit: Not saying that this is a bad thing, just that we should be aware that this might become the reality if we go this route
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 not sure, another approach would be to call .focus()
the date that the keyboard event targets, instead of updating activeDate in the month view. The month view would not set the activeDate itself, and would receive an updated value of the activeDate via the activeDateChange event.
i don't know how that would work with the page up/down arrow because they target dates which are not currently rendered, so there's nothing to call .focus on for them
.
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 generally not a fan of having code that's only intended to run in the tests, and I'm curious how we usually handle this situation.
This isn't really the way to think about this situation. In a real application, Angular will trigger change detection when a user interacts with the page (clicks, keypresses, etc). However, that won't happen if we trigger a fake event in a test (since obviously you can't trigger a real event in a test). So calling detectChanges
in the test is the thing that most closely approximates what happens when the real app is running. The library code should almost never have unnecessary behaviors added to support tests (barring things like extra attrs we add for harnesses).
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.
Okay, so I should think about if the behavior is different between the tests and a real application, rather than the exact code path.
The library code should almost never have unnecessary behaviors added to support tests
Does the library code mean Angular Material (this repo) or the Angular framework?
The above diff is out-of-date, but I made it only call detectChanges
if the event is not trusted, so that we don't impact the performance in a real 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.
LGTM
e68ce66
to
ddfd299
Compare
42e7b8e
to
bb41ac8
Compare
When a a date cell on the calendar recieves focus, set the active date to that cell. This ensures that the active date matches the date with browser focus. Previously, we set the active date on keydown and click, but that was problematic for screenreaders. That's because many screenreaders trigger a focus event instead of a keydown event when using screenreader specific navigation (VoiceOver, Chromevox, NVDA). Addresses angular#23483
bb41ac8
to
194ec1b
Compare
if (!event.isTrusted) { | ||
// Manually triggered events in unit tests do not trigger change detection. | ||
this._changeDetectorRef.detectChanges(); | ||
} |
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 isn't really the right approach to this issue. The unit test should directly call detectChanges
- library code (this code) shouldn't add additional behaviors explosively to make unit tests pass. Having the tests call detectChanges
in tandem with emitting the fake event is the normal/standard way of handling 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.
Well, calling detectChanges
in the unit test doesn't work in this situation because the microtask queue is flushing before change detection runs. the child component (MatCalendarBody
) needs to received the updated data from this.activeDate
before it can focus on the correct cell in this method
_focusActiveCell(movePreview = true) { |
Here's an explanation I wrote up earlier which seems to have gotten buried in the PR comments.
I just pushed a change to only call detectChanges if the event is not trusted. This is only needed for the tests because fake keyboard events do not trigger change detection. I'm generally not a fan of having code that's only intended to run in the tests, and I'm curious how we usually handle this situation.
Calling detectChanges in the test code doesn't work because by that time the calendar body will have already triggered the activeDateChange event with a stale value. This wasn't a problem until now, because before focusing on a stale date wouldn't trigger the activeDateChange event to update the active date. Calling detectChanges in the test code would case the calendar body to focus again but on a updated value.
Here's a rough timeline of what happens in the unit tests if we don't call detectChanges when handling the keydown event.
0. (active Date is Jan 12 2021)
- trigger a fake keyboard event with the right arrow key
- MatMonthView sets this.activeDate to Jan 13 2021
- MatMonthView calls _focusActiveDate
- MatCalendarBody calls .focus on Jan 12, 2021 date then triggers an activeDateChange Event with Jan 12, 2021
- test code calls detectChanges
- month-view receives sets the active date to Jan 12, 2021
- (active date is Jan 12, but it should be Jan 13, 2021).
There is two approaches I can think of, but both required changing the library code (this code)
- call detect changes in library code (this code) only if the event is not trusted
- add a
timeOut
to_focusActiveCell
so that the microtask queue does not get flushed until we manually flush it in the test code.
WDYT?
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.
If it is too difficult to handle testing these in unit tests, I would suggest just test these cases in our e2e tests with real keyboard events
this.activeDateChange.emit( | ||
this._dateAdapter.createDate( | ||
this._dateAdapter.getYear(this.activeDate), | ||
month, | ||
Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth), | ||
), | ||
); |
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.
Would the clampDate
method on DateAdapter
be a simpler way of implementing 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.
LGTM assuming the last couple of comments will be resolved.
@@ -122,6 +122,9 @@ export class MatCalendarBody implements OnChanges, OnDestroy { | |||
/** Emits when a new value is selected. */ | |||
@Output() readonly selectedValueChange = new EventEmitter<MatCalendarUserEvent<number>>(); | |||
|
|||
/** Emits when a new date becomes active. */ | |||
@Output() readonly activeValueChange = new EventEmitter<MatCalendarUserEvent<number>>(); |
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.
nit: might be more accurate to call this activeDateChange
to align with the events on the other calendar views.
Talked this over with @andrewseguin , This solution is problematic for testing because change detections needs to run before the microtask queue is flushed. We also looked into making Closing this PR, I'm looking into another approach, where we would be able to run change detection in the tests. Will send out a new PR when I have it working. |
FYI this is closed in favor of #24279 24279 |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
When a date cell on the calendar receives focus, set the active date
to that cell. This ensures that the active date matches the date with
browser focus.
Previously, we set the active date on keydown and click, but that was
problematic for screen readers. That's because many screen readers trigger
a focus event instead of a keydown event when using screen reader
specific navigation (VoiceOver, Chromevox, NVDA).
Fixes #23483