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

Fix issue number #33 #34

Merged
merged 7 commits into from
Aug 8, 2017
Merged

Fix issue number #33 #34

merged 7 commits into from
Aug 8, 2017

Conversation

kloots
Copy link
Contributor

@kloots kloots commented Jun 5, 2017

This PR addresses issue #33 by refining the heuristics for when the focus-ring class is applied: if the user navigates to it via Tab or Shift + Tab.

In addition, this PR also:

  1. Removes use of data-focus-ring-added attribute
  2. Removes the need for use of dom-matches polyfill dependency
  3. Removes the need for setTimeout() when handling the keydown event.

…o the target if the user navigates to it via Tab or Shift + Tab

2. Remove use of data-focus-ring-added
3. Remove need for use of matches polyfill
4. Remove need for setTimeout()
This was referenced Jun 5, 2017
@alice
Copy link
Member

alice commented Jun 5, 2017

Mostly looks good, but I'd like to get at least @bkardell @robdodson or @marcysutton to take a look before we merge, since this is a big behaviour change.

return;

var isTabKey = e.keyCode == 9;
var isTabNavigation = isTabKey || isTabKey && e.shiftKey;
Copy link
Member

Choose a reason for hiding this comment

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

This reduces to just isTabKey, doesn't it? Perhaps a comment instead noting that the shift key may or may not be pressed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does indeed. Previous approach was more out of the desire to have the code be explicit & self-documenting about when we match. But if you don't like it, then consider it changed.

addFocusRingClass(document.activeElement);
if (keyboardThrottleTimeoutID !== 0)
clearTimeout(keyboardThrottleTimeoutID);
keyboardThrottleTimeoutID = setTimeout(function() {
Copy link
Member

Choose a reason for hiding this comment

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

Hmmm how do you feel about the case where, say, using Enter to activate a button causes a UI change? Should :focus-ring match then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't adding a focus ring superfluous in that case as the UI change alone is an indicator?

Copy link
Member

Choose a reason for hiding this comment

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

I think it depends what ends up being focused. If it's a widget you might immediately want to interact with, I think it might be disorienting to hide the focus ring. Using enter to activate a button is a stronger signal than using a keyboard shortcut.

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 entirely clear on the use case you are advocating for. Would you mind explaining in more detail? Perhaps with a real-life example.

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a real life example, unfortunately, but I'm imagining something like a custom date picker which pops up some extra UI when you press a button.

Another thought: this also means :focus-ring wouldn't match for UI which supports directional navigation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't directional navigation in the same category as a screen reader's virtual navigation, and therefore the visual indicator is provided by the host service (e.g. like VoiceOver's black outline).

Copy link
Member

Choose a reason for hiding this comment

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

I mean author-implemented directional navigation as opposed to "spatial navigation" as a UA feature using some kind of D-pad.

The example I just saw was in Google Slides, where you go to add a new slide and you get a (in my case at least) 3x4 grid of slide layout options that you can choose between.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not all keyboard users are also using AT, if you had a menu of menu items for example you want them to show the ring, right?

Copy link
Contributor Author

@kloots kloots Jun 5, 2017

Choose a reason for hiding this comment

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

@bkardell wouldn't a focus ring be superfluous for menu items? Consider the menubar in Mac OS—you don't get both the selection highlight + a focus ring when navigating via the up & down arrow keys, you simply see the selection state change (blue background).

I don't know how we could account for all author-defined directional navigation while at the same time not matching too aggressively (as we are now). Hence my comment in issue #33 indicating we should be more conservative with this polyfill and allow authors to decide when to show the focus ring when they are implementing author-defined keyboard shortcuts or navigation.

For example, take your menu use case. Any menu I have ever implemented with keyboard accessibility has code that looks like this:

function onKeyDown(e) {
  if (e.keyCode == 40) {
    $(e.target).next().focus();
    $(e.target).next().addClass('selected');
  }
}

In such cases I'm already explicitly managing both where focus should move and what it should look like via adding a class—so I can match the selection style also used by the mouse. So, having a focus-ring class also applied in this case wouldn't be desirable to me. In fact, it would be duplicating efforts.

* @param {Event} e
*/
function onFocus(e) {
if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target))
addFocusRingClass(e.target);
hadKeyboardEvent = false;
Copy link
Member

Choose a reason for hiding this comment

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

Is this weirdly indented or is it meant to be part of the if block?

Copy link
Collaborator

Choose a reason for hiding this comment

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

always use {} and save yourself the hassle is my motto :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bkardell agreed. All {} all the time. Done. Only didn't previously as previously the code didn't use them and I thought that may have been the preferred style by @alice or @robdodson.

Copy link
Member

Choose a reason for hiding this comment

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

