Skip to content
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

Merged
merged 12 commits into from
Feb 4, 2017
Merged

Conversation

mmalerba
Copy link
Contributor

No description provided.

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Jan 14, 2017
Copy link
Member

@jelbourn jelbourn left a 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 {
Copy link
Member

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;
Copy link
Member

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;

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 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);
Copy link
Member

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', () => {
Copy link
Member

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);
Copy link
Member

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?

Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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({
Copy link
Member

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.

Copy link
Contributor Author

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', () => {
Copy link
Member

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?

Copy link
Member

@devversion devversion Jan 14, 2017

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 and touchstart 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)

@mmalerba
Copy link
Contributor Author

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 */);
Copy link
Member

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');
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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!

@mmalerba
Copy link
Contributor Author

mmalerba commented Feb 1, 2017

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

@mmalerba
Copy link
Contributor Author

mmalerba commented Feb 1, 2017

+ @tinayuangao since jeremy is out

Copy link
Member

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

@tinayuangao
Copy link
Contributor

LGTM

@mmalerba mmalerba added pr: lgtm action: merge The PR is ready for merge by the caretaker and removed pr: needs review labels Feb 1, 2017
@kara kara merged commit 8a6d902 into angular:master Feb 4, 2017
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 6, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants