Skip to content

Commit

Permalink
Allow explicitly setting focus-ring class
Browse files Browse the repository at this point in the history
  • Loading branch information
Alice Boxhall committed Mar 8, 2017
1 parent fc6b3d1 commit 8d00580
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 96 deletions.
77 changes: 58 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
Based on a conversation between Alice Boxhall, Brian Kardell and Marcy Sutton, this prototype attaches/manages metadata in the form of a `modality` attribute to the body, as a way to allow authors to experiment with adapting style based on the user's _active_ input modality (i.e., how they are interacting with the UI _right now_).
Based on the proposed CSS
[`:focus-ring`](https://drafts.csswg.org/selectors-4/#the-focusring-pseudo)
pseudo-selector,
this prototype adds a `focus-ring` class to the focused element,
in situations in which the `:focus-ring` pseudo-selector should match.

[Demo](https://alice.github.io/modality/demo)
[Demo](https://alice.github.io/focus-ring/demo)

## Rationale

There are many instances in which it would be useful for authors to understand the user's current interaction modality and be able to adapt the UI with better accomodations. The motivating example is `:focus` where the status quo is quite problematic:
The status quo, `:focus`, is quite problematic:

- The default focus ring is not based on a single :focus rule as some might expect, not all things which can receive focus receive a ring in all cases. Adding such a rule is always currently problematic, but it's also exceptionally common.
- Many developers disable the default focus ring in their CSS styles, others attempt to style it in concert with their design. The former often seems to be a result of finding the default focus ring both aesthetically unpleasant and confusing to users when applied after a mouse or touch event and introduces accessibility problems. The latter inevitably creates considerably more of the kind of problem that the former was trying to solve.
- Many developers disable the default focus ring in their CSS styles,
others attempt to style it in concert with their design.
The former often seems to be a result of finding the default focus ring
both aesthetically unpleasant and confusing to users
when applied after a mouse or touch event and introduces accessibility problems.
The latter inevitably creates considerably more of the kind of problem that the former was trying to solve.
- Some native elements in some browsers,
notably `<button>` in Chrome,
have a "magic" focus style which does _not_ apply
unless focus was received via a keyboard interaction.

To deal with this:
- It seems evident that a visual indication of what has focus is only interesting to a user who is using the keyboard to interact with the page. A user using any kind of pointing device would only be interested in what is in focus if they were _just about_ to use the keyboard - otherwise, it is irrelevant and potentially confusing.
- Thus, if we only show the focus ring when relevant, we can avoid user confusion and avoid creating incentives for developers to disable it.
- A mechanism for exposing focus ring styles only when the keyboard is the user's current input modality gives us this opportunity.
- It seems evident that a visual indication of what has focus
is only interesting to a user who is using the keyboard
to interact with the page.
A user using any kind of pointing device
would only be interested in what is in focus
if they were _just about_ to use the keyboard -
otherwise, it is irrelevant and potentially confusing.
- Thus, if we only show the focus ring when relevant,
we can avoid user confusion
and avoid creating incentives for developers to disable it.
- A mechanism for exposing focus ring styles
only when the keyboard is the user's current input modality
gives us this opportunity.

## API Proposal

Expand All @@ -23,13 +45,13 @@ To deal with this:
}

/* establish desired focus ring appearance for appropriate input modalities */
:focusring {
:focus-ring {
outline: 2px solid blue;
}
```

:focusring matches native elements that are
1. focussed; and
1. focussed; and
2. would display a focus ring if only UA styles applied

Additionally, :focusring matches non-native elements as if they were
Expand All @@ -45,24 +67,41 @@ the modality is not keyboard.

## Implementation Prototype

The tiny [keyboard-modality.js](http://alice.github.io/modality/src/keyboard-modality.js) provides a prototype intended to achieve the goals we are proposing with technology that exists today in order for developers to be able to try it out, understand it and provide feedback. Simply speaking, it sets a `modality=keyboard` attribute on `body` if the script determines that the keyboard is being used. Similarly, the attribute is removed if the script determines that the user is no longer using the keyboard. This allows authors to write rules which consider the input modality and style appropriately. Note that the prototype does not match the proposed API - it is intended to give developers a feel for the model rather than to provide a high-fidelity polyfill.
The tiny
[focus-ring.js](http://alice.github.io/focus-ring/src/focus-ring.js)
provides a prototype intended to achieve the goals we are proposing
with technology that exists today
in order for developers to be able to try it out, understand it and provide feedback.
It simply sets a `.focus-ring` class on the active element
if the script determines that the keyboard is being used.
This attribute is removed on any `blur` event.
This allows authors to write rules
which show a focus style only when it would be relevant to the user.
Note that the prototype does not match the proposed API -
it is intended to give developers a feel for the model
rather than to provide a high-fidelity polyfill.

We suggest that users
selectively disable the default focus style
by selecting for the case when `.focus-ring` is _not_ applied:

It also simulates how the default UA styles would be adjusted by appending the following style as the first rule in the page, which disables the focus ring _unless_ `modality` is set to `keyboard`:

```html
body:not([modality=keyboard]) :focus {
:focus:not(.focus-ring) {
outline: none;
}
```

(This is added in a `<style>` element with the ID `"disable-focus-ring"`, to allow easy removal if different behaviour is desired.)

If there are elements which should always have a focus ring shown,
authors may explicitly add the `focus-ring` class.
If explicitly added, it will not be removed on `blur`.

### How it works
The script uses two heuristics to determine whether the keyboard is being used:

- a `focus` event immediately following a `keydown` event
- focus moves into an element which requires keyboard interaction, such as a text field
- _TODO: ideally, we also trigger keyboard modality following a keyboard event which activates an element or causes a mutation; this still needs to be implemented._

Custom elements may use the `supports-modality` attribute to provide a whitelist of supported modalities; any element without this whitelist is considered to support all modalities. Only elements which only support keyboard modality will trigger the `modality=keyboard` attribute on `<body>`.
- focus moves into an element which requires keyboard interaction,
such as a text field
- _TODO: ideally, we also trigger keyboard modality
following a keyboard event which activates an element or causes a mutation;
this still needs to be implemented._
15 changes: 8 additions & 7 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<html><head>
<!-- Original test page written by Dominic Mazzoni -->
<title>modality attribute demo</title>
<title>focus-ring class demo</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
div,input,textarea,h1,h2 {
Expand All @@ -21,10 +21,10 @@
width: 10em;
}
</style>
<script src="../src/focusring.js"></script>
<script src="../src/focus-ring.js"></script>
<style>
.focus-ring {
outline: 2px solid green;
:focus:not(.focus-ring) {
outline: none;
}
</style>
</head>
Expand All @@ -48,9 +48,6 @@ <h2>Buttons</h2>
<div>
<button>Button</button>
</div>
<div>
<button supports-modality=keyboard>Button with <code>supports-modality=keyboard</code></button>
</div>
<div>
<input type="submit" value="Submit button">
</div>
Expand All @@ -61,6 +58,10 @@ <h2>Buttons</h2>
<label><input name="a" type="radio">Radio 1</label>
<label><input name="a" type="radio">Radio 2</label>
</div>
<h2>Focusable <code>&lt;div&gt;</code>s</h2>
<div tabindex="0"><code>tabindex="0"</code></div>
<div tabindex="-1"><code>tabindex="-1"</code></div>
<div tabindex="0" class="focus-ring"><code>tabindex="0"</code> with explicit <code>class="focus-ring"</code></div>
<h2>Selects</h2>
<label>Select one from drop-down:<br>
<select>
Expand Down
60 changes: 60 additions & 0 deletions src/focus-ring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* https://github.com/WICG/focusring */
document.addEventListener('DOMContentLoaded', function() {
var hadKeyboardEvent = false,
keyboardModalityWhitelist = [ 'input:not([type])',
'input[type=text]',
'input[type=number]',
'input[type=date]',
'input[type=time]',
'input[type=datetime]',
'textarea',
'[role=textbox]' ].join(','),
isHandlingKeyboardThrottle,
matcher = (function () {
var el = document.body;
if (el.matchesSelector)
return el.matchesSelector;
if (el.webkitMatchesSelector)
return el.webkitMatchesSelector;
if (el.mozMatchesSelector)
return el.mozMatchesSelector;
if (el.msMatchesSelector)
return el.msMatchesSelector;
console.error('Couldn\'t find any matchesSelector method on document.body.');
}()),
focusTriggersKeyboardModality = function (el) {
var triggers = false;
if (matcher) {
triggers = matcher.call(el, keyboardModalityWhitelist) && matcher.call(el, ':not([readonly]');
}
return triggers;
};

document.body.addEventListener('keydown', function() {
hadKeyboardEvent = true;
if (document.activeElement.matches(':focus'))
document.activeElement.classList.add('focus-ring');
if (isHandlingKeyboardThrottle) {
clearTimeout(isHandlingKeyboardThrottle);
}
isHandlingKeyboardThrottle = setTimeout(function() {
hadKeyboardEvent = false;
}, 100);
}, true);

document.body.addEventListener('focus', function(e) {
if (!hadKeyboardEvent && !focusTriggersKeyboardModality(e.target))
return;
if (e.target.classList.contains('focus-ring'))
return;
e.target.classList.add('focus-ring');
e.target.setAttribute('data-focus-ring-added', '');
}, true);

document.body.addEventListener('blur', function(e) {
if (!e.target.hasAttribute('data-focus-ring-added'))
return;
e.target.classList.remove('focus-ring');
e.target.removeAttribute('data-focus-ring-added')
}, true);
});
70 changes: 0 additions & 70 deletions src/focusring.js

This file was deleted.

0 comments on commit 8d00580

Please sign in to comment.