Skip to content

Commit cc5a493

Browse files
authored
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.
1 parent 796c67a commit cc5a493

File tree

6 files changed

+88
-33
lines changed

6 files changed

+88
-33
lines changed

packages/react-events/docs/Focus.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Focus
1+
# Focus
22

33
The `Focus` module responds to focus and blur events on its child. Focus events
44
are dispatched for `mouse`, `pen`, `touch`, and `keyboard`
@@ -18,15 +18,18 @@ const TextField = (props) => (
1818
);
1919
```
2020

21+
## Types
22+
2123
```js
22-
// Types
2324
type FocusEvent = {
2425
target: Element,
2526
type: 'blur' | 'focus' | 'focuschange'
2627
}
2728
```
2829

29-
### disabled: boolean
30+
## Props
31+
32+
### disabled: boolean = false
3033

3134
Disables all `Focus` events.
3235

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
## Hover
1+
# Hover
22

33
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.
66

77
Hover events do not propagate between `Hover` event responders.
88

@@ -25,15 +25,18 @@ const Link = (props) => (
2525
);
2626
```
2727

28+
## Types
29+
2830
```js
29-
// Types
3031
type HoverEvent = {
3132
pointerType: 'mouse' | 'pen',
3233
target: Element,
3334
type: 'hoverstart' | 'hoverend' | 'hovermove' | 'hoverchange'
3435
}
3536
```
3637

38+
## Props
39+
3740
### delayHoverEnd: number
3841

3942
The duration of the delay between when hover ends and when `onHoverEnd` is

packages/react-events/docs/Press.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Press
1+
# Press
22

33
The `Press` module responds to press events on the element it wraps. Press
44
events are dispatched for `mouse`, `pen`, `touch`, and `keyboard` pointer types.
@@ -33,22 +33,25 @@ const Button = (props) => (
3333
);
3434
```
3535

36+
## Types
37+
3638
```js
37-
// Types
3839
type PressEvent = {
3940
pointerType: 'mouse' | 'touch' | 'pen' | 'keyboard',
4041
target: Element,
4142
type: 'press' | 'pressstart' | 'pressend' | 'presschange' | 'pressmove' | 'longpress' | 'longpresschange'
4243
}
4344

