React Events: FocusScope tweaks and docs (#15515)
* FocusScope: rename trap to contain. * FocusScope: avoid potential for el.focus() errors. * FocusScope: add docs. * Update docs formatting.
Nicolas Gallagher committed
Apr 26, 2019 at 13:38 UTC
cc5a49379bdbd78545af9f77465ed10f3283895f
6 files changed
+88
-33
packages/react-events/docs/Focus.md
+6
-3
@@ -1,4 +1,4 @@
1
-## Focus
1
+# Focus
2
3
The `Focus` module responds to focus and blur events on its child. Focus events
4
are dispatched for `mouse`, `pen`, `touch`, and `keyboard`
@@ -18,15 +18,18 @@ const TextField = (props) => (
18
);
19
```
20
21
+## Types
22
+
23
```js
22
-// Types
24
type FocusEvent = {
25
target: Element,
26
type: 'blur' | 'focus' | 'focuschange'
27
}
28
```
29
29
-### disabled: boolean
30
+## Props
31
+
32
+### disabled: boolean = false
33
34
Disables all `Focus` events.
35
packages/react-events/docs/FocusScope.md
+37
@@ -0,0 +1,37 @@
1
+# FocusScope
2
+
3
+The `FocusScope` module can be used to manage focus within its subtree.
4
+
5
+```js
6
+// Example
7
+const Modal = () => (
8
+ <FocusScope
9
+ autoFocus={true}
10
+ contain={true}
11
+ restoreFocus={true}
12
+ >
13
+ <h1>Focus contained within modal</h1>
14
+ <input placeholder="Focusable input" />
15
+ <div role="button" tabIndex={0}>Focusable element</div>
16
+ <input placeholder="Non-focusable input" tabIndex={-1} />
17
+ <Press onPress={onPressClose}>
18
+ <div role="button" tabIndex={0}>Close</div>
19
+ </Press>
20
+ </FocusScope>
21
+);
22
+```
23
+
24
+## Props
25
+
26
+### autoFocus: boolean = false
27
+
28
+Automatically moves focus to the first focusable element within scope.
29
+
30
+### contain: boolean = false
31
+
32
+Contain focus within the subtree of the `FocusScope` instance.
33
+
34
+### restoreFocus: boolean = false
35
+
36
+Automatically restores focus to element that was last focused before focus moved
37
+within the scope.
packages/react-events/docs/Hover.md
+7
-4
@@ -1,8 +1,8 @@
1
-## Hover
1
+# Hover
2
3
The `Hover` module responds to hover events on the element it wraps. Hover
4
-events are only dispatched for `mouse` pointer types. Hover begins when the
5
-pointer enters the element's bounds and ends when the pointer leaves.
4
+events are only dispatched for `mouse` and `pen` pointer types. Hover begins
5
+when the pointer enters the element's bounds and ends when the pointer leaves.
6
7
Hover events do not propagate between `Hover` event responders.
8
@@ -25,8 +25,9 @@ const Link = (props) => (
25
);
26
```
27
28
+## Types
29
+
30
```js
29
-// Types
31
type HoverEvent = {
32
pointerType: 'mouse' | 'pen',
33
target: Element,
@@ -34,6 +35,8 @@ type HoverEvent = {
35
}
36
```
37
38
+## Props
39
+
40
### delayHoverEnd: number
41
42
The duration of the delay between when hover ends and when `onHoverEnd` is
packages/react-events/docs/Press.md
+11
-8
@@ -1,4 +1,4 @@
1
-## Press
1
+# Press
2
3
The `Press` module responds to press events on the element it wraps. Press
4
events are dispatched for `mouse`, `pen`, `touch`, and `keyboard` pointer types.
@@ -33,8 +33,9 @@ const Button = (props) => (
33
);
34
```
35
36
+## Types
37
+
38
```js
37
-// Types
39
type PressEvent = {
40
pointerType: 'mouse' | 'touch' | 'pen' | 'keyboard',
41
target: Element,
@@ -42,13 +43,15 @@ type PressEvent = {
43
}
44
45
type PressOffset = {
45
- top: number,
46
- right: number,
47
- bottom: number,
48
- right: number
46
+ top?: number,
47
+ right?: number,
48
+ bottom?: number,
49
+ right?: number
50
};
51
```
52
53
+## Props
54
+
55
### delayLongPress: number = 500ms
56
57
The duration of a press before `onLongPress` and `onLongPressChange` are called.
@@ -64,7 +67,7 @@ The duration of a delay between when the press starts and when `onPressStart` is
67
called. This delay is cut short (and `onPressStart` is called) if the press is
68
released before the threshold is exceeded.
69
67
-### disabled: boolean
70
+### disabled: boolean = false
71
72
Disables all `Press` events.
73
@@ -118,7 +121,7 @@ Called once the element is pressed down. If the press is released before the
121
122
Defines how far the pointer (while held down) may move outside the bounds of the
123
element before it is deactivated. Ensure you pass in a constant to reduce memory
121
-allocations.
124
+allocations. Default is `20` for each offset.
125
126
### preventDefault: boolean = true
127
packages/react-events/src/FocusScope.js
+23
-14
@@ -15,8 +15,8 @@ import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
15
16
type FocusScopeProps = {
17
autoFocus: Boolean,
18
+ contain: Boolean,
19
restoreFocus: Boolean,
19
- trap: Boolean,
20
};
21
22
type FocusScopeState = {
@@ -27,14 +27,21 @@ type FocusScopeState = {
27
const targetEventTypes = [{name: 'keydown', passive: false}];
28
const rootEventTypes = [{name: 'focus', passive: true, capture: true}];
29
30
-function focusFirstChildEventTarget(
30
+function focusElement(element: ?HTMLElement) {
31
+ if (element != null) {
32
+ try {
33
+ element.focus();
34
+ } catch (err) {}
35
+ }
36
+}
37
+
38
+function getFirstFocusableElement(
39
context: ReactResponderContext,
40
state: FocusScopeState,
33
-): void {
41
+): ?HTMLElement {
42
const elements = context.getFocusableElementsInScope();
43
if (elements.length > 0) {
36
- const firstElement = elements[0];
37
- firstElement.focus();
44
+ return elements[0];
45
}
46
}
47
@@ -78,7 +85,7 @@ const FocusScopeResponder = {
85
86
if (shiftKey) {
87
if (position === 0) {
81
- if (props.trap) {
88
+ if (props.contain) {
89
nextElement = elements[lastPosition];
90
} else {
91
// Out of bounds
@@ -90,7 +97,7 @@ const FocusScopeResponder = {
97
}
98
} else {
99
if (position === lastPosition) {
93
- if (props.trap) {
100
+ if (props.contain) {
101
nextElement = elements[0];
102
} else {
103
// Out of bounds
@@ -107,7 +114,7 @@ const FocusScopeResponder = {
114
if (!context.isTargetWithinEventResponderScope(nextElement)) {
115
context.releaseOwnership();
116
}
110
- nextElement.focus();
117
+ focusElement(nextElement);
118
state.currentFocusedNode = nextElement;
119
((nativeEvent: any): KeyboardEvent).preventDefault();
120
}
@@ -122,14 +129,15 @@ const FocusScopeResponder = {
129
) {
130
const {target} = event;
131
125
- // Handle global trapping
126
- if (props.trap) {
132
+ // Handle global focus containment
133
+ if (props.contain) {
134
if (!context.isTargetWithinEventComponent(target)) {
135
const currentFocusedNode = state.currentFocusedNode;
136
if (currentFocusedNode !== null) {
130
- currentFocusedNode.focus();
137
+ focusElement(currentFocusedNode);
138
} else if (props.autoFocus) {
132
- focusFirstChildEventTarget(context, state);
139
+ const firstElement = getFirstFocusableElement(context, state);
140
+ focusElement(firstElement);
141
}
142
}
143
}
@@ -143,7 +151,8 @@ const FocusScopeResponder = {
151
state.nodeToRestore = context.getActiveDocument().activeElement;
152
}
153
if (props.autoFocus) {
146
- focusFirstChildEventTarget(context, state);
154
+ const firstElement = getFirstFocusableElement(context, state);
155
+ focusElement(firstElement);
156
}
157
},
158
onUnmount(
@@ -156,7 +165,7 @@ const FocusScopeResponder = {
165
state.nodeToRestore !== null &&
166
context.hasOwnership()
167
) {
159
- state.nodeToRestore.focus();
168
+ focusElement(state.nodeToRestore);
169
}
170
},
171
onOwnershipChange(
packages/react-events/src/__tests__/FocusScope-test.internal.js
+4
-4
@@ -85,7 +85,7 @@ describe('FocusScope event responder', () => {
85
expect(document.activeElement).toBe(divRef.current);
86
});
87
88
- it('should work as expected with autofocus and trapping', () => {
88
+ it('should work as expected with autoFocus and contain', () => {
89
const inputRef = React.createRef();
90
const input2Ref = React.createRef();
91
const buttonRef = React.createRef();
@@ -93,7 +93,7 @@ describe('FocusScope event responder', () => {
93
94
const SimpleFocusScope = () => (
95
<div>
96
- <FocusScope autoFocus={true} trap={true}>
96
+ <FocusScope autoFocus={true} contain={true}>
97
<input ref={inputRef} tabIndex={-1} />
98
<button ref={buttonRef} id={1} />
99
<button ref={button2Ref} id={2} />
@@ -154,7 +154,7 @@ describe('FocusScope event responder', () => {
154
expect(document.activeElement).toBe(button2Ref.current);
155
});
156
157
- it('should work as expected when nested with scope that is trapped', () => {
157
+ it('should work as expected when nested with scope that is contained', () => {
158
const inputRef = React.createRef();
159
const input2Ref = React.createRef();
160
const buttonRef = React.createRef();
@@ -167,7 +167,7 @@ describe('FocusScope event responder', () => {
167
<FocusScope>
168
<input ref={inputRef} tabIndex={-1} />
169
<button ref={buttonRef} id={1} />
170
- <FocusScope trap={true}>
170
+ <FocusScope contain={true}>
171
<button ref={button2Ref} id={2} />
172
<button ref={button3Ref} id={3} />
173
</FocusScope>