@samitouri / QOS-React-2 / commits / cc5a49379b

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>