-
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
Add directive to determine how elements were focused. #2646
Conversation
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.
+ @devversion who worked on something like this for material1. I suspect there may be some edge cases this doesn't capture
|
||
/** Singleton that allows all instances of CdkAddFocusClasses to share document event listeners. */ | ||
@Injectable() | ||
export class CdkFocusCauseDetector { |
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.
How about FocusOriginMonitor
? I would like to avoid prefixing any classes with "cdk".
export class CdkFocusCauseDetector { | ||
/** Whether a keydown event has just occurred. */ | ||
get keydownOccurred() { return this._keydownOccurred; } | ||
private _keydownOccurred = 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.
Can you just do
private readonly keydownOccurred = 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.
This doesn't do the same thing does it? I do want this to be writable inside the class, just don't want other people setting it.
constructor() { | ||
document.addEventListener('keydown', () => { | ||
this._keydownOccurred = true; | ||
setTimeout(() => this._keydownOccurred = false, 0); |
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.
Instead of setTimeout
, can you do
Promise.resolve().then(() => this.keydownOccured = false);
(same elsewhere)
private _mousedownOccurred = false; | ||
|
||
constructor() { | ||
document.addEventListener('keydown', () => { |
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.
Add comment explaining why useCapture
is appropriate here.
constructor() { | ||
document.addEventListener('keydown', () => { | ||
this._keydownOccurred = true; | ||
setTimeout(() => this._keydownOccurred = false, 0); |
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 slightly concerned about the performance implications of scheduling a asynchronous action on every keydown in the application. What can we do to conclusively say the cost for this would be minor?
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.
In Material 1 we only had a timeout for touch events (for the buffering), but for normal mousedown
events we didn't need one.
We always keep track of the timestamp for the last interaction and then compare it based on the time difference.
https://github.com/angular/material/blob/master/src/core/services/interaction/interaction.js#L144
https://github.com/angular/material/blob/master/src/components/button/button.js#L169
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.
@devversion Why did you guys choose 15ms, that looks kind of race-condition-y.
@jelbourn I could do something similar to the ng1 version I suppose. The scheduled function is about as lightweight as possible, I wouldn't expect it to cost us much in performance. I'll make a little experiment to verify
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 my performance test code & results, doesn't look like it adds any noticeable latency vs the timestamp approach (at least on my machine)
* Directive that determines how a particular element was focused (via keyboard, mouse, or | ||
* programmatically) and adds corresponding classes to the element. | ||
*/ | ||
@Directive({ |
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.
How would you add this directive to something like MdButton
, where the host element itself is the focused element. It's not something the user should need to apply to the material components.
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.
good question... maybe I can move most of the functionality into the injectable and just have the directive pass its element to the injectable?
setTimeout(() => this._keydownOccurred = false, 0); | ||
}, true); | ||
|
||
document.addEventListener('mousedown', () => { |
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.
Will this cover touch events?
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.
A lot of touch devices actually emit a mousedown
event for touch clicks, but I don't think that this really reliable.
Test for
mousedown
andtouchstart
events: http://codepen.io/DevVersion/pen/qRNgOZ
In the Material 1 interaction service we had to buffer the touch events: see here
Also interesting would be how that handles IE11 and eventually IE mobile browsers (special treatment in M1)
PTAL, restructured the code so it can be easily used by MdButton and similar components and did a performance experiment. Still need to add touch event support, just want to make sure we're on the same page with this approach first. |
|
||
it('should detect focus via keyboard', async(() => { | ||
// Simulate focus via keyboard. | ||
dispatchKeydownEvent(document, 9 /* tab */); |
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.
You should be able to use the TAB
constant from here
focusVia(element: Node, renderer: Renderer, focusOrigin: FocusOrigin) { | ||
this._fakeOrigin = focusOrigin; | ||
document.addEventListener('focus', this._clearFakeOrigin, true); | ||
renderer.invokeElementMethod(element, 'focus'); |
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.
How about moving the focus
listener (above) to its own function, so you can just invoke it with the given fakeOrigin
.
export type FocusOriginType = 'keyboard' | ' mouse' | 'manual';
/* Somewhere in the code */
renderer.listen(element, 'focus', () => this._onElementFocus(element, origin));
/* Focus event listener */
private _onElementFocus(element: HTMLElement, target: FocusOriginType) {
/* Handle the target */
}
/* Fake Focusing*/
private focusVia(XXX) {
this._onElementFocus(element, focusOrigin);
}
This would also mean that you get rid of the _keydownOccurred
properties and just assign the given string from FocusOriginType
/ or you use an enum.
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 this does the same thing. We need to actually focus the element in addition to adding the styles, so we want to invoke the focus method. We need to record the fakeOrigin
first though, so that when we later wind up in the focus listener we know that we still want to fake the origin.
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.
Good point- Although I think that you would simplify a lot of things by only keeping the focusOrigin
as type of FocusOriginType
instead of having booleans like _keydownOccured
.
document.addEventListener('keydown', () => {
this.focusOrigin = 'keyboard';
setTimeout(() => this.focusOrigin = null, 0);
}, true);
private focusVia(element: HTMLElement, focusOrigin: FocusOrigin) {
this.focusOrigin = focusOrigin;
element.focus(); // Or with renderer
}
private _onElementFocus(element: HTMLElement) {
/* Handle the this.focusOrigin */
this.focusOrigin = null;
}
This way you would not need to listen for another focus
event when faking the origin.
In this example above, null
would be treated as programmatic.
Also this could be just an idea. Let me know what you think.
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.
Ah I see, yeah I think that will be much simpler, thanks!
… the FocusOrigin.
053854b
to
1b97f7a
Compare
33a5ac4
to
731c0dd
Compare
6ffbca2
to
2e70169
Compare
@devversion So it seems like the focus & blur event listeners are not triggering in the tests on Firefox. They do work fine in the demo app though. It may have something to do with the test driver starting multiple windows, I've seen a couple mentions around the web of Firefox not bothering to fire these events if the window isn't active. I just disabled these tests for Firefox. |
+ @tinayuangao since jeremy is out |
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! - Chatted a lot with @mmalerba about the API and implementation. I'm still not a fan of program
as FocusOrigin
, but it is definitely very precise.
In the future we should definitely revisit the Firefox tests.
LGTM |
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. |
No description provided.