Skip to content

Commit b33633d

Browse files
authored
[react-interactions] Repurpose React a11y modules (#16997)
1 parent de2edc2 commit b33633d

19 files changed

+698
-722
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# FocusContain
2+
3+
`FocusContain` is a component that contains user-focusability to only that
4+
of the children of the component. This means focus control will not escape
5+
unless the componoent is disabled (using the `disabled` prop) or unmounted.
6+
Additionally, `FocusContain` can contain tab focus when passed a `ReactScope`
7+
using the `tabFocus` prop.
8+
9+
## Usage
10+
11+
```jsx
12+
import FocusContain from 'react-interactions/accessibility/focus-contain';
13+
import TabbableScope from 'react-interactions/accessibility/tabbable-scope';
14+
15+
function MyDialog(props) {
16+
return (
17+
<FocusContain tabScope={TabbableScope} disabled={false}>
18+
<div>
19+
<h2>{props.title}<h2>
20+
<p>{props.text}</p>
21+
<Button onPress={...}>Accept</Button>
22+
<Button onPress={...}>Close</Button>
23+
</div>
24+
</FocusContain>
25+
)
26+
}
27+
```

packages/react-interactions/accessibility/docs/FocusControl.md

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,59 @@
11
# FocusManager
22

3-
`FocusManager` is a component that is designed to provide basic focus management
4-
control. These are the various props that `FocusManager` accepts:
3+
`FocusManager` is a module that exports a selection of helpful utility functions to be used
4+
in conjunction with the `ref` from a React Scope, such as `TabbableScope`.
55

6-
## Usage
6+
## Example
77

88
```jsx
9-
function MyDialog(props) {
9+
import {
10+
focusFirst,
11+
focusNext,
12+
focusPrevious,
13+
getNextScope,
14+
getPreviousScope,
15+
} from 'react-interactions/accessibility/focus-manager';
16+
17+
function KeyboardFocusMover(props) {
18+
const scopeRef = useRef(null);
19+
20+
useEffect(() => {
21+
const scope = scopeRef.current;
22+
23+
if (scope) {
24+
// Focus the first tabbable DOM node in my children
25+
focusFirst(scope);
26+
// Then focus the next chilkd
27+
focusNext(scope);
28+
}
29+
});
30+
1031
return (
11-
<FocusManager containFocus={true} autoFocus={true}>
12-
<div>
13-
<h2>{props.title}<h2>
14-
<p>{props.text}</p>
15-
<Button onPress={...}>Accept</Button>
16-
<Button onPress={...}>Close</Button>
17-
</div>
18-
</FocusManager>
19-
)
32+
<TabbableScope ref={scopeRef}>
33+
{props.children}
34+
</TabbableScope>
35+
);
2036
}
2137
```
2238

23-
### `scope`
24-
`FocusManager` accepts a custom `ReactScope`. If a custom one is not supplied, `FocusManager`
25-
will default to using `TabbableScope`.
39+
## FocusManager API
40+
41+
### `focusFirst`
42+
43+
Focus the first node that matches the given scope.
44+
45+
### `focusNext`
46+
47+
Focus the next sequential node that matches the given scope.
48+
49+
### `focusPrevious`
50+
51+
Focus the previous sequential node that matches the given scope.
52+
53+
### `getNextScope`
2654

27-
### `autoFocus`
28-
When enabled, the first host node that matches the `FocusManager` scope will be focused
29-
upon the `FocusManager` mounting.
55+
Focus the first node that matches the next sibling scope from the given scope.
3056

31-
### `restoreFocus`
32-
When enabled, the previous host node that was focused as `FocusManager` is mounted,
33-
has its focus restored upon `FocusManager` unmounting.
57+
### `getPreviousScope`
3458

35-
### `containFocus`
36-
This contains the user focus to only that of `FocusManager`s sub-tree. Tabbing or
37-
interacting with nodes outside the sub-tree will restore focus back into the `FocusManager`.
38-
This is useful for modals, dialogs, dropdowns and other UI elements that require
39-
a form of user-focus control that is similar to the `inert` property on the web.
59+
Focus the first node that matches the previous sibling scope from the given scope.

packages/react-interactions/accessibility/docs/TabbableScope.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# TabbableScope
22

33
`TabbableScope` is a custom scope implementation that can be used with
4-
`FocusManager`, `FocusList`, `FocusTable` and `FocusControl` modules.
4+
`FocusContain`, `FocusGroup`, `FocusTable` and `FocusManager` modules.
55

66
## Usage
77

88
```jsx
9+
import TabbableScope from 'react-interactions/accessibility/tabbable-scope';
10+
911
function FocusableNodeCollector(props) {
1012
const scopeRef = useRef(null);
1113

packages/react-interactions/accessibility/focus-control.js renamed to packages/react-interactions/accessibility/focus-contain.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99

1010
'use strict';
1111

12-
module.exports = require('./src/FocusControl');
12+
module.exports = require('./src/FocusContain');

packages/react-interactions/accessibility/focus-list.js renamed to packages/react-interactions/accessibility/focus-group.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99

1010
'use strict';
1111

12-
module.exports = require('./src/FocusList');
12+
module.exports = require('./src/FocusGroup');
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {ReactScope} from 'shared/ReactTypes';
11+
import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12+
13+
import React from 'react';
14+
import {useFocusWithin} from 'react-interactions/events/focus';
15+
import {useKeyboard} from 'react-interactions/events/keyboard';
16+
import {
17+
focusPrevious,
18+
focusNext,
19+
} from 'react-interactions/accessibility/focus-manager';
20+
21+
type FocusContainProps = {|
22+
children: React.Node,
23+
disabled?: boolean,
24+
tabScope: ReactScope,
25+
|};
26+
27+
const {useLayoutEffect, useRef} = React;
28+
29+
export default function FocusContain({
30+
children,
31+
disabled,
32+
tabScope: TabScope,
33+
}: FocusContainProps) {
34+
const scopeRef = useRef(null);
35+
// This ensures tabbing works through the React tree (including Portals and Suspense nodes)
36+
const keyboard = useKeyboard({
37+
onKeyDown(event: KeyboardEvent): void {
38+
if (disabled || event.key !== 'Tab') {
39+
event.continuePropagation();
40+
return;
41+
}
42+
const scope = scopeRef.current;
43+
if (scope !== null) {
44+
if (event.shiftKey) {
45+
focusPrevious(scope, event, true);
46+
} else {
47+
focusNext(scope, event, true);
48+
}
49+
}
50+
},
51+
});
52+
const focusWithin = useFocusWithin({
53+
onBlurWithin: function(event) {
54+
if (disabled) {
55+
event.continuePropagation();
56+
return;
57+
}
58+
const lastNode = event.target;
59+
if (lastNode) {
60+
requestAnimationFrame(() => {
61+
(lastNode: any).focus();
62+
});
63+
}
64+
},
65+
});
66+
useLayoutEffect(
67+
() => {
68+
const scope = scopeRef.current;
69+
if (scope && !disabled) {
70+
const elems = scope.getScopedNodes();
71+
if (elems && elems.indexOf(document.activeElement) === -1) {
72+
elems[0].focus();
73+
}
74+
}
75+
},
76+
[disabled],
77+
);
78+
79+
return (
80+
<TabScope ref={scopeRef} listeners={[keyboard, focusWithin]}>
81+
{children}
82+
</TabScope>
83+
);
84+
}

0 commit comments

Comments
 (0)