Skip to content

Commit

Permalink
Make focusable container logic opt in (#24)
Browse files Browse the repository at this point in the history
* Only focus containers with class

* Refactor tests

* Update README

* Make the focusable class work alone

* Switch to attribute rather than class

* Fix scrolling with keyboard
Don't block overflow, only if it's a keypress
  • Loading branch information
Murreey committed Aug 11, 2023
1 parent 89a1849 commit eeed08a
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 262 deletions.
53 changes: 34 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,43 @@
Move focus around a HTML document using Left, Right, Up, Down keys.

## API

<pre>
getNextFocus(<i>currentFocus</i>, <i>keyCode</i>, <i>[scope]</i>)
</pre>

### Parameters
* `currentFocus` should be an
[`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement)
that you want LRUD spatial to consider as the element you are navigating _from_.
In simple applications, this can just be a reference to `document.activeElement`.
* `keyCode` should be a
[`keyCode`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)
decimal representing the directional key pressed.
* `scope` is an optional `HTMLElement` that you only want to look for focusable candidates inside of. Defaults to the whole page if not provided.

- `currentFocus` should be an
[`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement)
that you want LRUD spatial to consider as the element you are navigating _from_.
In simple applications, this can just be a reference to `document.activeElement`.
- `keyCode` should be a
[`keyCode`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)
decimal representing the directional key pressed.
- `scope` is an optional `HTMLElement` that you only want to look for focusable candidates inside of. Defaults to the whole page if not provided.

### Returns

An `HTMLElement` that LRUD spatial thinks you should
navigate _to_.

## Focusables

LRUD spatial defines focusable elements as those which match any of the
following CSS selectors:
* `[tabindex]`
* `a`
* `button`
* `input`

- `[tabindex]`
- `a`
- `button`
- `input`

### Ignoring Focusables

Any potential candidate with the `lrud-ignore` class, or inside any parent with the `lrud-ignore` class, will not be considered focusable and will be skipped over. By default LRUD will not ignore candidates that have `opacity: 0` or have a parent with `opacity: 0`, so this class can be used for that.

## Containers

Focusables can be wrapped in containers. Containers can keep track of the last
active focusable item within them using a `data-focus` attribute.

Expand All @@ -46,20 +53,23 @@ used instead.
At this time, containers are defined as matching the CSS selectors:
`nav`, `section` or `.lrud-container`.

Adding attribute `data-lrud-consider-container-distance` on a container will make LRUD also measure distances to that container's boundary, as well as its children. If the container is the closest of all the possible candidates assessed, LRUD will return one of its children - even if they are not necessarily the spatially closest focusable. The above container focus logic will still be used, so if the container has a previous focus state that will be the returned element. This allows for layouts where moving between containers is the desired behaviour, but their individual elements may not be in the correct positions for that. By default LRUD will only consider focusables when measuring, and ignores container positions.

### Block exits
In some instances, it is desirable to prevent lrud-spatial from selecting another
"best candidate" for example at the bottom of a list. To do this, a container element
should add an additional `data-block-exit` attribute to prevent further selection in

In some instances, it is desirable to prevent lrud-spatial from selecting another
"best candidate" for example at the bottom of a list. To do this, a container element
should add an additional `data-block-exit` attribute to prevent further selection in
that direction. This should be a space separated list.

E.g.
E.g.

```html
<div class="lrud-container" data-block-exit="up down">
...
</div>
<div class="lrud-container" data-block-exit="up down">...</div>
```

## How does it work?

To determine the next element that should be focused;

1. Uses the key code to get the direction of movement
Expand All @@ -71,16 +81,19 @@ To determine the next element that should be focused;
length

## Developing

Requirements: Node.js 18

To get started, run `npm ci`.

### Building

`npm run build` will emit a transpiled and minified version of the library.
This is run during CI to prepare the artifact published to NPM. It can also be
useful for integrating against another local project with `npm link`.

### Testing

The `test/layouts` directory contains HTML designed to mirror various
real-world use cases, and allow for behavioural vertification of library
features such as containers and grids.
Expand All @@ -90,6 +103,7 @@ Significant new features should come with corresponding layouts that test them.
Use `npm test` to run the tests.

#### Debugging

The tests use [puppeteer](https://github.com/puppeteer/puppeteer) to spin up a
headless browser. The browser loads the layouts mentioned above and runs
scenarios from [lrud.test.js](./test/lrud.test.js) against the unminified
Expand All @@ -102,4 +116,5 @@ select a layout.
> Remember to terminate this process before running the tests again!
## Contact

[TVOpenSource@bbc.co.uk](mailto:TVOpenSource@bbc.co.uk)
3 changes: 2 additions & 1 deletion lib/lrud.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// Any "interactive content" https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Interactive_content
const focusableSelector = '[tabindex], a, input, button';
const containerSelector = 'nav, section, .lrud-container';
const focusableContainerSelector = '[data-lrud-consider-container-distance]';
const ignoredClass = 'lrud-ignore';

/**
Expand Down Expand Up @@ -284,7 +285,7 @@ export const getNextFocus = (elem, keyCode, scope) => {
if (!bestCandidate) {
const focusableCandidates = [
...getFocusables(scope),
...toArray(scope.querySelectorAll(containerSelector)).filter(container => getFocusables(container)?.length > 0)
...toArray(scope.querySelectorAll(focusableContainerSelector)).filter(container => getFocusables(container)?.length > 0)
];

bestCandidate = getBestCandidate(elem, focusableCandidates, exitDir);
Expand Down
11 changes: 7 additions & 4 deletions test/handleKeyDown.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ let scope = null;
* @param {Event} event The key event
*/
const handleKeyDown = (event) => {
const nextFocus = getNextFocus(event.target, event.keyCode, scope);
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
event.preventDefault();
const nextFocus = getNextFocus(event.target, event.keyCode, scope);

if (nextFocus) {
nextFocus.focus();
if (nextFocus) {
nextFocus.focus();
}
}
};

window.setScope = (newScope) => scope = newScope;

window.addEventListener('click', (e) => {
if (e.target.nodeName !== 'INPUT') {
if (e.target.nodeName !== 'INPUT' && e.target.nodeName !== 'LABEL') {
e.preventDefault();
}
});
Expand Down
22 changes: 13 additions & 9 deletions test/layouts/container-with-empty-space.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<section>
<section style="width: 980px" id="section-1">
<a id="item-1" class="item" href="">
<h2>1</h2>
</a>
Expand All @@ -14,20 +14,20 @@ <h2>3</h2>
<a id="item-4" class="item" href="">
<h2>4</h2>
</a>
</section>

<section style="width: 980px">
<a id="item-5" class="item" href="">
<a id="item-5" class="item" href="">
<h2>5</h2>
</a>
</section>

<section>
<a id="item-6" class="item" href="">
<h2>6</h2>
</a>
<section style="width: 980px" id="section-2">
<a id="item-6" class="item" href="">
<h2>6</h2>
</a>
</section>

<a id="item-7" class="item" href="">
<section id="section-3">
<a id="item-7" class="item" href="">
<h2>7</h2>
</a>

Expand All @@ -38,4 +38,8 @@ <h2>8</h2>
<a id="item-9" class="item" href="">
<h2>9</h2>
</a>

<a id="item-10" class="item" href="">
<h2>10</h2>
</a>
</section>
41 changes: 41 additions & 0 deletions test/layouts/focusable-container-with-empty-space.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<section style="width: 980px" id="section-1">
<a id="item-1" class="item" href="">
<h2>1</h2>
</a>

<a id="item-2" class="item" href="">
<h2>2</h2>
</a>

<a id="item-3" class="item" href="">
<h2>3</h2>
</a>

<a id="item-4" class="item" href="">
<h2>4</h2>
</a>
</section>

<section style="width: 980px" id="section-2" data-lrud-consider-container-distance>
<a id="item-6" class="item" href="">
<h2>6</h2>
</a>
</section>

<section id="section-3">
<a id="item-7" class="item" href="">
<h2>7</h2>
</a>

<a id="item-8" class="item" href="">
<h2>8</h2>
</a>

<a id="item-9" class="item" href="">
<h2>9</h2>
</a>

<a id="item-10" class="item" href="">
<h2>10</h2>
</a>
</section>

This file was deleted.

This file was deleted.

Loading

0 comments on commit eeed08a

Please sign in to comment.