Skip to content

Commit

Permalink
Merge 11b74d4 into fad9d0c
Browse files Browse the repository at this point in the history
  • Loading branch information
rodneyrehm committed Feb 1, 2016
2 parents fad9d0c + 11b74d4 commit 53c5f39
Show file tree
Hide file tree
Showing 20 changed files with 785 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -39,12 +39,14 @@ The following lists show the changes to the library grouped by domain.
* fixing [`ally.get.focusTarget`][ally/get/focus-target] to resolve elements redirecting focus to other elements
* fixing [`ally.is.tabbable`][ally/is/tabbable] to consider `<iframe>` elements not tabbable
* fixing [`ally.is.onlyTabbable`][ally/is/only-tabbable] to not consider `<object>` elements only tabbable anymore
* adding [`ally.is.activeElement`][ally/is/active-element] to identify if an element is the activeElement within its context


#### Keyboard support

* changing [`ally.when.key`][ally/when/key] to handle modifier keys and respect `context` and `filter` options - [issue #59](https://github.com/medialize/ally.js/issues/59)
* changing [`ally.map.keycode`][ally/map/keycode] to provide alphanumeric keys and aliasing
* adding [`ally.maintain.tabFocus`][ally/maintain/tab-focus] to trap <kbd>TAB</kbd> focus in the tabsequence - [issue #63](https://github.com/medialize/ally.js/issues/63)

#### Internals

Expand Down Expand Up @@ -229,6 +231,7 @@ Version `1.0.0` is a complete rewrite from the the early `0.0.x` releases, there
[ally/get/parents]: http://allyjs.io/api/get/parents.html
[ally/get/shadow-host-parents]: http://allyjs.io/api/get/shadow-host-parents.html
[ally/get/shadow-host]: http://allyjs.io/api/get/shadow-host.html
[ally/is/active-element]: http://allyjs.io/api/is/active-element.html
[ally/is/disabled]: http://allyjs.io/api/is/disabled.html
[ally/is/focus-relevant]: http://allyjs.io/api/is/focus-relevant.html
[ally/is/focusable]: http://allyjs.io/api/is/focusable.html
Expand All @@ -240,6 +243,7 @@ Version `1.0.0` is a complete rewrite from the the early `0.0.x` releases, there
[ally/is/visible]: http://allyjs.io/api/is/visible.html
[ally/maintain/disabled]: http://allyjs.io/api/maintain/disabled.html
[ally/maintain/hidden]: http://allyjs.io/api/maintain/hidden.html
[ally/maintain/tab-focus]: http://allyjs.io/api/maintain/tab-focus.html
[ally/map/attribute]: http://allyjs.io/api/map/attribute.html
[ally/map/keycode]: http://allyjs.io/api/map/keycode.html
[ally/observe/interaction-type]: http://allyjs.io/api/observe/interaction-type.html
Expand Down
2 changes: 2 additions & 0 deletions docs/api/README.md
Expand Up @@ -30,6 +30,7 @@ While it's best to use standardized features and leave browsers to figure things

* [`ally.maintain.disabled`](maintain/disabled.md) renders elements inert to prevent any user interaction
* [`ally.maintain.hidden`](maintain/hidden.md) sets `aria-hidden="true"` on insignificant branches
* [`ally.maintain.tabFocus`](maintain/tab-focus.md) traps <kbd>TAB</kbd> focus in the tabsequence


## Finding elements
Expand All @@ -46,6 +47,7 @@ In order to work with focusable elements, we must first know which elements we'r

Unlike any other ally modules, these components do not take take [`options.context` argument](concepts.md#Single-options-argument), but expect the `element` as first argument, allowing easy use in [`.filter()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). See [what does "focusable" mean?](../what-is-focusable.md) for a differentiation.

* [`ally.is.activeElement`](is/active-element.md) returns true if the element is the activeElement of its host context, i.e. its document, iFrame or ShadowHost
* [`ally.is.disabled`](is/disabled.md) returns true if the element is `:disabled`
* [`ally.is.focusRelevant`](is/focus-relevant.md) returns true if the element is considered theoretically focusable
* [`ally.is.focusable`](is/focusable.md) returns true if the element is considered focusable by script
Expand Down
56 changes: 56 additions & 0 deletions docs/api/is/active-element.md
@@ -0,0 +1,56 @@
---
layout: doc-api.html
tags: argument-list, shadow-dom
---

# ally.is.activeElement

Determines if an element is the activeElement of its host context, i.e. its document, iFrame or ShadowHost.


## Description


## Usage

```js
var element = document.getElementById('victim');
var isActiveElement = ally.is.activeElement(element);
```

### Arguments

| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| element | [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) | *required* | The Element to test. |

### Returns

Boolean, `true` if the element is the activeElement of its host context.

### Throws

[`TypeError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError) if `element` argument is not of type `HTMLElement`.


## Examples


## Changes

* Added in `v#master`.


## Notes



## Related resources


## Contributing

* [module source](https://github.com/medialize/ally.js/blob/master/src/is/active-element.js)
* [document source](https://github.com/medialize/ally.js/blob/master/docs/api/is/active-element.md)
* [unit test](https://github.com/medialize/ally.js/blob/master/test/unit/is.active-element.test.js)

76 changes: 76 additions & 0 deletions docs/api/maintain/tab-focus.example.html
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ally.maintain.hidden Example</title>
<link rel="jsbin" href="">
<style id="example-css">
:disabled,
[data-ally-disabled] {
outline: 1px solid red;
opacity: 0.5;
}
</style>
</head>
<body>

<article id="example-introduction">
<h1><code>ally.maintain.tabFocus</code> Example</h1>

<p>
Use the <code>trap focus</code> button to engage <code>ally.maintain.disabled</code> and <code>ally.maintain.tabFocus</code> so that only the input elements <em>first</em>, <em>second</em> and <em>third</em> can be focused by pressing the <kbd>Tab</kbd> key.
Press the <kbd>Escape</kbd> key to stop trapping focus.
Note that the disabled elements are visualized with reduced opacity and a red border only for this demo.
</p>
</article>

<div id="example-html">
<main>
<button id="toggle" type="button">trap focus</button>

<hr>

<input type="text" value="before">

<div id="container">
<input type="text" value="second" tabindex="0">
<input type="text" value="not keyboard focusable" tabindex="-1">
<input type="text" value="first" tabindex="1" id="start-focus">
<input type="text" value="third" tabindex="0">
</div>

<input type="text" value="after">

</main>
</div>

<script src="https://cdn.jsdelivr.net/ally.js/1.0.1/ally.min.js"></script>

<script id="example-js">
var handle;

var wrapper = document.getElementById('example-html');
var container = document.getElementById('container');
var toggle = document.getElementById('toggle');

toggle.addEventListener('click', function() {
handle = ally.maintain.tabFocus({
context: container,
});

ally.when.key({
escape: function(event, disengage) {
handle.disengage();
handle = null;
toggle.focus();
disengage();
},
});

document.getElementById('start-focus').focus();
});

</script>

</body>
</html>
66 changes: 66 additions & 0 deletions docs/api/maintain/tab-focus.md
@@ -0,0 +1,66 @@
---
layout: doc-api.html
tags: service, argument-object
---

# ally.maintain.tabFocus

Traps <kbd>TAB</kbd> focus in the tabsequence to prevent the browser from shifting focus to its UI (e.g. the location bar).


## Description

`ally.maintain.tabFocus` intercepts the keyboard events for <kbd>Tab</kbd> and <kbd>Shift Tab</kbd> in order to make sure the element receiving focus is part of the context element's tabsequence ([Sequential Navigation Focus Order](../../concepts.md#Sequential-navigation-focus-order)). The tabsequence is obtained via [`ally.query.tabsequence`](../query/tabsequence.md) in order to follow the browser's rules of sorting the sequence.

As focus can be shifted by various means, even other keyboard commands (e.g. via spatial navigation), it is also necessary to engage [`ally.maintain.disabled`](disabled.md), whenever `ally.maintain.tabFocus` is engaged.


## Usage

```js
var handle = ally.maintain.tabFocus({
context: '.dialog',
});

handle.disengage();
```

### Arguments

| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| context | [`<selector>`](../concepts.md#Selector) | [`documentElement`](https://developer.mozilla.org/en-US/docs/Web/API/Document/documentElement) | The scope of the DOM in which to consider the tabsequence. The first element of a collection is used. |

### Returns

A [`<service>`](../concepts.md#Service) interface, providing the `handle.disengage()` method to stop the service.

### Throws


## Examples

* **EXAMPLE:** [`ally.maintain.tabFocus` Example](./tab-focus.example.html)


## Changes

* Added in `v#master`.


## Notes

* **WARNING:** As SVG elements cannot be focused by script in Internet Explorer and Firefox, these elements will not be part of the tabsequence, thus not reachable when `ally.maintain.tabFocus` is active.


## Related resources

* [`ally.maintain.disabled`](disabled.md) is a [service](../concepts.md#Service) disabling interactive elements in the DOM


## Contributing

* [module source](https://github.com/medialize/ally.js/blob/master/src/maintain/tab-focus.js)
* [document source](https://github.com/medialize/ally.js/blob/master/docs/api/maintain/tab-focus.md)
* [unit test](https://github.com/medialize/ally.js/blob/master/test/unit/maintain.tab-focus.test.js)

2 changes: 1 addition & 1 deletion docs/concepts.md
Expand Up @@ -33,7 +33,7 @@ The Accessibility Tree (often abbreviated "AT", which may be ambiguous as it is

## Sequential navigation focus order

The Sequential Navigation Focus Order, also referred to as the Tabbing Order, is an ordered list of all keyboard focusable elements in a document. Unless elements are moved to the front of the list by specifying a positive `tabindex` attribute such as `tabindex="1"`, the tabbing order correlates to the DOM order. Users can usually navigate to the next and previous element in the list by pressing the <kbd>Tab</kbd> and <kbd>Shift Tab</kbd> keys respectively.
The Sequential Navigation Focus Order, also referred to as the Tabbing Order or *tabsequence*, is an ordered list of all keyboard focusable elements in a document. Unless elements are moved to the front of the list by specifying a positive `tabindex` attribute such as `tabindex="1"`, the tabbing order usually correlates to the DOM order. Users can usually navigate to the next and previous element in the list by pressing the <kbd>Tab</kbd> and <kbd>Shift Tab</kbd> keys respectively.


## Virtual focus
Expand Down
36 changes: 35 additions & 1 deletion docs/tutorials/accessible-dialog.md
Expand Up @@ -304,7 +304,6 @@ A naive implementation might listen to `keydown` events, filtering for <kbd>Tab<

* focus may *not only* be shifted through <kbd>Tab</kbd>, as users of [spatial navigation](http://blog.codinghorror.com/spatial-navigation-and-opera/) will attest
* assistive tools that provide more than sequential focus navigation (i.e. random access) may list all focusable elements of the page, including those visually behind the backdrop
* by redirecting focus to stay within the dialog it becomes impossible to reach the browser's UI by pressing <kbd>Tab</kbd>, so we're breaking native browser behavior

We could do away with the need to react to <kbd>Tab</kbd>, by simply hiding everything outside the dialog. But while setting everything to `visibility: hidden;` would certainly do the job, it would also visually hide *everything but the dialog*, rendering the backdrop useless. Most visual designers I know digress.

Expand All @@ -324,6 +323,7 @@ function openDialog() {
disabledHandle = ally.maintain.disabled({
filter: dialog,
});

// create or show the dialog
dialog.hidden = false;
}
Expand All @@ -336,6 +336,40 @@ function closeDialog() {
}
```

### Reacting to <kbd>Tab</kbd> and <kbd>Shift Tab</kbd>

When the last element of the document's tabbing order has focus and the user presses the <kbd>Tab</kbd> key, focus is not wrapped around to the first element of the tabbing order, but to the browser's UI (e.g. location bar or tabs). The same is true for the first element being focused and the user pressing <kbd>Shift Tab</kbd>.

This is not quite the behavior we see in the modal dialogs provided by our operating systems, where focus is always trapped within the dialog. This is a behavior keyboard users have come to expect and we need to replicate in our web UIs as well.

While [`ally.maintain.disabled`](../api/maintain/disabled.md) makes sure we can't focus any other element within the document, we still need to observe the <kbd>Tab</kbd> key to make focus wrap within the dialog's tabbing order. That's what [`ally.maintain.tabFocus`](../api/maintain/tab-focus.md) is for.

```js
var dialog = document.getElementById('dialog');
var tabHandle;

function openDialog() {
// Make sure that Tab key controlled focus is trapped within
// the tabsequence of the dialog and does not reach the
// browser's UI, e.g. the location bar.
tabHandle = ally.maintain.tabFocus({
context: dialog,
});

// create or show the dialog
dialog.hidden = false;
}

function closeDialog() {
// undo trapping Tab key focus
tabHandle.disengage();
// hide or remove the dialog
dialog.hidden = true;
}
```

* **NOTE:** [`ally.maintain.tabFocus`](../api/maintain/tab-focus.md) was added in version `v#master`.

### Reacting to <kbd>Enter</kbd> and <kbd>Escape</kbd>

The <kbd>Escape</kbd> key usually closes (dismisses) a dialog and the <kbd>Enter</kbd> key usually activates the dialog's primary action. Because our example uses a `<form>` and a submit button for the save action, we don't have to listen for <kbd>Enter</kbd>, but can instead rely on the `submit` event of the `<form>`.
Expand Down
10 changes: 10 additions & 0 deletions docs/tutorials/dialog.example.html
Expand Up @@ -210,6 +210,7 @@ <h1 id="dialog-title">Name Entry</h1>

// Data filled by openDialog() and later used by closeDialog()
var keyHandle;
var tabHandle;
var disabledHandle;
var hiddenHandle;
var focusedElementBeforeDialogOpened;
Expand Down Expand Up @@ -258,6 +259,13 @@ <h1 id="dialog-title">Name Entry</h1>
filter: dialog,
});

// Make sure that Tab key controlled focus is trapped within
// the tabsequence of the dialog and does not reach the
// browser's UI, e.g. the location bar.
tabHandle = ally.maintain.tabFocus({
context: dialog,
});

// React to enter and escape keys as mandated by ARIA Practices
keyHandle = ally.when.key({
escape: closeDialogByKey,
Expand Down Expand Up @@ -289,6 +297,8 @@ <h1 id="dialog-title">Name Entry</h1>
function closeDialog() {
// undo listening to keyboard
keyHandle.disengage();
// undo trapping Tab key focus
tabHandle.disengage();
// undo hiding elements outside of the dialog
hiddenHandle.disengage();
// undo disabling elements outside of the dialog
Expand Down
2 changes: 2 additions & 0 deletions src/is/_is.js
@@ -1,6 +1,7 @@

// exporting modules to be included the UMD bundle

import activeElement from './active-element';
import disabled from './disabled';
import focusRelevant from './focus-relevant';
import focusable from './focusable';
Expand All @@ -11,6 +12,7 @@ import validArea from './valid-area';
import validTabindex from './valid-tabindex';
import visible from './visible';
export default {
activeElement,
disabled,
focusRelevant,
focusable,
Expand Down
27 changes: 27 additions & 0 deletions src/is/active-element.js
@@ -0,0 +1,27 @@

// Determines if an element is the activeElement within its context, i.e. its document iFrame or ShadowHost

import getShadowHost from '../get/shadow-host';
import getDocument from '../util/get-document';

export default function(element) {
if (element === document) {
element = document.documentElement;
}

if (!element || element.nodeType !== Node.ELEMENT_NODE) {
throw new TypeError('is/active-element requires an argument of type Element');
}

const _document = getDocument(element);
if (_document.activeElement === element) {
return true;
}

const shadowHost = getShadowHost({ context: element });
if (shadowHost && shadowHost.shadowRoot.activeElement === element) {
return true;
}

return false;
}
3 changes: 3 additions & 0 deletions src/maintain/_maintain.js
Expand Up @@ -3,7 +3,10 @@

import disabled from './disabled';
import hidden from './hidden';
import tabFocus from './tab-focus';

export default {
disabled,
hidden,
tabFocus,
};

0 comments on commit 53c5f39

Please sign in to comment.