Skip to content

Commit

Permalink
Merge pull request #61 from jamesplease/more-work
Browse files Browse the repository at this point in the history
Continue work
  • Loading branch information
jamesplease committed Jun 17, 2021
2 parents a679694 + 444943e commit 4230f75
Show file tree
Hide file tree
Showing 14 changed files with 95 additions and 43 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ This library has the following peer dependencies:
## Table of Contents

- [**Guides**](#guides)
- [Getting started](#getting-started)
- [Basic Setup](#basic-setup)
- [Getting Started](#getting-started)
- [FAQ](#faq)
- [**API Reference**](#api-reference)
- [\<FocusRoot/\>](#focusroot-)
Expand All @@ -58,7 +59,7 @@ This library has the following peer dependencies:

## Guides

### Getting Started
### Basic Setup

Render the `FocusRoot` high up in your application's component tree.

Expand Down Expand Up @@ -90,6 +91,13 @@ LRUD commands on their keyboard or remote control.
This behavior can be configured through the props of the FocusNode component. To
learn more about those props, refer to the API documentation below.

### Getting Started

The recommended way to familiarize yourself with this library is to begin by looking at the [examples](#examples). The examples
do a great job at demonstrating the kinds of interfaces you can create with this library using little code.

Once you've checked out a few examples you should be in a better position to read through these API docs!

### FAQ

#### What is LRUD?
Expand Down Expand Up @@ -147,7 +155,7 @@ All props are optional. Example usage appears beneath the props table.
| `disabled` | boolean | `false` | This node will not receive focus when `true`. |
| `isGrid` | boolean | `false` | Pass `true` to make this a grid. |
| `isTrap` | boolean | `false` | Pass `true` to make this a focus trap. |
| `restoreTrapFocusHierarchy` | boolean | `true` | Pass `false` and, if this node is a trap, it will not restore their previous focus hierarchy when becoming focused again. |
| `forgetTrapFocusHierarchy` | boolean | `false` | Pass `true` and, if this node is a trap, it will not restore their previous focus hierarchy when becoming focused again. |
| `onMountAssignFocusTo` | string | | A focus ID of a nested child to default focus to when this node mounts. |
| `defaultFocusColumn` | number | `0` | The column index that should receive focus when focus is assigned to this focus node. Applies to grids only. |
| `defaultFocusRow` | number | `0` | The row index that should receive focus when focus is assigned to this focus node. Applies to grids only. |
Expand Down Expand Up @@ -340,7 +348,7 @@ A focus node. Each `<FocusNode/>` React component creates one of these.
| `wrapGridHorizontal` | boolean | `true` when grid columns will wrap. |
| `isRoot` | boolean | `true` this is the root node. |
| `trap` | boolean | `true` when this node is a focus trap. |
| `restoreTrapFocusHierarchy` | boolean | Set to `true` and a focus trap will restore its previous hierarchy upon becoming re-focused. |
| `forgetTrapFocusHierarchy` | boolean | Set to `false` and a focus trap will restore its previous hierarchy upon becoming re-focused. |
| `parentId` | string \| `null` | The focus ID of the parent node. `null` for the root node. |
| `orientation` | string | A string representing the orientation of the node (either `"horizontal"` or `"vertical"`) |
| `navigationStyle` | string | One of `'first-child'` or `'grid'` |
Expand Down
2 changes: 1 addition & 1 deletion examples/advanced/modal/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function App() {
}}
focusId="trap"
className="modal-container"
restoreTrapFocusHierarchy={false}
forgetTrapFocusHierarchy
isTrap
orientation="vertical">
<div className="modal-background"></div>
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/focus-trap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ The example will be running at `localhost:3000`.
### Features Demonstrated

- The `isTrap` prop, which creates a focus trap
- The `restoreTrapFocusHierarchy` prop, which makes it so that re-entering the trap does not preserve
- The `forgetTrapFocusHierarchy` prop, which makes it so that re-entering the trap does not preserve
the previous hierarchy.
- The `useSetFocus` hook to imperatively set the focus
2 changes: 1 addition & 1 deletion examples/basic/focus-trap/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function App() {
<FocusNode
focusId="trap"
className="block-container focus-trap"
restoreTrapFocusHierarchy={false}
forgetTrapFocusHierarchy
isTrap
orientation="vertical">
<div className="block-header">Focus Trap</div>
Expand Down
20 changes: 20 additions & 0 deletions guides/disabled.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Disabled Focus Nodes

A common UI pattern is to disable interactive elements such as buttons. This library
allows you to disable Focus Nodes to achieve that same effect.

Disabled nodes cannot receive focus and cannot be selected.

### How to Disable Nodes

Use the `disabled` prop to disable a FocusNode.

```jsx
function MyComponent({ isEnabled }) {
return (
<FocusNode disabled={!isEnabled}>
Continue
</FocusNode>
)
}
```
14 changes: 10 additions & 4 deletions guides/exiting.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# `isExiting`

This is an advanced prop that can help for a very particular kind of exit animation. The specific situation where this prop is helpful is:
This is an advanced prop that most people will never need to use. It exists to support one very specific situation that most apps simply
will never encounter.

- A new page is animating in while an existing page is animating out
- The user did not select a focus node to initiate the transition. For example, it may have occurred automatically due to a timeout.
- During the transition, you wish to display the focus node that is animating out in a focused state.
If the following three things are true about your situation, then you should use this prop:

1. A new page is animating in while an existing page is animating out
2. During the transition, you wish to display the focus node that is animating out in a focused state.
3. The user did not select a focus node to initiate the transition. For example, it may have occurred automatically due to a timeout.

If any of these three things are not true, then you do not need to use `isExiting`. For example, if the user selects the node, then
you should instead use the active prop to ensure that the element animates out while looking visually focused.
23 changes: 21 additions & 2 deletions guides/focus-traps.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Focus Traps

Sometimes, you need to trap the focus in a UI element, so that as a user navigates using LRUD,
Sometimes, you need to trap the focus in a UI element so that as a user navigates using LRUD,
focus does not leave the element. Examples include:

- Modals / popups
Expand Down Expand Up @@ -57,6 +57,25 @@ export default function App() {
}
```

### Focus Traps Remember Their Hierarchy

When you exit a focus trap, and re-enter it, it will remember what was last focused and place focus there.

If you wish to disable this, use the `forgetTrapFocusHierarchy` prop.

```jsx
<FocusNode
focusId="trap"
forgetTrapFocusHierarchy
isTrap>
<FocusNode onSelected={() => setFocus('enter-trap-button')}>
Exit Focus Trap
</FocusNode>
</FocusNode>
```



## Examples

- [Focus Trap](../examples/focus-trap)
- [Focus Trap](../examples/focus-trap)
1 change: 0 additions & 1 deletion guides/focus.md

This file was deleted.

20 changes: 10 additions & 10 deletions src/focus-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function FocusNode(
orientation,
isGrid = false,
isTrap = false,
restoreTrapFocusHierarchy,
forgetTrapFocusHierarchy = false,

defaultFocusColumn,
defaultFocusRow,
Expand Down Expand Up @@ -216,7 +216,7 @@ export function FocusNode(
onClickRef.current = onClick;
onMouseOverRef.current = onMouseOver;

const defaultRestoreFocusTrap = isTrap ? true : undefined;
const defaultForgetFocusTrap = isTrap ? false : undefined;
const defaultOrientation = !isGrid ? undefined : 'horizontal';

const contextValue = useContext(FocusContext.Context);
Expand Down Expand Up @@ -248,10 +248,10 @@ export function FocusNode(
trap: Boolean(isTrap),
wrapGridHorizontal: wrapGridHorizontalValue,
wrapGridVertical: wrapGridVerticalValue,
restoreTrapFocusHierarchy:
restoreTrapFocusHierarchy !== undefined
? restoreTrapFocusHierarchy
: defaultRestoreFocusTrap,
forgetTrapFocusHierarchy:
forgetTrapFocusHierarchy !== undefined
? forgetTrapFocusHierarchy
: defaultForgetFocusTrap,
navigationStyle: isGrid ? 'grid' : 'first-child',

defaultFocusColumn: defaultFocusColumn ?? 0,
Expand Down Expand Up @@ -304,9 +304,9 @@ export function FocusNode(
);
}

if (restoreTrapFocusHierarchy && !nodeDefinition.trap) {
if (forgetTrapFocusHierarchy && !nodeDefinition.trap) {
warning(
'You passed the restoreTrapFocusHierarchy prop to a focus node that is not a trap. ' +
'You passed the forgetTrapFocusHierarchy prop to a focus node that is not a trap. ' +
'This will have no effect, but it may represent an error in your code. ' +
`This node has a focus ID of ${focusId}.`,
'RESTORE_TRAP_FOCUS_WITHOUT_TRAP'
Expand Down Expand Up @@ -382,7 +382,7 @@ export function FocusNode(
defaultFocusRow,
wrapping,
trap: isTrap,
restoreTrapFocusHierarchy,
forgetTrapFocusHierarchy,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
Expand All @@ -392,7 +392,7 @@ export function FocusNode(
defaultFocusRow,
wrapping,
isTrap,
restoreTrapFocusHierarchy,
forgetTrapFocusHierarchy,
]);

useEffect(() => {
Expand Down
10 changes: 5 additions & 5 deletions src/focus-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const dynamicNodeProps = [
'defaultFocusRow',
'wrapping',
'trap',
'restoreTrapFocusHierarchy',
'forgetTrapFocusHierarchy',
];

export default function createFocusStore({
Expand Down Expand Up @@ -76,7 +76,7 @@ export default function createFocusStore({
wrapping,
navigationStyle: 'first-child',
nodeNavigationItem: 'default',
restoreTrapFocusHierarchy: true,
forgetTrapFocusHierarchy: true,
children: [],
focusedChildIndex: null,
prevFocusedChildIndex: null,
Expand Down Expand Up @@ -318,9 +318,9 @@ export default function createFocusStore({
defaultFocusRow: update.defaultFocusRow ?? currentNode.defaultFocusRow,
wrapping: update.wrapping ?? currentNode.wrapping,
trap: update.wrapping ?? currentNode.trap,
restoreTrapFocusHierarchy:
update.restoreTrapFocusHierarchy ??
currentNode.restoreTrapFocusHierarchy,
forgetTrapFocusHierarchy:
update.forgetTrapFocusHierarchy ??
currentNode.forgetTrapFocusHierarchy,
};

const updatedChildren = recursivelyUpdateChildren(
Expand Down
8 changes: 4 additions & 4 deletions src/tests/focus-trap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
import { warning } from '../utils/warning';

describe('Focus Traps', () => {
it('warns when restoreTrapFocusHierarchy is passed to a non-trap', () => {
it('warns when forgetTrapFocusHierarchy is passed to a non-trap', () => {
function TestComponent() {
return (
<FocusNode restoreTrapFocusHierarchy>
<FocusNode forgetTrapFocusHierarchy>
<FocusNode>
<FocusNode />
</FocusNode>
Expand Down Expand Up @@ -206,7 +206,7 @@ describe('Focus Traps', () => {
expect(focusStore.getState().focusedNodeId).toEqual('nodeB-B');
});

it('supports restoreTrapFocusHierarchy=false', () => {
it('supports forgetTrapFocusHierarchy', () => {
let focusStore;
let setFocus;

Expand All @@ -228,7 +228,7 @@ describe('Focus Traps', () => {
focusId="nodeB"
data-testid="nodeB"
isTrap
restoreTrapFocusHierarchy={false}>
forgetTrapFocusHierarchy>
<FocusNode focusId="nodeB-A" data-testid="nodeB-A" />
<FocusNode focusId="nodeB-B" data-testid="nodeB-B" />
</FocusNode>
Expand Down
10 changes: 5 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export interface RootFocusNode extends BaseNode {
defaultFocusColumn: number;
defaultFocusRow: number;

restoreTrapFocusHierarchy: boolean;
forgetTrapFocusHierarchy: boolean;

navigationStyle: NavigationStyle;
nodeNavigationItem: NodeNavigationItem;
Expand All @@ -132,7 +132,7 @@ export interface NodeUpdate {
isExiting?: boolean;
wrapping?: boolean;
trap?: boolean;
restoreTrapFocusHierarchy?: boolean;
forgetTrapFocusHierarchy?: boolean;
defaultFocusColumn?: number;
defaultFocusRow?: number;
}
Expand All @@ -153,7 +153,7 @@ export interface FocusNode extends BaseNode {
defaultFocusColumn: number;
defaultFocusRow: number;

restoreTrapFocusHierarchy: boolean;
forgetTrapFocusHierarchy: boolean;

wrapGridVertical: boolean;
wrapGridHorizontal: boolean;
Expand Down Expand Up @@ -198,7 +198,7 @@ export interface NodeDefinition extends FocusNodeEvents {
defaultFocusColumn?: number;
defaultFocusRow?: number;

restoreTrapFocusHierarchy?: boolean;
forgetTrapFocusHierarchy?: boolean;

// This will seek out this node identifier, and set focus to it.
// IDs are more general, but child indices work, too.
Expand Down Expand Up @@ -246,7 +246,7 @@ export interface FocusNodeProps extends FocusNodeEvents {
orientation?: Orientation;
isGrid?: boolean;
isTrap?: boolean;
restoreTrapFocusHierarchy?: boolean;
forgetTrapFocusHierarchy?: boolean;
propsFromNode?: PropsFromNode;
isExiting?: boolean;
onMountAssignFocusTo?: Id;
Expand Down
6 changes: 3 additions & 3 deletions src/update-focus/get-nodes-from-focus-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function getParentGrid(nodes: NodeMap, node: Node): FocusNode {
const parentId = node.parentId as Id;
const rowNode = nodes[parentId] as FocusNode;

const rowChildren = rowNode.children.filter(nodeId => {
const rowChildren = rowNode.children.filter((nodeId) => {
const node = nodes[nodeId];
return node && !node.disabled && !node.isExiting;
});
Expand All @@ -28,7 +28,7 @@ function getParentGrid(nodes: NodeMap, node: Node): FocusNode {
const gridNodeId = rowNode.parentId as Id;
const gridNode = nodes[gridNodeId] as FocusNode;

const gridChildren = gridNode.children.filter(nodeId => {
const gridChildren = gridNode.children.filter((nodeId) => {
const node = nodes[nodeId];
return node && !node.disabled && !node.isExiting;
});
Expand Down Expand Up @@ -106,7 +106,7 @@ export default function getNodesFromFocusChange({
result[nodeId]._gridRowIndex = 0;
}

if (nodeToUpdate.trap && nodeToUpdate.restoreTrapFocusHierarchy) {
if (nodeToUpdate.trap && !nodeToUpdate.forgetTrapFocusHierarchy) {
const childHierarchy = blurHierarchy.slice(i + 1);
// @ts-ignore
result[nodeId]._focusTrapPreviousHierarchy = childHierarchy;
Expand Down
4 changes: 2 additions & 2 deletions src/utils/node-from-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export default function nodeFromDefinition({
defaultFocusColumn: defaultFocusColumnValue,
defaultFocusRow: defaultFocusRowValue,

restoreTrapFocusHierarchy: Boolean(
nodeDefinition.restoreTrapFocusHierarchy ?? true
forgetTrapFocusHierarchy: Boolean(
nodeDefinition.forgetTrapFocusHierarchy ?? false
),
children: [],
focusedChildIndex: null,
Expand Down

0 comments on commit 4230f75

Please sign in to comment.