For single lines I'm in the habit of avoiding them since that's the style we use in Chromium. But you have to be used to that in order to be used to adding them for additional lines (since not adding them can create bugs along the lines of GOTOFAIL)

* - a keyboard event happened in the past 100ms, or
* - the focus event target triggers "keyboard modality" and should always
* have a focus ring drawn.
* - a keydown event preceded the focus event, meaning the target received
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: remove everything up to and including "meaning"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -265,7 +225,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (index(el).contains('focus-ring'))
return;
index(el).add('focus-ring');
el.setAttribute('data-focus-ring-added', '');
Copy link
Member

Choose a reason for hiding this comment

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

Oh, why is this being removed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I couldn't work out why it was necessary in the first place. Isn't it superfluous? Doesn't the presence of the focus-ring class give us enough?

Copy link
Member

Choose a reason for hiding this comment

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

It disambiguates an explicit, author-added focus-ring class (to allow always matching .focus-ring) from a programmatically added one. See line 281 for where it's used. It was added in 8d00580

Copy link
Contributor Author

@kloots kloots Jun 5, 2017

Choose a reason for hiding this comment

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

What's a use case for an author-added focus-ring class? And why would we want to support that if this is a polyfill for :focus-ring and there would be no similar pattern once this hopefully is implemented as a pseudo selector in browsers. Wouldn't we want to discourage authors adding focus-ring?

Copy link
Member

@alice alice Jun 5, 2017

Choose a reason for hiding this comment

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

Check out the change for context.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you explain how this is more 'with-the-grain' of the eventual pseudo? I'm not following.

Copy link
Member

Choose a reason for hiding this comment

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

In any case, this is orthogonal to the main point of this PR, so maybe let's revert it here and continue the discussion on a bug?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bkardell I don't think this polyfill should give authors abilities they won't have with the actual :focus-ring psuedo. Therefore, once actual support for :focus-ring lands in browsers the only change authors need to make to their CSS is changing their rules from .focus-ring to :focus-ring. Hence, more with the grain.

Copy link
Member

Choose a reason for hiding this comment

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

Can we file a bug for this discussion and revert this change in the context of this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alice bug is filed. And this PR has been updated to restore data-focus-ring-added.

