diff --git a/packages/react-events/docs/Focus.md b/packages/react-events/docs/Focus.md index 8f2ca5bda7a68..ea4151bf7302d 100644 --- a/packages/react-events/docs/Focus.md +++ b/packages/react-events/docs/Focus.md @@ -1,4 +1,4 @@ -## Focus +# Focus The `Focus` module responds to focus and blur events on its child. Focus events are dispatched for `mouse`, `pen`, `touch`, and `keyboard` @@ -18,15 +18,18 @@ const TextField = (props) => ( ); ``` +## Types + ```js -// Types type FocusEvent = { target: Element, type: 'blur' | 'focus' | 'focuschange' } ``` -### disabled: boolean +## Props + +### disabled: boolean = false Disables all `Focus` events. diff --git a/packages/react-events/docs/FocusScope.md b/packages/react-events/docs/FocusScope.md index e69de29bb2d1d..996a4e3de5e47 100644 --- a/packages/react-events/docs/FocusScope.md +++ b/packages/react-events/docs/FocusScope.md @@ -0,0 +1,37 @@ +# FocusScope + +The `FocusScope` module can be used to manage focus within its subtree. + +```js +// Example +const Modal = () => ( + +

Focus contained within modal

+ +
Focusable element
+ + +
Close
+
+
+); +``` + +## Props + +### autoFocus: boolean = false + +Automatically moves focus to the first focusable element within scope. + +### contain: boolean = false + +Contain focus within the subtree of the `FocusScope` instance. + +### restoreFocus: boolean = false + +Automatically restores focus to element that was last focused before focus moved +within the scope. diff --git a/packages/react-events/docs/Hover.md b/packages/react-events/docs/Hover.md index 7101419344d86..1f68edf11bb28 100644 --- a/packages/react-events/docs/Hover.md +++ b/packages/react-events/docs/Hover.md @@ -1,8 +1,8 @@ -## Hover +# Hover The `Hover` module responds to hover events on the element it wraps. Hover -events are only dispatched for `mouse` pointer types. Hover begins when the -pointer enters the element's bounds and ends when the pointer leaves. +events are only dispatched for `mouse` and `pen` pointer types. Hover begins +when the pointer enters the element's bounds and ends when the pointer leaves. Hover events do not propagate between `Hover` event responders. @@ -25,8 +25,9 @@ const Link = (props) => ( ); ``` +## Types + ```js -// Types type HoverEvent = { pointerType: 'mouse' | 'pen', target: Element, @@ -34,6 +35,8 @@ type HoverEvent = { } ``` +## Props + ### delayHoverEnd: number The duration of the delay between when hover ends and when `onHoverEnd` is diff --git a/packages/react-events/docs/Press.md b/packages/react-events/docs/Press.md index 20330bf7b6e6e..de40a1f12a236 100644 --- a/packages/react-events/docs/Press.md +++ b/packages/react-events/docs/Press.md @@ -1,4 +1,4 @@ -## Press +# Press The `Press` module responds to press events on the element it wraps. Press events are dispatched for `mouse`, `pen`, `touch`, and `keyboard` pointer types. @@ -33,8 +33,9 @@ const Button = (props) => ( ); ``` +## Types + ```js -// Types type PressEvent = { pointerType: 'mouse' | 'touch' | 'pen' | 'keyboard', target: Element, @@ -42,13 +43,15 @@ type PressEvent = { } type PressOffset = { - top: number, - right: number, - bottom: number, - right: number + top?: number, + right?: number, + bottom?: number, + right?: number }; ``` +## Props + ### delayLongPress: number = 500ms 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 called. This delay is cut short (and `onPressStart` is called) if the press is released before the threshold is exceeded. -### disabled: boolean +### disabled: boolean = false Disables all `Press` events. @@ -118,7 +121,7 @@ Called once the element is pressed down. If the press is released before the Defines how far the pointer (while held down) may move outside the bounds of the element before it is deactivated. Ensure you pass in a constant to reduce memory -allocations. +allocations. Default is `20` for each offset. ### preventDefault: boolean = true diff --git a/packages/react-events/src/FocusScope.js b/packages/react-events/src/FocusScope.js index ba1747cfdcfaa..c31e11d8f8e52 100644 --- a/packages/react-events/src/FocusScope.js +++ b/packages/react-events/src/FocusScope.js @@ -15,8 +15,8 @@ import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; type FocusScopeProps = { autoFocus: Boolean, + contain: Boolean, restoreFocus: Boolean, - trap: Boolean, }; type FocusScopeState = { @@ -27,14 +27,21 @@ type FocusScopeState = { const targetEventTypes = [{name: 'keydown', passive: false}]; const rootEventTypes = [{name: 'focus', passive: true, capture: true}]; -function focusFirstChildEventTarget( +function focusElement(element: ?HTMLElement) { + if (element != null) { + try { + element.focus(); + } catch (err) {} + } +} + +function getFirstFocusableElement( context: ReactResponderContext, state: FocusScopeState, -): void { +): ?HTMLElement { const elements = context.getFocusableElementsInScope(); if (elements.length > 0) { - const firstElement = elements[0]; - firstElement.focus(); + return elements[0]; } } @@ -78,7 +85,7 @@ const FocusScopeResponder = { if (shiftKey) { if (position === 0) { - if (props.trap) { + if (props.contain) { nextElement = elements[lastPosition]; } else { // Out of bounds @@ -90,7 +97,7 @@ const FocusScopeResponder = { } } else { if (position === lastPosition) { - if (props.trap) { + if (props.contain) { nextElement = elements[0]; } else { // Out of bounds @@ -107,7 +114,7 @@ const FocusScopeResponder = { if (!context.isTargetWithinEventResponderScope(nextElement)) { context.releaseOwnership(); } - nextElement.focus(); + focusElement(nextElement); state.currentFocusedNode = nextElement; ((nativeEvent: any): KeyboardEvent).preventDefault(); } @@ -122,14 +129,15 @@ const FocusScopeResponder = { ) { const {target} = event; - // Handle global trapping - if (props.trap) { + // Handle global focus containment + if (props.contain) { if (!context.isTargetWithinEventComponent(target)) { const currentFocusedNode = state.currentFocusedNode; if (currentFocusedNode !== null) { - currentFocusedNode.focus(); + focusElement(currentFocusedNode); } else if (props.autoFocus) { - focusFirstChildEventTarget(context, state); + const firstElement = getFirstFocusableElement(context, state); + focusElement(firstElement); } } } @@ -143,7 +151,8 @@ const FocusScopeResponder = { state.nodeToRestore = context.getActiveDocument().activeElement; } if (props.autoFocus) { - focusFirstChildEventTarget(context, state); + const firstElement = getFirstFocusableElement(context, state); + focusElement(firstElement); } }, onUnmount( @@ -156,7 +165,7 @@ const FocusScopeResponder = { state.nodeToRestore !== null && context.hasOwnership() ) { - state.nodeToRestore.focus(); + focusElement(state.nodeToRestore); } }, onOwnershipChange( diff --git a/packages/react-events/src/__tests__/FocusScope-test.internal.js b/packages/react-events/src/__tests__/FocusScope-test.internal.js index 673246269ab07..2194422c3b0e7 100644 --- a/packages/react-events/src/__tests__/FocusScope-test.internal.js +++ b/packages/react-events/src/__tests__/FocusScope-test.internal.js @@ -85,7 +85,7 @@ describe('FocusScope event responder', () => { expect(document.activeElement).toBe(divRef.current); }); - it('should work as expected with autofocus and trapping', () => { + it('should work as expected with autoFocus and contain', () => { const inputRef = React.createRef(); const input2Ref = React.createRef(); const buttonRef = React.createRef(); @@ -93,7 +93,7 @@ describe('FocusScope event responder', () => { const SimpleFocusScope = () => (
- +