4445
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
4950
};
5051
```
5152

53+
## Props
54+
5255
### delayLongPress: number = 500ms
5356

5457
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
6467
called. This delay is cut short (and `onPressStart` is called) if the press is
6568
released before the threshold is exceeded.
6669

67-
### disabled: boolean
70+
### disabled: boolean = false
6871

6972
Disables all `Press` events.
7073

@@ -118,7 +121,7 @@ Called once the element is pressed down. If the press is released before the
118121

119122
Defines how far the pointer (while held down) may move outside the bounds of the
120123
element before it is deactivated. Ensure you pass in a constant to reduce memory
121-
allocations.
124+
allocations. Default is `20` for each offset.
122125

123126
### preventDefault: boolean = true
124127

packages/react-events/src/FocusScope.js

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
1515

1616
type FocusScopeProps = {
1717
autoFocus: Boolean,
18+
contain: Boolean,
1819
restoreFocus: Boolean,
19-
trap: Boolean,
2020
};
2121

2222
type FocusScopeState = {
@@ -27,14 +27,21 @@ type FocusScopeState = {
2727
const targetEventTypes = [{name: 'keydown', passive: false}];
2828
const rootEventTypes = [{name: 'focus', passive: true, capture: true}];
2929

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(
3139
context: ReactResponderContext,
3240
state: FocusScopeState,
33-
): void {
41+
): ?HTMLElement {
3442
const elements = context.getFocusableElementsInScope();
3543
if (elements.length > 0) {
36-
const firstElement = elements[0];
37-
firstElement.focus();
44+
return elements[0];
3845
}
3946
}
4047

@@ -78,7 +85,7 @@ const FocusScopeResponder = {
7885

7986
if (shiftKey) {
8087
if (position === 0) {
81-
if (props.trap) {
88+
if (props.contain) {
8289
nextElement = elements[lastPosition];
8390
} else {
8491
// Out of bounds
@@ -90,7 +97,7 @@ const FocusScopeResponder = {
9097
}
9198
} else {
9299
if (position === lastPosition) {
93-
if (props.trap) {
100+
if (props.contain) {
94101
nextElement = elements[0];
95102
} else {
96103
// Out of bounds
@@ -107,7 +114,7 @@ const FocusScopeResponder = {
107114
if (!context.isTargetWithinEventResponderScope(nextElement)) {
108115
context.releaseOwnership();
109116
}
110-
nextElement.focus();
117+
focusElement(nextElement);
111118
state.currentFocusedNode = nextElement;
112119
((nativeEvent: any): KeyboardEvent).preventDefault();
113120
}
@@ -122,14 +129,15 @@ const FocusScopeResponder = {
122129
) {
123130
const {target} = event;
124131

125-
// Handle global trapping
126-
if (props.trap) {
132+
// Handle global focus containment
133+
if (props.contain) {
127134
if (!context.isTargetWithinEventComponent(target)) {
128135
const currentFocusedNode = state.currentFocusedNode;
129136
if (currentFocusedNode !== null) {
130-
currentFocusedNode.focus();
137+
focusElement(currentFocusedNode);
131138
} else if (props.autoFocus) {
132-
focusFirstChildEventTarget(context, state);
139+
const firstElement = getFirstFocusableElement(context, state);
140+
focusElement(firstElement);
133141
}
134142
}
135143
}
@@ -143,7 +151,8 @@ const FocusScopeResponder = {
143151
state.nodeToRestore = context.getActiveDocument().activeElement;
144152
}
145153
if (props.autoFocus) {
146-
focusFirstChildEventTarget(context, state);
154+
const firstElement = getFirstFocusableElement(context, state);
155+
focusElement(firstElement);
147156
}
148157
},
149158
onUnmount(
@@ -156,7 +165,7 @@ const FocusScopeResponder = {
156165
state.nodeToRestore !== null &&
157166
context.hasOwnership()
158167
) {
159-
state.nodeToRestore.focus();
168+
focusElement(state.nodeToRestore);
160169
}
161170
},
162171
onOwnershipChange(

packages/react-events/src/__tests__/FocusScope-test.internal.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,15 @@ describe('FocusScope event responder', () => {
8585
expect(document.activeElement).toBe(divRef.current);
8686
});
8787

88-
it('should work as expected with autofocus and trapping', () => {
88+
it('should work as expected with autoFocus and contain', () => {
8989
const inputRef = React.createRef();
9090
const input2Ref = React.createRef();
9191
const buttonRef = React.createRef();
9292
const button2Ref = React.createRef();
9393

9494
const SimpleFocusScope = () => (
9595
<div>
96-
<FocusScope autoFocus={true} trap={true}>
96+
<FocusScope autoFocus={true} contain={true}>
9797
<input ref={inputRef} tabIndex={-1} />
9898
<button ref={buttonRef} id={1} />
9999
<button ref={button2Ref} id={2} />
@@ -154,7 +154,7 @@ describe('FocusScope event responder', () => {
154154
expect(document.activeElement).toBe(button2Ref.current);
155155
});
156156

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', () => {
158158
const inputRef = React.createRef();
159159
const input2Ref = React.createRef();
160160
const buttonRef = React.createRef();
@@ -167,7 +167,7 @@ describe('FocusScope event responder', () => {
167167
<FocusScope>
168168
<input ref={inputRef} tabIndex={-1} />
169169
<button ref={buttonRef} id={1} />
170-
<FocusScope trap={true}>
170+
<FocusScope contain={true}>
171171
<button ref={button2Ref} id={2} />
172172
<button ref={button3Ref} id={3} />
173173
</FocusScope>

0 commit comments

Comments
 (0)