-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from knuton/rewrite
Add initial implementation of library
- Loading branch information
Showing
21 changed files
with
3,711 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
}) |
Oops, something went wrong.