react-roving-tabindex
Advanced tools
Comparing version 2.0.0-beta.6 to 2.0.0-beta.7
@@ -5,3 +5,3 @@ # Changelog | ||
This release is a complete rewrite to support a roving tabindex in a grid and a more flexible system for configuring keyboard navigation. There are breaking changes compared to v1. Please see the project README file for the migration details. | ||
This release is a complete rewrite to support a roving tabindex in a grid. There are breaking changes compared to v1. Please see the project README file for the migration details. | ||
@@ -8,0 +8,0 @@ Other notable changes: |
@@ -1,4 +0,4 @@ | ||
export { Provider as RovingTabIndexProvider, DEFAULT_KEY_CONFIG } from "./Provider"; | ||
export { Provider as RovingTabIndexProvider } from "./Provider"; | ||
export { useRovingTabIndex } from "./use-roving-tabindex"; | ||
export { useFocusEffect } from "./use-focus-effect"; | ||
export * from "./types"; |
@@ -39,13 +39,2 @@ import React, { createContext, useReducer, useEffect, useMemo, useRef, useContext, useCallback } from 'react'; | ||
})(EventKey || (EventKey = {})); | ||
var Key; | ||
(function (Key) { | ||
Key["ARROW_LEFT"] = "ArrowLeft"; | ||
Key["ARROW_RIGHT"] = "ArrowRight"; | ||
Key["ARROW_UP"] = "ArrowUp"; | ||
Key["ARROW_DOWN"] = "ArrowDown"; | ||
Key["HOME"] = "Home"; | ||
Key["END"] = "End"; | ||
Key["HOME_WITH_CTRL"] = "HomeWithCtrl"; | ||
Key["END_WITH_CTRL"] = "EndWithCtrl"; | ||
})(Key || (Key = {})); | ||
var Navigation; | ||
@@ -69,16 +58,5 @@ (function (Navigation) { | ||
ActionType["TAB_STOP_UPDATED"] = "TAB_STOP_UPDATED"; | ||
ActionType["KEY_CONFIG_UPDATED"] = "KEY_CONFIG_UPDATED"; | ||
ActionType["DIRECTION_UPDATED"] = "DIRECTION_UPDATED"; | ||
})(ActionType || (ActionType = {})); | ||
var _a; | ||
var DEFAULT_KEY_CONFIG = (_a = {}, | ||
_a[Key.ARROW_LEFT] = Navigation.PREVIOUS, | ||
_a[Key.ARROW_RIGHT] = Navigation.NEXT, | ||
_a[Key.ARROW_UP] = Navigation.PREVIOUS, | ||
_a[Key.ARROW_DOWN] = Navigation.NEXT, | ||
_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); | ||
var DOCUMENT_POSITION_FOLLOWING = 4; | ||
@@ -162,6 +140,2 @@ // Note: The `allowFocusing` state property is required | ||
var _b = action.payload, id_3 = _b.id, key = _b.key, ctrlKey = _b.ctrlKey; | ||
var navigation = getNavigationValue(key, ctrlKey, state.keyConfig); | ||
if (!navigation) { | ||
return state; | ||
} | ||
var index = state.tabStops.findIndex(function (tabStop) { return tabStop.id === id_3; }); | ||
@@ -177,2 +151,6 @@ if (index === -1) { | ||
var isGrid = currentTabStop.rowIndex !== null; | ||
var navigation = getNavigationValue(key, ctrlKey, isGrid, state.direction); | ||
if (!navigation) { | ||
return state; | ||
} | ||
switch (navigation) { | ||
@@ -331,5 +309,5 @@ case Navigation.NEXT: | ||
} | ||
case ActionType.KEY_CONFIG_UPDATED: { | ||
var keyConfig = action.payload.keyConfig; | ||
return keyConfig === state.keyConfig ? state : __assign(__assign({}, state), { keyConfig: keyConfig }); | ||
case ActionType.DIRECTION_UPDATED: { | ||
var direction = action.payload.direction; | ||
return direction === state.direction ? state : __assign(__assign({}, state), { direction: direction }); | ||
} | ||
@@ -360,19 +338,39 @@ default: | ||
// Translates the user key down event info into a navigation instruction. | ||
function getNavigationValue(key, ctrlKey, keyConfig) { | ||
var translatedKey = null; | ||
function getNavigationValue(key, ctrlKey, isGrid, direction) { | ||
switch (key) { | ||
case Key.ARROW_LEFT: | ||
case Key.ARROW_RIGHT: | ||
case Key.ARROW_UP: | ||
case Key.ARROW_DOWN: | ||
translatedKey = key; | ||
break; | ||
case Key.HOME: | ||
translatedKey = ctrlKey ? Key.HOME_WITH_CTRL : Key.HOME; | ||
break; | ||
case Key.END: | ||
translatedKey = ctrlKey ? Key.END_WITH_CTRL : Key.END; | ||
break; | ||
case EventKey.ArrowLeft: | ||
return isGrid || direction === "horizontal" || direction === "both" | ||
? Navigation.PREVIOUS | ||
: null; | ||
case EventKey.ArrowRight: | ||
return isGrid || direction === "horizontal" || direction === "both" | ||
? Navigation.NEXT | ||
: null; | ||
case EventKey.ArrowUp: | ||
if (isGrid) { | ||
return Navigation.PREVIOUS_ROW; | ||
} | ||
else { | ||
return direction === "vertical" || direction === "both" | ||
? Navigation.PREVIOUS | ||
: null; | ||
} | ||
case EventKey.ArrowDown: | ||
if (isGrid) { | ||
return Navigation.NEXT_ROW; | ||
} | ||
else { | ||
return direction === "vertical" || direction === "both" | ||
? Navigation.NEXT | ||
: null; | ||
} | ||
case EventKey.Home: | ||
return !isGrid || ctrlKey | ||
? Navigation.VERY_FIRST | ||
: Navigation.FIRST_IN_ROW; | ||
case EventKey.End: | ||
return !isGrid || ctrlKey ? Navigation.VERY_LAST : Navigation.LAST_IN_ROW; | ||
default: | ||
return null; | ||
} | ||
return translatedKey === null ? null : keyConfig[translatedKey] || null; | ||
} | ||
@@ -399,3 +397,3 @@ // Creates the new state for a tab stop when it becomes the selected one. | ||
tabStops: [], | ||
keyConfig: DEFAULT_KEY_CONFIG, | ||
direction: "horizontal", | ||
rowStartMap: null | ||
@@ -412,17 +410,23 @@ }; | ||
* include the DOM elements to rove between using the tab key. | ||
* @param {keyConfig} keyConfig An optional key navigation configuration | ||
* object that specifies exactly how the roving tabindex should move | ||
* when particular keys are pressed by the user. A default config | ||
* is used when none is supplied. If you pass a config object then | ||
* it can be changed throughout the lifetime of the containing component. | ||
* But it is best if its identity changes only if the configuration | ||
* values themselves change. | ||
* @param {KeyDirection} direction An optional direction value | ||
* that only applies when the roving tabindex is not being | ||
* used within a grid. This value specifies the arrow key behaviour. | ||
* When set to 'horizontal' then only the ArrowLeft and ArrowRight | ||
* keys move to the previous and next tab stop respectively. | ||
* When set to 'vertical' then only the ArrowUp and ArrowDown keys | ||
* move to the previous and next tab stop respectively. When set | ||
* to 'both' then both the ArrowLeft and ArrowUp keys can be used | ||
* to move to the previous tab stop, and both the ArrowRight | ||
* and ArrowDown keys can be used to move to the next tab stop. | ||
* If you do not pass an explicit value then the 'horizontal' | ||
* behaviour applies. You can change this direction value | ||
* at any time. | ||
*/ | ||
var Provider = function (_a) { | ||
var children = _a.children, _b = _a.keyConfig, keyConfig = _b === void 0 ? DEFAULT_KEY_CONFIG : _b; | ||
var _c = useReducer(reducer, __assign(__assign({}, INITIAL_STATE), { keyConfig: keyConfig })), state = _c[0], dispatch = _c[1]; | ||
// Update the keyConfig whenever it changes: | ||
var children = _a.children, _b = _a.direction, direction = _b === void 0 ? "horizontal" : _b; | ||
var _c = useReducer(reducer, __assign(__assign({}, INITIAL_STATE), { direction: direction })), state = _c[0], dispatch = _c[1]; | ||
// Update the direction whenever it changes: | ||
useEffect(function () { | ||
dispatch({ type: ActionType.KEY_CONFIG_UPDATED, payload: { keyConfig: keyConfig } }); | ||
}, [keyConfig]); | ||
dispatch({ type: ActionType.DIRECTION_UPDATED, payload: { direction: direction } }); | ||
}, [direction]); | ||
// Create a cached object to use as the context value: | ||
@@ -566,3 +570,3 @@ var context = useMemo(function () { return ({ state: state, dispatch: dispatch }); }, [state]); | ||
export { ActionType, DEFAULT_KEY_CONFIG, EventKey, Key, Navigation, Provider as RovingTabIndexProvider, useFocusEffect, useRovingTabIndex }; | ||
export { ActionType, EventKey, Navigation, Provider as RovingTabIndexProvider, useFocusEffect, useRovingTabIndex }; | ||
//# sourceMappingURL=index.es.js.map |
@@ -47,12 +47,2 @@ 'use strict'; | ||
})(exports.EventKey || (exports.EventKey = {})); | ||
(function (Key) { | ||
Key["ARROW_LEFT"] = "ArrowLeft"; | ||
Key["ARROW_RIGHT"] = "ArrowRight"; | ||
Key["ARROW_UP"] = "ArrowUp"; | ||
Key["ARROW_DOWN"] = "ArrowDown"; | ||
Key["HOME"] = "Home"; | ||
Key["END"] = "End"; | ||
Key["HOME_WITH_CTRL"] = "HomeWithCtrl"; | ||
Key["END_WITH_CTRL"] = "EndWithCtrl"; | ||
})(exports.Key || (exports.Key = {})); | ||
(function (Navigation) { | ||
@@ -74,16 +64,5 @@ Navigation["PREVIOUS"] = "PREVIOUS"; | ||
ActionType["TAB_STOP_UPDATED"] = "TAB_STOP_UPDATED"; | ||
ActionType["KEY_CONFIG_UPDATED"] = "KEY_CONFIG_UPDATED"; | ||
ActionType["DIRECTION_UPDATED"] = "DIRECTION_UPDATED"; | ||
})(exports.ActionType || (exports.ActionType = {})); | ||
var _a; | ||
var DEFAULT_KEY_CONFIG = (_a = {}, | ||
_a[exports.Key.ARROW_LEFT] = exports.Navigation.PREVIOUS, | ||
_a[exports.Key.ARROW_RIGHT] = exports.Navigation.NEXT, | ||
_a[exports.Key.ARROW_UP] = exports.Navigation.PREVIOUS, | ||
_a[exports.Key.ARROW_DOWN] = exports.Navigation.NEXT, | ||
_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); | ||
var DOCUMENT_POSITION_FOLLOWING = 4; | ||
@@ -167,6 +146,2 @@ // Note: The `allowFocusing` state property is required | ||
var _b = action.payload, id_3 = _b.id, key = _b.key, ctrlKey = _b.ctrlKey; | ||
var navigation = getNavigationValue(key, ctrlKey, state.keyConfig); | ||
if (!navigation) { | ||
return state; | ||
} | ||
var index = state.tabStops.findIndex(function (tabStop) { return tabStop.id === id_3; }); | ||
@@ -182,2 +157,6 @@ if (index === -1) { | ||
var isGrid = currentTabStop.rowIndex !== null; | ||
var navigation = getNavigationValue(key, ctrlKey, isGrid, state.direction); | ||
if (!navigation) { | ||
return state; | ||
} | ||
switch (navigation) { | ||
@@ -336,5 +315,5 @@ case exports.Navigation.NEXT: | ||
} | ||
case exports.ActionType.KEY_CONFIG_UPDATED: { | ||
var keyConfig = action.payload.keyConfig; | ||
return keyConfig === state.keyConfig ? state : __assign(__assign({}, state), { keyConfig: keyConfig }); | ||
case exports.ActionType.DIRECTION_UPDATED: { | ||
var direction = action.payload.direction; | ||
return direction === state.direction ? state : __assign(__assign({}, state), { direction: direction }); | ||
} | ||
@@ -365,19 +344,39 @@ default: | ||
// Translates the user key down event info into a navigation instruction. | ||
function getNavigationValue(key, ctrlKey, keyConfig) { | ||
var translatedKey = null; | ||
function getNavigationValue(key, ctrlKey, isGrid, direction) { | ||
switch (key) { | ||
case exports.Key.ARROW_LEFT: | ||
case exports.Key.ARROW_RIGHT: | ||
case exports.Key.ARROW_UP: | ||
case exports.Key.ARROW_DOWN: | ||
translatedKey = key; | ||
break; | ||
case exports.Key.HOME: | ||
translatedKey = ctrlKey ? exports.Key.HOME_WITH_CTRL : exports.Key.HOME; | ||
break; | ||
case exports.Key.END: | ||
translatedKey = ctrlKey ? exports.Key.END_WITH_CTRL : exports.Key.END; | ||
break; | ||
case exports.EventKey.ArrowLeft: | ||
return isGrid || direction === "horizontal" || direction === "both" | ||
? exports.Navigation.PREVIOUS | ||
: null; | ||
case exports.EventKey.ArrowRight: | ||
return isGrid || direction === "horizontal" || direction === "both" | ||
? exports.Navigation.NEXT | ||
: null; | ||
case exports.EventKey.ArrowUp: | ||
if (isGrid) { | ||
return exports.Navigation.PREVIOUS_ROW; | ||
} | ||
else { | ||
return direction === "vertical" || direction === "both" | ||
? exports.Navigation.PREVIOUS | ||
: null; | ||
} | ||
case exports.EventKey.ArrowDown: | ||
if (isGrid) { | ||
return exports.Navigation.NEXT_ROW; | ||
} | ||
else { | ||
return direction === "vertical" || direction === "both" | ||
? exports.Navigation.NEXT | ||
: null; | ||
} | ||
case exports.EventKey.Home: | ||
return !isGrid || ctrlKey | ||
? exports.Navigation.VERY_FIRST | ||
: exports.Navigation.FIRST_IN_ROW; | ||
case exports.EventKey.End: | ||
return !isGrid || ctrlKey ? exports.Navigation.VERY_LAST : exports.Navigation.LAST_IN_ROW; | ||
default: | ||
return null; | ||
} | ||
return translatedKey === null ? null : keyConfig[translatedKey] || null; | ||
} | ||
@@ -404,3 +403,3 @@ // Creates the new state for a tab stop when it becomes the selected one. | ||
tabStops: [], | ||
keyConfig: DEFAULT_KEY_CONFIG, | ||
direction: "horizontal", | ||
rowStartMap: null | ||
@@ -417,17 +416,23 @@ }; | ||
* include the DOM elements to rove between using the tab key. | ||
* @param {keyConfig} keyConfig An optional key navigation configuration | ||
* object that specifies exactly how the roving tabindex should move | ||
* when particular keys are pressed by the user. A default config | ||
* is used when none is supplied. If you pass a config object then | ||
* it can be changed throughout the lifetime of the containing component. | ||
* But it is best if its identity changes only if the configuration | ||
* values themselves change. | ||
* @param {KeyDirection} direction An optional direction value | ||
* that only applies when the roving tabindex is not being | ||
* used within a grid. This value specifies the arrow key behaviour. | ||
* When set to 'horizontal' then only the ArrowLeft and ArrowRight | ||
* keys move to the previous and next tab stop respectively. | ||
* When set to 'vertical' then only the ArrowUp and ArrowDown keys | ||
* move to the previous and next tab stop respectively. When set | ||
* to 'both' then both the ArrowLeft and ArrowUp keys can be used | ||
* to move to the previous tab stop, and both the ArrowRight | ||
* and ArrowDown keys can be used to move to the next tab stop. | ||
* If you do not pass an explicit value then the 'horizontal' | ||
* behaviour applies. You can change this direction value | ||
* at any time. | ||
*/ | ||
var Provider = function (_a) { | ||
var children = _a.children, _b = _a.keyConfig, keyConfig = _b === void 0 ? DEFAULT_KEY_CONFIG : _b; | ||
var _c = React.useReducer(reducer, __assign(__assign({}, INITIAL_STATE), { keyConfig: keyConfig })), state = _c[0], dispatch = _c[1]; | ||
// Update the keyConfig whenever it changes: | ||
var children = _a.children, _b = _a.direction, direction = _b === void 0 ? "horizontal" : _b; | ||
var _c = React.useReducer(reducer, __assign(__assign({}, INITIAL_STATE), { direction: direction })), state = _c[0], dispatch = _c[1]; | ||
// Update the direction whenever it changes: | ||
React.useEffect(function () { | ||
dispatch({ type: exports.ActionType.KEY_CONFIG_UPDATED, payload: { keyConfig: keyConfig } }); | ||
}, [keyConfig]); | ||
dispatch({ type: exports.ActionType.DIRECTION_UPDATED, payload: { direction: direction } }); | ||
}, [direction]); | ||
// Create a cached object to use as the context value: | ||
@@ -571,3 +576,2 @@ var context = React.useMemo(function () { return ({ state: state, dispatch: dispatch }); }, [state]); | ||
exports.DEFAULT_KEY_CONFIG = DEFAULT_KEY_CONFIG; | ||
exports.RovingTabIndexProvider = Provider; | ||
@@ -574,0 +578,0 @@ exports.useFocusEffect = useFocusEffect; |
import React, { ReactElement, ReactNode } from "react"; | ||
import { Action, KeyConfig, RowStartMap, State } from "./types"; | ||
export declare const DEFAULT_KEY_CONFIG: KeyConfig; | ||
import { Action, KeyDirection, RowStartMap, State } from "./types"; | ||
export declare function reducer(state: State, action: Action): State; | ||
@@ -15,3 +14,3 @@ export declare const RovingTabIndexContext: React.Context<Readonly<{ | ||
}>[]; | ||
keyConfig: KeyConfig; | ||
direction: KeyDirection; | ||
rowStartMap: RowStartMap | null; | ||
@@ -25,13 +24,19 @@ }>; | ||
* include the DOM elements to rove between using the tab key. | ||
* @param {keyConfig} keyConfig An optional key navigation configuration | ||
* object that specifies exactly how the roving tabindex should move | ||
* when particular keys are pressed by the user. A default config | ||
* is used when none is supplied. If you pass a config object then | ||
* it can be changed throughout the lifetime of the containing component. | ||
* But it is best if its identity changes only if the configuration | ||
* values themselves change. | ||
* @param {KeyDirection} direction An optional direction value | ||
* that only applies when the roving tabindex is not being | ||
* used within a grid. This value specifies the arrow key behaviour. | ||
* When set to 'horizontal' then only the ArrowLeft and ArrowRight | ||
* keys move to the previous and next tab stop respectively. | ||
* When set to 'vertical' then only the ArrowUp and ArrowDown keys | ||
* move to the previous and next tab stop respectively. When set | ||
* to 'both' then both the ArrowLeft and ArrowUp keys can be used | ||
* to move to the previous tab stop, and both the ArrowRight | ||
* and ArrowDown keys can be used to move to the next tab stop. | ||
* If you do not pass an explicit value then the 'horizontal' | ||
* behaviour applies. You can change this direction value | ||
* at any time. | ||
*/ | ||
export declare const Provider: ({ children, keyConfig }: { | ||
export declare const Provider: ({ children, direction }: { | ||
children: ReactNode; | ||
keyConfig?: KeyConfig | undefined; | ||
direction?: "horizontal" | "vertical" | "both" | undefined; | ||
}) => ReactElement; |
@@ -10,12 +10,3 @@ /// <reference types="react" /> | ||
} | ||
export declare enum Key { | ||
ARROW_LEFT = "ArrowLeft", | ||
ARROW_RIGHT = "ArrowRight", | ||
ARROW_UP = "ArrowUp", | ||
ARROW_DOWN = "ArrowDown", | ||
HOME = "Home", | ||
END = "End", | ||
HOME_WITH_CTRL = "HomeWithCtrl", | ||
END_WITH_CTRL = "EndWithCtrl" | ||
} | ||
export declare type KeyDirection = "horizontal" | "vertical" | "both"; | ||
export declare enum Navigation { | ||
@@ -31,12 +22,2 @@ PREVIOUS = "PREVIOUS", | ||
} | ||
export declare type KeyConfig = { | ||
[Key.ARROW_LEFT]?: Navigation.PREVIOUS | null; | ||
[Key.ARROW_RIGHT]?: Navigation.NEXT | null; | ||
[Key.ARROW_UP]?: Navigation.PREVIOUS | Navigation.PREVIOUS_ROW | null; | ||
[Key.ARROW_DOWN]?: Navigation.NEXT | Navigation.NEXT_ROW | 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; | ||
}; | ||
export declare type TabStop = Readonly<{ | ||
@@ -53,3 +34,3 @@ id: string; | ||
tabStops: readonly TabStop[]; | ||
keyConfig: KeyConfig; | ||
direction: KeyDirection; | ||
rowStartMap: RowStartMap | null; | ||
@@ -63,3 +44,3 @@ }>; | ||
TAB_STOP_UPDATED = "TAB_STOP_UPDATED", | ||
KEY_CONFIG_UPDATED = "KEY_CONFIG_UPDATED" | ||
DIRECTION_UPDATED = "DIRECTION_UPDATED" | ||
} | ||
@@ -94,5 +75,5 @@ export declare type Action = { | ||
} | { | ||
type: ActionType.KEY_CONFIG_UPDATED; | ||
type: ActionType.DIRECTION_UPDATED; | ||
payload: { | ||
keyConfig: KeyConfig; | ||
direction: KeyDirection; | ||
}; | ||
@@ -99,0 +80,0 @@ }; |
{ | ||
"name": "react-roving-tabindex", | ||
"version": "2.0.0-beta.6", | ||
"version": "2.0.0-beta.7", | ||
"description": "React implementation of a roving tabindex, now with grid support", | ||
@@ -5,0 +5,0 @@ "author": "stevejay", |
205
README.md
@@ -9,9 +9,9 @@ # react-roving-tabindex | ||
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. | ||
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. It receives focus again when the user tabs back into the group. | ||
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. | ||
When in the group, the ArrowLeft and ArrowRight keys move focus between the inputs. (You can also configure this library so that the ArrowUp and ArrowDown keys move focus.) 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). | ||
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. Conventionally, 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. | ||
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). Conventionally, the containing element for the grid should be given the `grid` role, and each grid item should be given the `gridcell` role. The ArrowLeft and ArrowRight keys are used to move focus between inputs in a row, while the ArrowUp and ArrowDown keys move focus between the rows. The Home and End keys (Fn+LeftArrow and Fn+RightArrow on macOS) move focus to a row's first and last inputs respectively. If the Control key is held while pressing the Home and End keys then focus is moved to the very first and very last input in the grid respectively. | ||
@@ -31,3 +31,3 @@ ### Implementation considerations | ||
This package is designed as a general solution for a roving tabindex in a toolbar or a smallish grid. If you need a roving tabindex in a very large grid or table (a few hundred cells or more) then you may be better served with a bespoke implementation. By not including any unnecessary flexibility that this package offers then you will likely create a more performant implementation when performance becomes paramount. For example, you might not need to support the enabling and unenabling of tab stops. It also takes time to register the cells with the package, and there is an overhead to creating the row index mapping. | ||
This package is designed as a general solution for a roving tabindex in a toolbar or a smallish grid. If you need a roving tabindex in a very large grid or table (a few hundred cells or more) then you will likely be better served with a bespoke implementation. By not including any unnecessary flexibility that this package offers then you should create a more performant implementation. For example, you might not need to support the enabling and unenabling of tab stops. It also takes time to register the cells with the package, and there is an overhead to creating the row index mapping for a grid. | ||
@@ -46,7 +46,7 @@ ## Requirements | ||
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. | ||
If you need to support IE then you also need to 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 include a `findIndex` polyfill. | ||
## Usage | ||
There is a storybook for this package [here](https://stevejay.github.io/react-roving-tabindex/), with both basic and grid usage examples. | ||
There is a storybook for this package [here](https://stevejay.github.io/react-roving-tabindex/), with both toolbar and grid usage examples. | ||
@@ -96,3 +96,3 @@ ### Basic usage | ||
const SomeComponent = () => ( | ||
// Wrap each group in a RovingTabIndexProvider. | ||
// Wrap each roving tabindex group in a RovingTabIndexProvider. | ||
<RovingTabIndexProvider> | ||
@@ -109,113 +109,59 @@ {/* | ||
#### Optional ID parameter | ||
If you need to incorporate your own handling for `onClick` and/or `onKeyDown` events then just create handlers that invoke multiple handlers: | ||
The `useRovingTabIndex` hook has an optional third argument that is an options object. One use for it is to pass a custom ID: | ||
```jsx | ||
const [tabIndex, focused, handleKeyDown, handleClick] = useRovingTabIndex( | ||
ref, | ||
disabled, | ||
{ id: "custom-id-1" } // A custom ID. | ||
```ts | ||
return ( | ||
<button | ||
ref={ref} | ||
tabIndex={tabIndex} | ||
disabled={disabled} | ||
onKeyDown={(event) => { | ||
handleKeyDown(event); // handler from the hook | ||
someKeyDownHandler(event); // your handler | ||
}} | ||
onClick={(event) => { | ||
handleClick(event); // handler from the hook | ||
someClickHandler(event); // your handler | ||
}} | ||
> | ||
{children} | ||
</button> | ||
); | ||
``` | ||
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. | ||
#### Direction | ||
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. | ||
It is the ArrowLeft and ArrowRight keys that are used by default to move to the previous and next input respectively. The `RovingTabIndexProvider` has an optional `direction` property that allows you to change this: | ||
#### Navigation | ||
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 | ||
import { Key, Navigation, KeyConfig } from "react-roving-tabindex"; | ||
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 | ||
}; | ||
const SomeComponent = () => ( | ||
<RovingTabIndexProvider direction="vertical"> | ||
{/* whatever */} | ||
</RovingTabIndexProvider> | ||
); | ||
``` | ||
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: | ||
The default behaviour is selected by setting the direction to `horizontal`. If the direction is set to `vertical` then it is the ArrowUp and ArrowDown keys that are used to move to the previous and next input. If the direction is set to `both` then both the ArrowLeft and ArrowUp keys can be used to move to the previous input, and both the ArrowRight and ArrowDown keys can be used to move to the next input. You can update this `direction` value at any time. | ||
```ts | ||
import { | ||
DEFAULT_KEY_CONFIG, | ||
Key, | ||
RovingTabIndexProvider | ||
} from "react-roving-tabindex"; | ||
#### Optional ID parameter | ||
export const CUSTOM_KEY_CONFIG = { | ||
...DEFAULT_KEY_CONFIG, | ||
[Key.ARROW_UP]: undefined, // or null | ||
[Key.ARROW_DOWN]: undefined // or null | ||
}; | ||
The `useRovingTabIndex` hook has an optional third argument that is an options object. It can be used to pass a custom ID: | ||
const SomeComponent = () => ( | ||
<RovingTabIndexProvider keyConfig={CUSTOM_KEY_CONFIG}> | ||
{/* Whatever here */} | ||
</RovingTabIndexProvider> | ||
```jsx | ||
const [tabIndex, focused, handleKeyDown, handleClick] = useRovingTabIndex( | ||
ref, | ||
disabled, | ||
{ id: "custom-id-1" } // A custom ID. | ||
); | ||
``` | ||
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 if you ever need to dynamically change the key configuration behaviour. | ||
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. | ||
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. For example, you cannot assign `Navigation.VERY_FIRST` to `Key.ARROW_LEFT`. | ||
It is fine to create a new object for this third argument each time the containing component is rendered. (This will not trigger an unnecessary re-render.) | ||
### 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 basic usage instructions above. | ||
This package supports a roving tabindex in a grid. For each usage of the `useRovingTabIndex` hook in the grid, you _must_ use the options object that is the third argument of the hook to pass a `rowIndex` value: | ||
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 | ||
import { | ||
Key, | ||
KeyConfig, | ||
Navigation, | ||
RovingTabIndexProvider | ||
} from "react-roving-tabindex"; | ||
// 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( | ||
@@ -228,6 +174,8 @@ ref, | ||
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 three items per row. | ||
The `rowIndex` value must be the zero-based row index for the grid item that the hook is being used with. 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 have four items per row but get 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 an unnecessary re-render. Also, if required you can combine the `rowIndex` with a custom `id` in the same options object (e.g., `{ rowIndex: 0, id: 'some-id' }`). | ||
It is fine to create a new object for this third argument each time the containing component is rendered. (This will not trigger an unnecessary re-render.) Also, if required you can combine the `rowIndex` with a custom `id` in the options object (e.g., `{ rowIndex: 0, id: 'some-id' }`). | ||
The `direction` property of the `RovingTabIndexProvider` is ignored when row indexes are provided. This is because the ArrowUp and ArrowDown keys are always used to move between rows. | ||
## Upgrading | ||
@@ -237,52 +185,9 @@ | ||
There are three breaking changes that might require updating your usages of this library. | ||
There are a few breaking changes in version 2. | ||
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. | ||
This package no longer includes a ponyfill for `Array.prototype.findIndex` and now also uses the `Map` class. If you need to support IE then you will need to install a polyfill for both. That said, if you support IE then you are almost certainly using a suitable global polyfill already. Please see the Installation section earlier in this file for further guidance on polyfills. | ||
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: | ||
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 are passing your own IDs to the hook. So if you have previously used the following... | ||
```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. It is the equivalent of the setting `direction="both"`. 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, // Copy the default configuration. | ||
[Key.ARROW_LEFT]: undefined, // Or use null. | ||
[Key.ARROW_RIGHT]: undefined // Or use 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); | ||
@@ -292,3 +197,3 @@ // ^^ | ||
... then you will need to instead pass the ID in an object: | ||
... then you will need to now pass the ID in an object: | ||
@@ -300,7 +205,7 @@ ```ts | ||
Note that it is fine to create a new object for that third argument each time the containing component is rendered; by itself that will not 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. | ||
It is fine to create a new object for that third argument each time the containing component is rendered. (This will not trigger an unnecessary re-render.) | ||
### 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. | ||
The version 1 release has no breaking changes compared to v0.9.0. The version bump was because the package had stabilized. | ||
@@ -318,5 +223,1 @@ ## License | ||
- The `@types/styled-components` package is currently downgraded to v4.1.8 because of [this issue](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/33311). This only affects the Storybook build. | ||
## TODO | ||
- Own handling of click and keydown (integrating them with the hook's handlers). |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
177381
1289
214