react-roving-tabindex
Advanced tools
Comparing version 2.0.0-alpha.2 to 2.0.0-beta.1
@@ -1,3 +0,9 @@ | ||
# 1.0.0 | ||
# Changelog | ||
## 2.0.0 | ||
This release is a complete rewrite to support a roving tabindex in a grid and a more flexible system for configuring keyboard navigation. It has three breaking changes compared to v1. Please see the project README file for the migration details. | ||
## 1.0.0 | ||
This release has no breaking changes compared to v0.9.0. I just thought it was time to promote the package to v1. The changes are to do with the build process and Storybook; no source files have been altered. | ||
@@ -11,3 +17,3 @@ | ||
# 0.9.0 | ||
## 0.9.0 | ||
@@ -17,3 +23,3 @@ - Updated dev dependencies. | ||
# 0.8.0 | ||
## 0.8.0 | ||
@@ -24,3 +30,3 @@ - Updated dev dependencies. | ||
# 0.7.2 | ||
## 0.7.2 | ||
@@ -27,0 +33,0 @@ - Updated dev dependencies. |
@@ -54,4 +54,4 @@ import React, { createContext, useReducer, useEffect, useMemo, useRef, useContext, useCallback, useLayoutEffect } from 'react'; | ||
Navigation["NEXT"] = "NEXT"; | ||
Navigation["FIRST"] = "FIRST"; | ||
Navigation["LAST"] = "LAST"; | ||
Navigation["VERY_FIRST"] = "VERY_FIRST"; | ||
Navigation["VERY_LAST"] = "VERY_LAST"; | ||
Navigation["PREVIOUS_ROW"] = "PREVIOUS_ROW"; | ||
@@ -78,6 +78,6 @@ Navigation["NEXT_ROW"] = "NEXT_ROW"; | ||
_a[Key.ARROW_DOWN] = Navigation.NEXT, | ||
_a[Key.HOME] = Navigation.FIRST, | ||
_a[Key.HOME_WITH_CTRL] = Navigation.FIRST, | ||
_a[Key.END] = Navigation.LAST, | ||
_a[Key.END_WITH_CTRL] = Navigation.LAST, | ||
_a[Key.HOME] = Navigation.VERY_FIRST, | ||
_a[Key.HOME_WITH_CTRL] = Navigation.VERY_FIRST, | ||
_a[Key.END] = Navigation.VERY_LAST, | ||
_a[Key.END_WITH_CTRL] = Navigation.VERY_LAST, | ||
_a); | ||
@@ -197,3 +197,3 @@ var DOCUMENT_POSITION_PRECEDING = 2; | ||
break; | ||
case Navigation.FIRST: | ||
case Navigation.VERY_FIRST: | ||
{ | ||
@@ -208,3 +208,3 @@ for (var i = 0; i < state.tabStops.length; ++i) { | ||
break; | ||
case Navigation.LAST: | ||
case Navigation.VERY_LAST: | ||
{ | ||
@@ -211,0 +211,0 @@ for (var i = state.tabStops.length - 1; i >= 0; --i) { |
@@ -60,4 +60,4 @@ 'use strict'; | ||
Navigation["NEXT"] = "NEXT"; | ||
Navigation["FIRST"] = "FIRST"; | ||
Navigation["LAST"] = "LAST"; | ||
Navigation["VERY_FIRST"] = "VERY_FIRST"; | ||
Navigation["VERY_LAST"] = "VERY_LAST"; | ||
Navigation["PREVIOUS_ROW"] = "PREVIOUS_ROW"; | ||
@@ -83,6 +83,6 @@ Navigation["NEXT_ROW"] = "NEXT_ROW"; | ||
_a[exports.Key.ARROW_DOWN] = exports.Navigation.NEXT, | ||
_a[exports.Key.HOME] = exports.Navigation.FIRST, | ||
_a[exports.Key.HOME_WITH_CTRL] = exports.Navigation.FIRST, | ||
_a[exports.Key.END] = exports.Navigation.LAST, | ||
_a[exports.Key.END_WITH_CTRL] = exports.Navigation.LAST, | ||
_a[exports.Key.HOME] = exports.Navigation.VERY_FIRST, | ||
_a[exports.Key.HOME_WITH_CTRL] = exports.Navigation.VERY_FIRST, | ||
_a[exports.Key.END] = exports.Navigation.VERY_LAST, | ||
_a[exports.Key.END_WITH_CTRL] = exports.Navigation.VERY_LAST, | ||
_a); | ||
@@ -202,3 +202,3 @@ var DOCUMENT_POSITION_PRECEDING = 2; | ||
break; | ||
case exports.Navigation.FIRST: | ||
case exports.Navigation.VERY_FIRST: | ||
{ | ||
@@ -213,3 +213,3 @@ for (var i = 0; i < state.tabStops.length; ++i) { | ||
break; | ||
case exports.Navigation.LAST: | ||
case exports.Navigation.VERY_LAST: | ||
{ | ||
@@ -216,0 +216,0 @@ for (var i = state.tabStops.length - 1; i >= 0; --i) { |
@@ -23,4 +23,4 @@ /// <reference types="react" /> | ||
NEXT = "NEXT", | ||
FIRST = "FIRST", | ||
LAST = "LAST", | ||
VERY_FIRST = "VERY_FIRST", | ||
VERY_LAST = "VERY_LAST", | ||
PREVIOUS_ROW = "PREVIOUS_ROW", | ||
@@ -36,6 +36,6 @@ NEXT_ROW = "NEXT_ROW", | ||
[Key.ARROW_DOWN]?: Navigation.NEXT | Navigation.NEXT_ROW | null; | ||
[Key.HOME]?: Navigation.FIRST | Navigation.FIRST_IN_ROW | null; | ||
[Key.END]?: Navigation.LAST | Navigation.LAST_IN_ROW | null; | ||
[Key.HOME_WITH_CTRL]?: Navigation.FIRST | null; | ||
[Key.END_WITH_CTRL]?: Navigation.LAST | null; | ||
[Key.HOME]?: Navigation.VERY_FIRST | Navigation.FIRST_IN_ROW | null; | ||
[Key.END]?: Navigation.VERY_LAST | Navigation.LAST_IN_ROW | null; | ||
[Key.HOME_WITH_CTRL]?: Navigation.VERY_FIRST | null; | ||
[Key.END_WITH_CTRL]?: Navigation.VERY_LAST | null; | ||
}; | ||
@@ -42,0 +42,0 @@ export declare type TabStop = Readonly<{ |
{ | ||
"name": "react-roving-tabindex", | ||
"version": "2.0.0-alpha.2", | ||
"version": "2.0.0-beta.1", | ||
"description": "React implementation of a roving tabindex, now with grid support", | ||
@@ -5,0 +5,0 @@ "author": "stevejay", |
200
README.md
# react-roving-tabindex | ||
> React Hooks implementation of a roving tabindex. See the storybook [here](https://stevejay.github.io/react-roving-tabindex/) to try it out. | ||
> React Hooks implementation of a roving tabindex, now with grid support. See the storybook [here](https://stevejay.github.io/react-roving-tabindex/) to try it out. | ||
@@ -9,10 +9,12 @@ [![bundlephobia](https://img.shields.io/bundlephobia/minzip/react-roving-tabindex)](https://bundlephobia.com/result?p=react-roving-tabindex) [![NPM](https://img.shields.io/npm/v/react-roving-tabindex.svg)](https://www.npmjs.com/package/react-roving-tabindex) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) [![CircleCI](https://img.shields.io/circleci/project/github/stevejay/react-roving-tabindex/master.svg)](https://circleci.com/gh/stevejay/react-roving-tabindex/tree/master) | ||
The roving tabindex is an accessibility pattern for a grouped set of inputs. It assists people who are using their keyboard to navigate your Web site. All inputs in a group get treated as a single tab stop, which speeds up keyboard navigation. The last focused input in a group is also remembered, so that it can receive focus again when the user tabs back to the group. | ||
The roving tabindex is an accessibility pattern for a grouped set of inputs. It assists people who are using their keyboard to navigate your Web site. All inputs in a group get treated as a single tab stop, which speeds up keyboard navigation. The last focused input in the group is also remembered, so that it can receive focus again when the user tabs back into the group. | ||
When in the group, the left and right (or up and down) arrow keys move between the inputs. The Home and End keys (Fn+LeftArrow and Fn+RightArrow on macOS) move to the group's first and last inputs respectively. | ||
When in the group, the ArrowLeft and ArrowRight (or ArrowUp and ArrowDown) keys move focus between the inputs. The Home and End keys (Fn+LeftArrow and Fn+RightArrow on macOS) move focus to the group's first and last inputs respectively. | ||
More information about the roving tabindex pattern is available [here](https://www.stefanjudis.com/today-i-learned/roving-tabindex/) and [here](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Managing_focus_inside_groups). | ||
### Implementation Considerations | ||
This pattern can also be used for a grid of items, as in [this calendar example](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Grid_Role#Calendar_example) on the MDN website. The containing element for the grid should be given the `grid` role, and each grid item should given the `gridcell` role. ArrowLeft and ArrowRight are used to move focus between items in a row, while the ArrowUp and ArrowDown keys move between the rows. The Home and End keys (Fn+LeftArrow and Fn+RightArrow on macOS) move focus to a row's first and last items respectively. If the Control key is held while pressing the Home and End keys then focus is moved respectively to the very first and very last item in the grid. | ||
### Implementation considerations | ||
There are two main architectural choices to be made: | ||
@@ -29,3 +31,3 @@ | ||
This package has been written using the React Hooks API, so it is only usable with React version 16.8 onwards. | ||
This package has been written using the React Hooks API, so it is only usable with React version 16.8 onwards. It has peer dependencies of `react` and `react-dom`. | ||
@@ -40,8 +42,10 @@ ## Installation | ||
If you need to support IE 11 then you need to also install a polyfill for `Array.prototype.findIndex`. One option is the polyfill on the [MDN website](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex). | ||
If you need to support IE then you need to also install polyfills for `Array.prototype.findIndex` and `Map`. If you are using React with IE then [you already need to use a `Map` polyfill](https://reactjs.org/docs/javascript-environment-requirements.html). If you use a global polyfill like [core-js](https://github.com/zloirock/core-js) or [babel-polyfill](https://babeljs.io/docs/usage/polyfill/) then it should also include a `findIndex` polyfill. | ||
## Usage | ||
There is a storybook for this package [here](https://stevejay.github.io/react-roving-tabindex/). | ||
There is a storybook for this package [here](https://stevejay.github.io/react-roving-tabindex/), with both basic and grid usage examples. | ||
### Basic usage | ||
```tsx | ||
@@ -77,6 +81,6 @@ import React from "react"; | ||
ref={ref} | ||
tabIndex={tabIndex} // tabIndex must be applied here. | ||
tabIndex={tabIndex} // tabIndex must be applied here | ||
disabled={disabled} | ||
onKeyDown={handleKeyDown} | ||
onClick={handleClick} | ||
onKeyDown={handleKeyDown} // handler applied here | ||
onClick={handleClick} // handler applied here | ||
> | ||
@@ -88,3 +92,3 @@ {children} | ||
const App = () => ( | ||
const SomeComponent = () => ( | ||
// Wrap each group in a RovingTabIndexProvider. | ||
@@ -102,5 +106,5 @@ <RovingTabIndexProvider> | ||
### Custom ID | ||
#### Optional ID parameter | ||
You can optionally pass a custom ID to the `useRovingTabIndex` hook as the third argument: | ||
The `useRovingTabIndex` hook has an optional third argument that is an options object. One use for it is to pass a custom ID: | ||
@@ -111,24 +115,174 @@ ```jsx | ||
disabled, | ||
"custom-id-1" // A custom ID. | ||
{ id: "custom-id-1" } // A custom ID. | ||
); | ||
``` | ||
This is useful if you need to support server-side rendering. The value initially passed will be used for the lifetime of the containing component. You cannot dynamically change this ID. | ||
This is required if you need to support server-side rendering (SSR). Note that the value initially passed will be used for the lifetime of the containing component; you cannot dynamically update this ID. | ||
### Navigation | ||
Note that it is fine to create a new object for this third argument each time the containing component is rendered; it will not trigger a re-render. | ||
By default the left and right arrow keys are used to move between inputs. You can change this using the `direction` prop on the provider: | ||
#### Navigation | ||
```jsx | ||
<RovingTabIndexProvider direction="horizontal|vertical|both" /> | ||
The default navigation configuration is the following: | ||
| Key | Resulting navigation | | ||
| ---------- | -------------------- | | ||
| ArrowLeft | Previous tab stop | | ||
| ArrowRight | Next tab stop | | ||
| ArrowUp | Previous tab stop | | ||
| ArrowDown | Next tab stop | | ||
| Home | Very first tab stop | | ||
| Home+Ctrl | Very first tab stop | | ||
| End | Very last tab stop | | ||
| End+Ctrl | Very last tab stop | | ||
The above configuration is included with this package as the following default key configuration object: | ||
```ts | ||
export const DEFAULT_KEY_CONFIG: KeyConfig = { | ||
[Key.ARROW_LEFT]: Navigation.PREVIOUS, | ||
[Key.ARROW_RIGHT]: Navigation.NEXT, | ||
[Key.ARROW_UP]: Navigation.PREVIOUS, | ||
[Key.ARROW_DOWN]: Navigation.NEXT, | ||
[Key.HOME]: Navigation.VERY_FIRST, | ||
[Key.HOME_WITH_CTRL]: Navigation.VERY_FIRST, | ||
[Key.END]: Navigation.VERY_LAST, | ||
[Key.END_WITH_CTRL]: Navigation.VERY_LAST | ||
}; | ||
``` | ||
The `vertical` option requires the up and down arrows for navigation. The `both` option is a mix of the other two options. | ||
This object is used automatically by default. If this is not suitable for your use case then you can pass your own key configuration object to the `RovingTabIndexProvider`. For example, you might want the ArrowUp and ArrowDown keys to have no effect: | ||
```ts | ||
import { DEFAULT_KEY_CONFIG, Key } from "react-roving-tabindex"; | ||
export const MY_CONFIG = { | ||
...DEFAULT_KEY_CONFIG, | ||
[Key.ARROW_UP]: undefined, // or null | ||
[Key.ARROW_DOWN]: undefined // or null | ||
}; | ||
const SomeComponent = () => ( | ||
<RovingTabIndexProvider keyConfig={MY_CONFIG}> | ||
{/* Whatever here */} | ||
</RovingTabIndexProvider> | ||
); | ||
``` | ||
Note that your custom key configuration object should be stable; please do not recreate it each time the component containing the `RovingTabIndexProvider` is invoked as this will trigger a re-render. You can however pass a new configuration whenever you need to dynamically change the key configuration behaviour. | ||
Note also that the TypeScript typings for the key configuration object somewhat constrain the possible navigation options for each key to the most logical choices given the established a11y conventions for a roving tabindex. | ||
### Grid usage | ||
This package supports a roving tabindex in a grid of elements. For this to work you need to do two things in addition to following the instructions in the previous section. | ||
Firstly you need to create and pass a custom key configuration object to the `RovingTabIndexProvider` component. The exact configuration will depend on your needs but it will likely be something like the following: | ||
```ts | ||
// Create it... | ||
const GRID_KEY_CONFIG: KeyConfig = { | ||
[Key.ARROW_LEFT]: Navigation.PREVIOUS, | ||
[Key.ARROW_RIGHT]: Navigation.NEXT, | ||
[Key.ARROW_UP]: Navigation.PREVIOUS_ROW, | ||
[Key.ARROW_DOWN]: Navigation.NEXT_ROW, | ||
[Key.HOME]: Navigation.FIRST_IN_ROW, | ||
[Key.HOME_WITH_CTRL]: Navigation.VERY_FIRST, | ||
[Key.END]: Navigation.LAST_IN_ROW, | ||
[Key.END_WITH_CTRL]: Navigation.VERY_LAST | ||
}; | ||
// Then use it... | ||
const SomeComponent = () => ( | ||
<RovingTabIndexProvider keyConfig={GRID_KEY_CONFIG}> | ||
{/* Whatever here */} | ||
</RovingTabIndexProvider> | ||
); | ||
``` | ||
Secondly, for each usage of the `useRovingTabIndex` hook you need to pass a third argument of a `rowIndex` value in an options object: | ||
```ts | ||
const [tabIndex, focused, handleKeyDown, handleClick] = useRovingTabIndex( | ||
ref, | ||
disabled, | ||
{ rowIndex: 0 } // 0, 1, 2, ... | ||
); | ||
``` | ||
The `rowIndex` value should be the zero-based row index for the containing component in the grid it is in. Thus all items that represent the first row of grid items should have `{ rowIndex: 0 }` passed to the hook, the second row `{ rowIndex: 1 }`, and so on. If the shape of the grid can change dynamically then it is fine to update the rowIndex value. For example, the grid might initially has four items per row but be dynamically updated to have three items per row. | ||
Note that it is fine to create a new object for this third argument each time the containing component is rendered; it will not trigger a re-render. Also, if required you can combine the `rowIndex` with a custom `id` in the same options object. | ||
## Upgrading | ||
### From version 1 | ||
### From version 1 to version 2 | ||
This package no longer includes a ponyfill for `Array.prototype.findIndex`. If you need to support IE 11 then you now need to install a polyfill for that method. One option is the polyfill on the [MDN website](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex). | ||
There are three breaking changes that might require updating your usages of this library. | ||
Firstly, this package no longer includes a ponyfill for `Array.prototype.findIndex` and also now uses the `Map` class. If you need to support IE then you will need to install a polyfill for both, although if you already support IE then you are almost certainly using a suitable global polyfill. Please see the Installation section earlier in this file for further guidance. | ||
Secondly, the `direction` property of the `RovingTabIndexProvider` has been removed. Instead the behaviour of the roving tabindex for the possible key presses (ArrowLeft, ArrowRight, ArrowUp, ArrowDown, Home, End, Home+Ctrl and End+Ctrl) is now configurable. Rather than specifying a direction, you can now pass a key configuration object to the `RovingTabIndexProvider` component: | ||
```ts | ||
const SomeComponent = () => ( | ||
<RovingTabIndexProvider keyConfig={YOUR_CONFIG}> | ||
{/* Whatever here */} | ||
</RovingTabIndexProvider> | ||
); | ||
``` | ||
If you do not pass your own key configuration object then the following default one is automatically used: | ||
```ts | ||
export const DEFAULT_KEY_CONFIG: KeyConfig = { | ||
[Key.ARROW_LEFT]: Navigation.PREVIOUS, | ||
[Key.ARROW_RIGHT]: Navigation.NEXT, | ||
[Key.ARROW_UP]: Navigation.PREVIOUS, | ||
[Key.ARROW_DOWN]: Navigation.NEXT, | ||
[Key.HOME]: Navigation.VERY_FIRST, | ||
[Key.HOME_WITH_CTRL]: Navigation.VERY_FIRST, | ||
[Key.END]: Navigation.VERY_LAST, | ||
[Key.END_WITH_CTRL]: Navigation.VERY_LAST | ||
}; | ||
``` | ||
This configuration specifies the mapping between key press and focus movement, and it should be quite self-explanatory. The default mapping is likely what you want if you are migrating from version 1. If you do want to make changes, such as not supporting the ArrowLeft and ArrowRight keys then you can create and use a custom key configuration: | ||
```ts | ||
import { DEFAULT_KEY_CONFIG, Key } from "react-roving-tabindex"; | ||
export const CUSTOM_KEY_CONFIG = { | ||
...DEFAULT_KEY_CONFIG, | ||
[Key.ARROW_LEFT]: undefined, // or null | ||
[Key.ARROW_RIGHT]: undefined // or null | ||
}; | ||
const SomeComponent = () => ( | ||
<RovingTabIndexProvider keyConfig={CUSTOM_KEY_CONFIG}> | ||
{/* Whatever here */} | ||
</RovingTabIndexProvider> | ||
); | ||
``` | ||
The third and final breaking change is that the optional third argument to the `useRovingTabIndex` hook has changed type from a string ID to an object. This change only affects you if you have needed to pass your own IDs to the hook, in particular if you support server-side rendering (SSR). So if you have used the following... | ||
```ts | ||
const [...] = useRovingTabIndex(ref, true, id); | ||
// ^^ | ||
``` | ||
... then you will need to instead pass the ID in an object: | ||
```ts | ||
const [...] = useRovingTabIndex(ref, true, { id }); | ||
// ^^^^^^ | ||
``` | ||
Note that it is fine to create a new object for that third argument each time the containing component is rendered; that will not by itself trigger a re-render. As a reminder, the assigned ID will be captured on mounting of the containing component and cannot be changed during that component's lifetime. | ||
### From version 0.x to version 1 | ||
The version 1 release has no breaking changes compared to v0.9.0. The version bump was because the package had matured. | ||
## License | ||
@@ -140,3 +294,3 @@ | ||
If you have build errors when building Storybook locally, you are likely using Node v13. Please use either Node v14+ or Node v12. | ||
If you have build errors when building the Storybook locally, you are likely using Node v13. Please use either Node v14+ or Node v12. | ||
@@ -143,0 +297,0 @@ ### Issues |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
175736
292