@@ -52,8 +52,10 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {Element} el
*/
function addFocusRingClass(el) {
if (classList(el).contains('focus-ring'))
if (classList(el).contains('focus-ring')) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we please revert these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

...and done.

@@ -278,42 +237,49 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {Element} el
*/
function removeFocusRingClass(el) {
if (!el.hasAttribute('data-focus-ring-added'))
Copy link

@marcysutton marcysutton Jun 6, 2017

Choose a reason for hiding this comment

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

I see your commit to add these back in, but for some reason the diff is showing these are still removed.

Edit: Ah, I see it's on the dist version of the file. I wonder if git should exclude the dist files and generate them with build scripts instead? Makes reviewing easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@marcysutton Sorry about that! I forgot to re-build the dist. Fixed now.

@@ -72,31 +76,37 @@ document.addEventListener('DOMContentLoaded', function() {
* are no further keyboard events. The 100ms throttle handles cases where

Choose a reason for hiding this comment

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

If throttling is going away this comment should probably be updated, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@marcysutton nice eagle eye. Thank you! Fixed.

return;

var isTabKey = e.keyCode == 9;
var isTabNavigation = isTabKey || isTabKey && e.shiftKey;
Copy link
Collaborator

Choose a reason for hiding this comment

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

this seems a bit of a weird and'ing and or'ing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bkardell it is right in the source code, I just forgot to re-build the dist. Will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bkardell fixed now.

@marcysutton
Copy link

Initially I wasn't sure if suppressing focus on keyboard shortcuts was a good idea, but you can still override the focus style using outline-width...so this works. Here's a demo. https://codepen.io/marcysutton/pen/ryqKmd

@kloots
Copy link
Contributor Author

kloots commented Jun 7, 2017

@alice wanted to check in on this PR and if you've got any outstanding concerns that need to be addressed. @marcysutton and @bkardell have reviewed and I've addressed their issues. (@marcysutton, @bkardell either of you have any additional comments, or are you good?) I'm still waiting on a review from @robdodson.

@alice
Copy link
Member

alice commented Jun 7, 2017

Apologies, I've been in meetings all day. Yeah, would like to get @robdodson's 2c but he's out till next week.

@alice
Copy link
Member

alice commented Jun 7, 2017

@marcysutton

Initially I wasn't sure if suppressing focus on keyboard shortcuts was a good idea, but you can still override the focus style using outline-width...so this works.

Can you expand on how this plays into the context of whether focus has come from tab/shift tab or not?

@kloots
Copy link
Contributor Author

kloots commented Jun 7, 2017

@alice I think @marcysutton's comment with respect to "suppressing focus on keyboard shortcuts" was related to the thread you, @bkardell and I had going yesterday with respect to author-implemented directional navigation.

@marcysutton
Copy link

Yes, it was. With this version of the polyfill, tab/shift+tab worked as I expected and the keyboard shortcut case was still achievable with :focus.

@alice
Copy link
Member

alice commented Jun 7, 2017

Sure, the part I don't follow is how the outline style applies in the keyboard shortcut case - do you mean you would listen for the keyboard shortcut and then apply that style?

@marcysutton
Copy link

Here's a demo–I got a button to show a focus style after manually sending focus there via keyboard shortcut by using a combination of :focus and :focus-ring. I'm not sure if that's the use case you were talking about, but the change in this PR did suppress the focus style for that navigation pattern before I added a :focus override.

@alice
Copy link
Member

alice commented Jun 7, 2017

Right - that button also gets a focus ring if you just click it. So the part I'm wondering about is how we handle the case where we do want the button to get a focus ring on keyboard shortcuts, without reverting to simply :focus-style behaviour where it also gets a focus ring on click.

So perhaps it's just as simple as applying an extra class to the button after a keyboard shortcut is fired, to cause :focus to match?

@marcysutton
Copy link

Oh yeah, duh! This doesn't actually work then. And it highlights another issue–what if I want to show the focus style for focus management? I use that pattern often to show keyboard users where they are on the screen after sending focus to a new area. That style is suppressed with this change.

@kloots
Copy link
Contributor Author

kloots commented Jun 7, 2017

@alice @marcysutton I have to admit we're so far into this thread I've lost the original context with respect to:

  1. The problem as perceived by @marcysutton
  2. If the perceived problem is new/different from one we've already hashed out in previous comments for this PR or in issue When to match :focus-ring:? #33.

So, my apologies for not following along with you both.

If @marcysutton's concern is how to indicate focus when the user switches contexts (e.g. into a dialog, into a menu, or a side panel), I was under the impression @alice had already addressed that in her summary for why the recommendation to UAs is :focus-ring should only match on Tab or Shift + Tab:

"If a keyboard shortcut is likely to move focus, it is probably either showing some new UI, or it is specifically designed to assist keyboard users, in which case a manual focus style is likely to be applied."

@alice's comment is inline with my experience with any webapp I've ever worked on; the code managing a context switch—especially when moving to a dialog or menu—is also explicitly setting focus programmatically to the next control representing the next action the user is likely to take, and also applying some class to draw attention to said control.

In the case of a dialog this is most often either:

  1. The button confirming an action (e.g. Highlighting "Ok" in a confirmation dialog)
  2. Moving focus into the next most logical text box in a form in a dialog so the user can start typing

In either of these use case, authors can leverage the cascade to ensure the focus ring is visible in response to programatic focus following a context switch:

/* Hide focus ring by default */
:focus {
  outline: none
}

/* Show a focus outline when navigating via Tab */
:focus-ring {
  outline: 3px solid blue;
}

/* Ensure the default action button in dialogs has a focus outline */
.default-action-button:focus {
  background: green;
  outline: 3px solid blue;
}

In the case of a popup menu I've never had a need to render a focus outline as the selection change made via keyboard navigation is always intended to render to match the selection state on mouseover.

Anyway, sorry for the long message. I hope this resolves this issue (presuming this is the issue you are actually talking about).

@marcysutton
Copy link

marcysutton commented Jun 7, 2017

The reason I bring up the focus management use case is that it's impacted by this change. Sadly, I wouldn't use :focus-ring if it didn't provide adequate styling hooks for my use cases–I can continue to use What Input for that.

The problem I hoped :focus-ring would solve is that custom elements and focus management wrappers with tabindex should persist focus styles only after keyboard interaction (including "indirectly", per the spec). I encountered that same gotcha in my demo in this thread when I tried to override the behavior in this PR with a :focus style on a custom element. In my opinion, that puts us right back at the core of the problem.

For example, this code persists the focus style on click if the element is a custom control with tabindex="0" (now included in my demo):

// <div class="default-action-button" role="button" tabindex="0">
.default-action-button:focus {
  background: green;
  outline: 3px solid blue;
}

Custom controls aren't ideal, I know, but when they happen in the real world and persist on click, that's when focus styles get disabled entirely.

Whether you move focus to a wrapper element with tabindex="-1", or a native button element, or even a custom control, is widely dependent on implementation. My hope is that :focus-ring would give us better control over additional scenarios beyond native buttons with custom CSS styles. But I think we're having difficulty reconciling all of these use cases at the same time, and the big question is whether we can reliably suppress focus for Slack's use case while providing style hooks for custom controls and focus management targets.

Here are the various use cases I would hope this polyfill could cover:

  • Custom styles on natively focusable elements like button:focus that have historically persisted on click and triggered bug reports.
  • Custom controls using ARIA and tabindex="0", both directly and indirectly focused, which persist both the default focus ring and custom CSS :focus styles on click.
  • Focus management targets using tabindex="-1". (Should they not show focus at all? This is subjective)

This isn't an easy one to solve, so my apologies if this feels like piling on. But it was important for me to go back and remember my motivations for a tool like :focus-ring in the first place. Hopefully these details help bring that all into...focus. (I'll show myself out. :))

@kloots
Copy link
Contributor Author

kloots commented Jun 7, 2017

@marcysutton the custom control issue you call out is a legit problem, but that's just a bug and (IMO) orthogonal to the issue of the heuristics for the keyboard behavior we should match on. I'll open an issue and PR to address that... one moment.

@kloots
Copy link
Contributor Author

kloots commented Jun 7, 2017

@marcysutton actually, looking at the code and testing the demo we're already supporting custom controls save those with a tabindex set -1—which seems correct to me.

@marcysutton
Copy link

marcysutton commented Jun 7, 2017

looking at the code and testing the demo we're already supporting custom controls save those with a tabindex set -1

@kloots how? When I click on a custom element with tabindex="0", the focus ring shows. I want to make sure I didn't misunderstand the usage API.

@kloots
Copy link
Contributor Author

kloots commented Jun 7, 2017

@marcysutton ohhhh that! Yes, that is a bug. In fact, I believe I have fixed that in a separate PR.

@kloots
Copy link
Contributor Author

kloots commented Jun 7, 2017

@marcysutton I take it back, I cannot repro your bug in the demo on master or on this branch. Try clicking the <div> with a tabindex of 0 via the mouse. I don't see a focus ring except if I navigate to the element via the keyboard. Unless I still don't understand what you're talking about.

…listener such that it no longer calls addFocusRingClass()
@alice
Copy link
Member

alice commented Aug 4, 2017

👍

So sorry for letting this go cold!

@robdodson
Copy link
Collaborator

@kloots do you think this is ready to merge? If so let's 🚀

@kloots
Copy link
Contributor Author

kloots commented Aug 4, 2017

@robdodson It's ready to merge now. Just added one more commit from my fork which prevents handling focus and blur when the bubble to the document — which can caused errors in Firefox.

@robdodson
Copy link
Collaborator

Just started reviewing :)

:octocat: Sent from GH.

@robdodson
Copy link
Collaborator

I wanted to double check one last thing based on @marcysutton's comments.

In this example snippet you provided:

/* Hide focus ring by default */
:focus {
  outline: none
}

/* Show a focus outline when navigating via Tab */
:focus-ring {
  outline: 3px solid blue;
}

/* Ensure the default action button in dialogs has a focus outline */
.default-action-button:focus {
  background: green;
  outline: 3px solid blue;
}

The .default-action-button will always display a focus indicator of some kind. Including if it is clicked on with a mouse. Was that your intention? I think what you were trying to show was how to have it display a focus indicator in response to programmatic focus.

@kloots
Copy link
Contributor Author

kloots commented Aug 7, 2017

@robdodson yes, expected. In that instance the context (if I remember) was a focus style for a button that mapped to the default action for a dialog, and as such, the focus style should always be visible regardless of how the button received focus: programmatic (on initial display of the dialog) or via user interaction (via keyboard or mouse).

@robdodson
Copy link
Collaborator

@kloots ok. Just curious, do you have an example of using this behavior in Slack? I just wanted to see it in action if possible.

@kloots
Copy link
Contributor Author

kloots commented Aug 7, 2017

@robdodson no example of that behavior in Slack (at least I don't think). But I think this specific point is outside the scope of this PR and is the reason why @alice started a separate discussion for this in issue #42, no? Or is this blocking the merging of this PR?

@robdodson
Copy link
Collaborator

I think it's blocking me because I'm starting to wonder if a one-size-fits-all :focus-ring style, by itself, is workable. Seeing Slack and Twitter fork it so they can add specific behavior is really interesting to me and makes me wonder if we need to add in the power of #42 at the same time. Otherwise it seems like someone will always be unhappy. As @marcysutton stated, I think she prefers the way it currently works and if we made this change she would switch back to WhatInput.

@alice
Copy link
Member

alice commented Aug 8, 2017

I say let's go ahead and merge (and apologies again for how long that took) - but definitely stay open to feedback from people who feel this change breaks things.

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.

5 participants