Skip to content

Commit

Permalink
Merge pull request #2 from knuton/rewrite
Browse files Browse the repository at this point in the history
Add initial implementation of library
  • Loading branch information
knuton authored Feb 10, 2024
2 parents 6353be3 + 1554e0f commit da00214
Show file tree
Hide file tree
Showing 21 changed files with 3,711 additions and 25 deletions.
65 changes: 62 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,75 @@
name: Chrome
name: Test suite
on:
push:
pull_request:
# Allows to run this workflow manually from the Actions tab
workflow_dispatch:

jobs:
chrome:
runs-on: ubuntu-22.04
formatting:
runs-on: ubuntu-latest
name: Formatting
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v2
with:
node-version: "20"

- run: npm install

- run: npx prettier --check .

typechecks:
runs-on: ubuntu-latest
name: Typechecks
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v2
with:
node-version: "20"

- run: npm install

- run: npx tsc

e2e-chrome:
runs-on: ubuntu-latest
name: E2E on Chrome
steps:
- uses: actions/checkout@v4

- uses: cypress-io/github-action@v6
with:
browser: chrome

e2e-firefox:
runs-on: ubuntu-latest
name: E2E on Firefox
steps:
- uses: actions/checkout@v4

- uses: cypress-io/github-action@v6
with:
browser: firefox

e2e-edge:
runs-on: windows-latest
name: E2E on Edge
steps:
- uses: actions/checkout@v4

- uses: cypress-io/github-action@v6
with:
browser: edge

e2e-safari:
runs-on: macos-latest
name: E2E on WebKit
steps:
- uses: actions/checkout@v4

- uses: cypress-io/github-action@v6
with:
browser: webkit
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/*
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
semi: false
trailingComma: "none"
tabWidth: 2
overrides:
- files: "cypress/**/*"
options:
printWidth: 120
132 changes: 110 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,144 @@
# focus-shift

## Introduction
focus-shift is a lightweight JavaScript library designed for keyboard-based navigation in web applications. It allows users to move focus between elements using the arrow keys. The behaviour of focus shifting can be guided by annotations in the HTML markup.

The library is kept simple and assumes use in kiosk-like interfaces.
focus-shift is a lightweight, zero-dependency JavaScript library designed for keyboard-based navigation in web applications. It restricts itself to shifting focus between elements in response to arrow key events. The behaviour of focus shifting can be guided by annotations in the HTML markup. This allows the library to work well with technologies that prefer generating HTML over interacting with JavaScript directly.

## Features
- Move focus with arrow keys

- Move focus with the arrow keys
- Declare groups with custom focus strategies
- Mark subtrees of the DOM as muted
- Mark subtrees of the DOM that should trap focus
- Mark subtrees of the DOM that should be skipped

## Usage

Include the library in your HTML file:

```html
<script src="focus-shift.js"></script>
```

### Basic Example
Here's a simple example of how to use the library:

Here's a simple example of annotating markup:

```html
<div data-focus-group data-focus-group-select="active">
<div data-focus-group="active">
<button>Home</button>
<button data-focus-group-active>About</button>
<button data-focus-active>About</button>
<button>Contact</button>
</div>

<button data-focus-skip>Delete your account</button>
```

## Options

The following attributes may be added in the markup to guide the moving of focus"
The following attributes may be added in the markup to guide the moving of focus:

- `data-focus-group`: Defines a navigation group.
- `data-focus-group-select`: Determines the initial focus when focus moves to a group.
- `data-focus-group`: Defines a navigation group and the initial focus when focus moves to a group. Default is `linear`.
- `first`: The first element in the DOM order receives focus.
- `last`: The last element in the DOM order is focused initially.
- `active`: Focuses on the element within the group marked as active.
- `linear`: Focus is determined by the spatial direction of user navigation.
- `data-focus-group-active`: Marks an element as the currently active element within a group.
- `data-focus-mute`: Skips the element and its descendants in navigation.
- `data-focus-solo`: Focuses within this element only, ignoring others in the same group.
- `memorize`: The last focused element within the group receives focus again.
- `data-focus-active`: Marks an element as the currently active element within a group.
- `data-focus-skip`: Skips the element and its descendants in navigation.
- `data-focus-trap`: Only allows elements within the annotated layer to receive focus.

