mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[react-interactions] TabFocus -> FocusManager (#16874)
This commit is contained in:
@@ -9,4 +9,4 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./src/TabFocus');
|
||||
module.exports = require('./src/FocusManager');
|
||||
114
packages/react-interactions/accessibility/src/FocusManager.js
vendored
Normal file
114
packages/react-interactions/accessibility/src/FocusManager.js
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactScope} from 'shared/ReactTypes';
|
||||
import type {KeyboardEvent} from 'react-interactions/events/keyboard';
|
||||
|
||||
import React from 'react';
|
||||
import {useKeyboard} from 'react-interactions/events/keyboard';
|
||||
import {useFocusWithin} from 'react-interactions/events/focus';
|
||||
import {
|
||||
focusFirst,
|
||||
focusPrevious,
|
||||
focusNext,
|
||||
} from 'react-interactions/accessibility/focus-control';
|
||||
import TabbableScope from 'react-interactions/accessibility/tabbable-scope';
|
||||
|
||||
type TabFocusProps = {|
|
||||
autoFocus?: boolean,
|
||||
children: React.Node,
|
||||
containFocus?: boolean,
|
||||
restoreFocus?: boolean,
|
||||
scope: ReactScope,
|
||||
|};
|
||||
|
||||
const {useLayoutEffect, useRef} = React;
|
||||
|
||||
const FocusManager = React.forwardRef(
|
||||
(
|
||||
{
|
||||
autoFocus,
|
||||
children,
|
||||
containFocus,
|
||||
restoreFocus,
|
||||
scope: CustomScope,
|
||||
}: TabFocusProps,
|
||||
ref,
|
||||
): React.Node => {
|
||||
const ScopeToUse = CustomScope || TabbableScope;
|
||||
const scopeRef = useRef(null);
|
||||
// This ensures tabbing works through the React tree (including Portals and Suspense nodes)
|
||||
const keyboard = useKeyboard({
|
||||
onKeyDown(event: KeyboardEvent): void {
|
||||
if (event.key !== 'Tab') {
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
const scope = scopeRef.current;
|
||||
if (scope !== null) {
|
||||
if (event.shiftKey) {
|
||||
focusPrevious(scope, event, containFocus);
|
||||
} else {
|
||||
focusNext(scope, event, containFocus);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
const focusWithin = useFocusWithin({
|
||||
onBlurWithin: function(event) {
|
||||
if (!containFocus) {
|
||||
event.continuePropagation();
|
||||
}
|
||||
const lastNode = event.target;
|
||||
if (lastNode) {
|
||||
requestAnimationFrame(() => {
|
||||
(lastNode: any).focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
useLayoutEffect(
|
||||
() => {
|
||||
const scope = scopeRef.current;
|
||||
let restoreElem;
|
||||
if (restoreFocus) {
|
||||
restoreElem = document.activeElement;
|
||||
}
|
||||
if (autoFocus && scope !== null) {
|
||||
focusFirst(scope);
|
||||
}
|
||||
if (restoreElem) {
|
||||
return () => {
|
||||
(restoreElem: any).focus();
|
||||
};
|
||||
}
|
||||
},
|
||||
[scopeRef],
|
||||
);
|
||||
|
||||
return (
|
||||
<ScopeToUse
|
||||
ref={node => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(node);
|
||||
} else {
|
||||
ref.current = node;
|
||||
}
|
||||
}
|
||||
scopeRef.current = node;
|
||||
}}
|
||||
listeners={[keyboard, focusWithin]}>
|
||||
{children}
|
||||
</ScopeToUse>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default FocusManager;
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactScope} from 'shared/ReactTypes';
|
||||
import type {KeyboardEvent} from 'react-interactions/events/keyboard';
|
||||
|
||||
import React from 'react';
|
||||
import {useKeyboard} from 'react-interactions/events/keyboard';
|
||||
import {
|
||||
focusPrevious,
|
||||
focusNext,
|
||||
} from 'react-interactions/accessibility/focus-control';
|
||||
|
||||
type TabFocusProps = {
|
||||
children: React.Node,
|
||||
contain?: boolean,
|
||||
scope: ReactScope,
|
||||
};
|
||||
|
||||
const {useRef} = React;
|
||||
|
||||
const TabFocus = React.forwardRef(
|
||||
({children, contain, scope: Scope}: TabFocusProps, ref): React.Node => {
|
||||
const scopeRef = useRef(null);
|
||||
const keyboard = useKeyboard({
|
||||
onKeyDown(event: KeyboardEvent): void {
|
||||
if (event.key !== 'Tab') {
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
const scope = scopeRef.current;
|
||||
if (scope !== null) {
|
||||
if (event.shiftKey) {
|
||||
focusPrevious(scope, event, contain);
|
||||
} else {
|
||||
focusNext(scope, event, contain);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Scope
|
||||
ref={node => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(node);
|
||||
} else {
|
||||
ref.current = node;
|
||||
}
|
||||
}
|
||||
scopeRef.current = node;
|
||||
}}
|
||||
listeners={keyboard}>
|
||||
{children}
|
||||
</Scope>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default TabFocus;
|
||||
@@ -11,18 +11,16 @@ import {createEventTarget} from 'react-interactions/events/src/dom/testing-libra
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let TabFocus;
|
||||
let TabbableScope;
|
||||
let FocusManager;
|
||||
let FocusControl;
|
||||
|
||||
describe('TabFocusController', () => {
|
||||
describe('FocusManager', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableScopeAPI = true;
|
||||
ReactFeatureFlags.enableFlareAPI = true;
|
||||
TabFocus = require('../TabFocus').default;
|
||||
TabbableScope = require('../TabbableScope').default;
|
||||
FocusManager = require('../FocusManager').default;
|
||||
FocusControl = require('../FocusControl');
|
||||
React = require('react');
|
||||
});
|
||||
@@ -42,21 +40,21 @@ describe('TabFocusController', () => {
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('handles tab operations', () => {
|
||||
it('handles tab operations by default', () => {
|
||||
const inputRef = React.createRef();
|
||||
const input2Ref = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
const butto2nRef = React.createRef();
|
||||
const button2Ref = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
|
||||
const Test = () => (
|
||||
<TabFocus scope={TabbableScope}>
|
||||
<FocusManager>
|
||||
<input ref={inputRef} />
|
||||
<button ref={buttonRef} />
|
||||
<div ref={divRef} tabIndex={0} />
|
||||
<input ref={input2Ref} tabIndex={-1} />
|
||||
<button ref={butto2nRef} />
|
||||
</TabFocus>
|
||||
<button ref={button2Ref} />
|
||||
</FocusManager>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
@@ -66,24 +64,67 @@ describe('TabFocusController', () => {
|
||||
createEventTarget(document.activeElement).tabNext();
|
||||
expect(document.activeElement).toBe(divRef.current);
|
||||
createEventTarget(document.activeElement).tabNext();
|
||||
expect(document.activeElement).toBe(butto2nRef.current);
|
||||
expect(document.activeElement).toBe(button2Ref.current);
|
||||
createEventTarget(document.activeElement).tabPrevious();
|
||||
expect(document.activeElement).toBe(divRef.current);
|
||||
});
|
||||
|
||||
it('handles tab operations with containment', () => {
|
||||
it('handles autoFocus', () => {
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
const Test = () => (
|
||||
<FocusManager autoFocus={true}>
|
||||
<input tabIndex={-1} />
|
||||
<button ref={buttonRef} />
|
||||
</FocusManager>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
expect(document.activeElement).toBe(buttonRef.current);
|
||||
});
|
||||
|
||||
it('handles restoreFocus', () => {
|
||||
const difRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
const Test = ({flag}) => {
|
||||
return (
|
||||
<div ref={difRef} tabIndex={0}>
|
||||
{flag ? (
|
||||
<FocusManager autoFocus={true} restoreFocus={true}>
|
||||
<button ref={buttonRef} />
|
||||
</FocusManager>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Test flag={false} />, container);
|
||||
difRef.current.focus();
|
||||
expect(document.activeElement).toBe(difRef.current);
|
||||
ReactDOM.render(<Test flag={true} />, container);
|
||||
expect(document.activeElement).toBe(buttonRef.current);
|
||||
ReactDOM.render(<Test flag={false} />, container);
|
||||
expect(document.activeElement).toBe(difRef.current);
|
||||
});
|
||||
|
||||
it('handles containFocus', () => {
|
||||
const inputRef = React.createRef();
|
||||
const input2Ref = React.createRef();
|
||||
const input3Ref = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
const button2Ref = React.createRef();
|
||||
|
||||
const Test = () => (
|
||||
<TabFocus scope={TabbableScope} contain={true}>
|
||||
<input ref={inputRef} tabIndex={-1} />
|
||||
<button ref={buttonRef} id={1} />
|
||||
<button ref={button2Ref} id={2} />
|
||||
<input ref={input2Ref} tabIndex={-1} />
|
||||
</TabFocus>
|
||||
<div>
|
||||
<FocusManager containFocus={true}>
|
||||
<input ref={inputRef} tabIndex={-1} />
|
||||
<button ref={buttonRef} id={1} />
|
||||
<button ref={button2Ref} id={2} />
|
||||
<input ref={input2Ref} tabIndex={-1} />
|
||||
</FocusManager>
|
||||
<input ref={input3Ref} />
|
||||
</div>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
@@ -98,9 +139,16 @@ describe('TabFocusController', () => {
|
||||
expect(document.activeElement).toBe(buttonRef.current);
|
||||
createEventTarget(document.activeElement).tabPrevious();
|
||||
expect(document.activeElement).toBe(button2Ref.current);
|
||||
// Focus should be restored to the contained area
|
||||
const rAF = window.requestAnimationFrame;
|
||||
window.requestAnimationFrame = x => setTimeout(x);
|
||||
input3Ref.current.focus();
|
||||
jest.advanceTimersByTime(1);
|
||||
window.requestAnimationFrame = rAF;
|
||||
expect(document.activeElement).toBe(button2Ref.current);
|
||||
});
|
||||
|
||||
it('handles tab operations when controllers are nested', () => {
|
||||
it('works with nested FocusManagers', () => {
|
||||
const inputRef = React.createRef();
|
||||
const input2Ref = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
@@ -109,16 +157,16 @@ describe('TabFocusController', () => {
|
||||
const button4Ref = React.createRef();
|
||||
|
||||
const Test = () => (
|
||||
<TabFocus scope={TabbableScope}>
|
||||
<FocusManager>
|
||||
<input ref={inputRef} tabIndex={-1} />
|
||||
<button ref={buttonRef} id={1} />
|
||||
<TabFocus scope={TabbableScope}>
|
||||
<FocusManager>
|
||||
<button ref={button2Ref} id={2} />
|
||||
<button ref={button3Ref} id={3} />
|
||||
</TabFocus>
|
||||
</FocusManager>
|
||||
<input ref={input2Ref} tabIndex={-1} />
|
||||
<button ref={button4Ref} id={4} />
|
||||
</TabFocus>
|
||||
</FocusManager>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
@@ -136,7 +184,7 @@ describe('TabFocusController', () => {
|
||||
expect(document.activeElement).toBe(button2Ref.current);
|
||||
});
|
||||
|
||||
it('handles tab operations when controllers are nested with containment', () => {
|
||||
it('handles containFocus (nested FocusManagers)', () => {
|
||||
const inputRef = React.createRef();
|
||||
const input2Ref = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
@@ -145,16 +193,16 @@ describe('TabFocusController', () => {
|
||||
const button4Ref = React.createRef();
|
||||
|
||||
const Test = () => (
|
||||
<TabFocus scope={TabbableScope}>
|
||||
<FocusManager>
|
||||
<input ref={inputRef} tabIndex={-1} />
|
||||
<button ref={buttonRef} id={1} />
|
||||
<TabFocus contain={true} scope={TabbableScope}>
|
||||
<FocusManager containFocus={true}>
|
||||
<button ref={button2Ref} id={2} />
|
||||
<button ref={button3Ref} id={3} />
|
||||
</TabFocus>
|
||||
</FocusManager>
|
||||
<input ref={input2Ref} tabIndex={-1} />
|
||||
<button ref={button4Ref} id={4} />
|
||||
</TabFocus>
|
||||
</FocusManager>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
@@ -195,14 +243,14 @@ describe('TabFocusController', () => {
|
||||
}
|
||||
|
||||
const Test = () => (
|
||||
<TabFocus scope={TabbableScope}>
|
||||
<FocusManager>
|
||||
<button ref={buttonRef} id={1} />
|
||||
<button ref={button2Ref} id={2} />
|
||||
<React.Suspense fallback={<button ref={button3Ref} id={3} />}>
|
||||
<Component />
|
||||
</React.Suspense>
|
||||
<button ref={button4Ref} id={4} />
|
||||
</TabFocus>
|
||||
</FocusManager>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
@@ -220,7 +268,7 @@ describe('TabFocusController', () => {
|
||||
expect(document.activeElement).toBe(button2Ref.current);
|
||||
});
|
||||
|
||||
it('allows for imperative tab focus control', () => {
|
||||
it('allows for imperative tab focus control using FocusControl', () => {
|
||||
const firstFocusControllerRef = React.createRef();
|
||||
const secondFocusControllerRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
@@ -229,16 +277,16 @@ describe('TabFocusController', () => {
|
||||
|
||||
const Test = () => (
|
||||
<div>
|
||||
<TabFocus ref={firstFocusControllerRef} scope={TabbableScope}>
|
||||
<FocusManager ref={firstFocusControllerRef}>
|
||||
<input tabIndex={-1} />
|
||||
<button ref={buttonRef} />
|
||||
<button ref={button2Ref} />
|
||||
<input tabIndex={-1} />
|
||||
</TabFocus>
|
||||
<TabFocus ref={secondFocusControllerRef} scope={TabbableScope}>
|
||||
</FocusManager>
|
||||
<FocusManager ref={secondFocusControllerRef}>
|
||||
<input tabIndex={-1} />
|
||||
<div ref={divRef} tabIndex={0} />
|
||||
</TabFocus>
|
||||
</FocusManager>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ type FocusEvent = {|
|
||||
type: FocusEventType | FocusWithinEventType,
|
||||
pointerType: PointerType,
|
||||
timeStamp: number,
|
||||
continuePropagation: () => void,
|
||||
|};
|
||||
|
||||
type FocusState = {
|
||||
@@ -47,12 +48,16 @@ type FocusProps = {
|
||||
type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
|
||||
|
||||
type FocusWithinProps = {
|
||||
disabled: boolean,
|
||||
onFocusWithinChange: boolean => void,
|
||||
onFocusWithinVisibleChange: boolean => void,
|
||||
disabled?: boolean,
|
||||
onBlurWithin?: (e: FocusEvent) => void,
|
||||
onFocusWithinChange?: boolean => void,
|
||||
onFocusWithinVisibleChange?: boolean => void,
|
||||
};
|
||||
|
||||
type FocusWithinEventType = 'focuswithinvisiblechange' | 'focuswithinchange';
|
||||
type FocusWithinEventType =
|
||||
| 'focuswithinvisiblechange'
|
||||
| 'focuswithinchange'
|
||||
| 'blurwithin';
|
||||
|
||||
/**
|
||||
* Shared between Focus and FocusWithin
|
||||
@@ -89,6 +94,14 @@ function createFocusEvent(
|
||||
type,
|
||||
pointerType,
|
||||
timeStamp: context.getTimeStamp(),
|
||||
// We don't use stopPropagation, as the default behavior
|
||||
// is to not propagate. Plus, there might be confusion
|
||||
// using stopPropagation as we don't actually stop
|
||||
// native propagation from working, but instead only
|
||||
// allow propagation to the others keyboard responders.
|
||||
continuePropagation() {
|
||||
context.continuePropagation();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -226,6 +239,26 @@ function dispatchBlurEvents(
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchBlurWithinEvents(
|
||||
context: ReactDOMResponderContext,
|
||||
event: ReactDOMResponderEvent,
|
||||
props: FocusWithinProps,
|
||||
state: FocusState,
|
||||
) {
|
||||
const pointerType = state.pointerType;
|
||||
const target = ((state.focusTarget: any): Element | Document) || event.target;
|
||||
const onBlurWithin = (props.onBlurWithin: any);
|
||||
if (isFunction(onBlurWithin)) {
|
||||
const syntheticEvent = createFocusEvent(
|
||||
context,
|
||||
'blurwithin',
|
||||
target,
|
||||
pointerType,
|
||||
);
|
||||
context.dispatchEvent(syntheticEvent, onBlurWithin, DiscreteEvent);
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchFocusChange(
|
||||
context: ReactDOMResponderContext,
|
||||
props: FocusProps,
|
||||
@@ -364,7 +397,7 @@ function dispatchFocusWithinChangeEvent(
|
||||
state: FocusState,
|
||||
value: boolean,
|
||||
) {
|
||||
const onFocusWithinChange = props.onFocusWithinChange;
|
||||
const onFocusWithinChange = (props.onFocusWithinChange: any);
|
||||
if (isFunction(onFocusWithinChange)) {
|
||||
context.dispatchEvent(value, onFocusWithinChange, DiscreteEvent);
|
||||
}
|
||||
@@ -379,7 +412,7 @@ function dispatchFocusWithinVisibleChangeEvent(
|
||||
state: FocusState,
|
||||
value: boolean,
|
||||
) {
|
||||
const onFocusWithinVisibleChange = props.onFocusWithinVisibleChange;
|
||||
const onFocusWithinVisibleChange = (props.onFocusWithinVisibleChange: any);
|
||||
if (isFunction(onFocusWithinVisibleChange)) {
|
||||
context.dispatchEvent(value, onFocusWithinVisibleChange, DiscreteEvent);
|
||||
}
|
||||
@@ -447,6 +480,7 @@ const focusWithinResponderImpl = {
|
||||
!context.isTargetWithinResponder(relatedTarget)
|
||||
) {
|
||||
dispatchFocusWithinChangeEvent(context, props, state, false);
|
||||
dispatchBlurWithinEvents(context, event, props, state);
|
||||
state.isFocused = false;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -681,11 +681,12 @@ const bundles = [
|
||||
{
|
||||
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
|
||||
moduleType: NON_FIBER_RENDERER,
|
||||
entry: 'react-interactions/accessibility/tab-focus',
|
||||
global: 'ReactTabFocus',
|
||||
entry: 'react-interactions/accessibility/focus-manager',
|
||||
global: 'ReactFocusManager',
|
||||
externals: [
|
||||
'react',
|
||||
'react-interactions/events/keyboard',
|
||||
'react-interactions/events/focus',
|
||||
'react-interactions/accessibility/tabbable-scope',
|
||||
'react-interactions/accessibility/focus-control',
|
||||
],
|
||||
@@ -721,6 +722,7 @@ const bundles = [
|
||||
];
|
||||
|
||||
const fbBundleExternalsMap = {
|
||||
'react-interactions/events/focus': 'ReactEventsFocus',
|
||||
'react-interactions/events/keyboard': 'ReactEventsKeyboard',
|
||||
'react-interactions/events/tap': 'ReactEventsTap',
|
||||
'react-interactions/accessibility/tabbable-scope': 'ReactTabbableScope',
|
||||
|
||||
Reference in New Issue
Block a user