Setting `window.FOCUS_SHIFT_DEBUG = true` lets the library log processing steps to the browser's console.

## Principles and Scope

- **It doesn't just work.** It would be nice if focus could automatically move to the intuitive element in each case, but this seems to require a sophisticated model of visual weight and Gestalt principles. This is out of scope for a simple library like this.
- **It should be easy to make it work.** With a little bit of annotation in the markup, one can express relationships to help the algorithm move focus in an adequate way.
- **Annotations should be logical, not spatial.** To be useful in responsive layouts, the annotations should express logical rather than spatial relationships.
- **Keep state to a minimum.** As much as possible, the library should treat each event in isolation and not maintain state representing the page layout. This may make the library less performant, but avoids complicated and error prone recomputation logic.

### What the library doesn't do, but might

- Dispatch cancelable events when descending into or out of groups
- Dispatch cancelable events before applying focus to an element
- Treat elements in open shadow DOM as focusable
- Allow defining custom selectors for focusables
- Use focus heuristics based on user agent's text direction
- Offer a JavaScript API

### What the library probably shouldn't do

- Handle keyboard events other than arrow keys

### What the library can not do

- Handle focus in iframes or closed shadow DOM

### Mechanism

## Limitations and simplifying assumptions
- **Arrow key navigation only**: The library listens only to arrow key events for navigation.
- **No iframe or ShadowDOM support**: Does not handle navigation within iframes or shadow DOM elements.
- **Viewport-filling applications without scrollbars**: Optimized for applications that fill the viewport without scrolling.
- **Exclusion of conflicting elements**: Avoids navigation to inputs like radio buttons to simplify navigation logic.
```mermaid
flowchart TB
Idle(((Idle))) == keypress ==> D_KP{Is arrow key?}
%% Terminology from https://github.com/whatwg/html/issues/897
D_KP -- Yes --> A_BC[Get top blocking element]
D_KP -- No --> Idle
A_BC --> D_AE{Contains activeElement?}
D_AE -- No --> A_SI[Select initial focus]
A_SI --> Idle
D_AE -- Yes --> Find
These limitations are intentional to keep the library simple and focused.
subgraph Find
direction TB
A_FC[Find candidates within next parent group]
A_FC --> D_CF{Candidates found?}
D_CF -- Yes --> A_SN[Stop with candidates]
D_CF -- No --> D_PB[Is parent the top blocking element?]
D_PB -- Yes --> A_NC[Stop with no candidates]
D_PB -- No --> A_FC
end
Find --> D_HC[One or more candidates?]
D_HC -- Yes --> Activate
D_HC -- No --> Idle
subgraph Activate
direction TB
D_DP[Direct projection along movement axis non-empty?] -- Yes --> A_FD[Reduce to only those candidates]
D_DP -- No --> A_CA[Continue with all candidates]
A_CA --> A_SC[Select candidate with lowest Euclidean distance]
A_FD --> A_SC
A_SC --> D_CG[Is selected candidate a group?]
D_CG -- Yes --> A_DG[Select new candidate based on group's strategy]
A_DG --> D_CG
D_CG -- No --> A_FS[Focus selected candidate]
end
Activate --> Idle
```

## Development

The library is implemented in [withered](https://en.wikipedia.org/wiki/Gunpei_Yokoi#Lateral_Thinking_with_Withered_Technology) JavaScript, so it should work directly with most browsers and a development server is not needed.

There is ample JSDoc documentation so that the TypeScript compiler may be used for typechecking in strict mode:

npm test

The code is formatted with slightly non-standard prettier:

npm run format

End-to-end tests are done using Cypress.

## Contributing

Contributions are welcome. Please fork the repository and submit a pull request with your proposed changes.

## Related Work and Inspiration

- https://github.com/bbc/lrud-spatial, a nice and simple but more spatially oriented library
- https://github.com/luke-chang/js-spatial-navigation, spatial navigation library with good functionality but JavaScript-focused and stateful configuration
- https://github.com/WICG/spatial-navigation, a possibly abandoned proposal for a Web Platform API

## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details.

(C) Copyright 2024 Dividat AG

Published under the MIT License. See [LICENSE](LICENSE) for details.
10 changes: 10 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "cypress"

export default defineConfig({
experimentalWebKitSupport: true,
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
}
}
})
Loading

0 comments on commit da00214

Please sign in to comment.