mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[react-interactions] Add tab handling to FocusList (#16958)
This commit is contained in:
+45
-9
@@ -12,6 +12,7 @@ import type {KeyboardEvent} from 'react-interactions/events/keyboard';
|
||||
|
||||
import React from 'react';
|
||||
import {useKeyboard} from 'react-interactions/events/keyboard';
|
||||
import {setElementCanTab} from 'react-interactions/accessibility/focus-control';
|
||||
|
||||
type FocusItemProps = {
|
||||
children?: React.Node,
|
||||
@@ -22,6 +23,7 @@ type FocusListProps = {|
|
||||
children: React.Node,
|
||||
portrait: boolean,
|
||||
wrap?: boolean,
|
||||
tabScope?: ReactScope,
|
||||
|};
|
||||
|
||||
const {useRef} = React;
|
||||
@@ -41,7 +43,7 @@ function getPreviousListItem(
|
||||
const items = list.getChildren();
|
||||
if (items !== null) {
|
||||
const currentItemIndex = items.indexOf(currentItem);
|
||||
const wrap = getListWrapProp(currentItem);
|
||||
const wrap = getListProps(currentItem).wrap;
|
||||
if (currentItemIndex === 0 && wrap) {
|
||||
return items[items.length - 1] || null;
|
||||
} else if (currentItemIndex > 0) {
|
||||
@@ -58,7 +60,7 @@ function getNextListItem(
|
||||
const items = list.getChildren();
|
||||
if (items !== null) {
|
||||
const currentItemIndex = items.indexOf(currentItem);
|
||||
const wrap = getListWrapProp(currentItem);
|
||||
const wrap = getListProps(currentItem).wrap;
|
||||
const end = currentItemIndex === items.length - 1;
|
||||
if (end && wrap) {
|
||||
return items[0] || null;
|
||||
@@ -69,22 +71,38 @@ function getNextListItem(
|
||||
return null;
|
||||
}
|
||||
|
||||
function getListWrapProp(currentItem: ReactScopeMethods): boolean {
|
||||
const list = currentItem.getParent();
|
||||
function getListProps(currentCell: ReactScopeMethods): Object {
|
||||
const list = currentCell.getParent();
|
||||
if (list !== null) {
|
||||
const listProps = list.getProps();
|
||||
return (listProps.type === 'list' && listProps.wrap) || false;
|
||||
if (listProps && listProps.type === 'list') {
|
||||
return listProps;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return {};
|
||||
}
|
||||
|
||||
export function createFocusList(scope: ReactScope): Array<React.Component> {
|
||||
const TableScope = React.unstable_createScope(scope.fn);
|
||||
|
||||
function List({children, portrait, wrap}): FocusListProps {
|
||||
function List({
|
||||
children,
|
||||
portrait,
|
||||
wrap,
|
||||
tabScope: TabScope,
|
||||
}): FocusListProps {
|
||||
const tabScopeRef = useRef(null);
|
||||
return (
|
||||
<TableScope type="list" portrait={portrait} wrap={wrap}>
|
||||
{children}
|
||||
<TableScope
|
||||
type="list"
|
||||
portrait={portrait}
|
||||
wrap={wrap}
|
||||
tabScopeRef={tabScopeRef}>
|
||||
{TabScope ? (
|
||||
<TabScope ref={tabScopeRef}>{children}</TabScope>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</TableScope>
|
||||
);
|
||||
}
|
||||
@@ -100,6 +118,24 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
|
||||
if (list !== null && listProps.type === 'list') {
|
||||
const portrait = listProps.portrait;
|
||||
switch (event.key) {
|
||||
case 'Tab': {
|
||||
const tabScope = getListProps(currentItem).tabScopeRef.current;
|
||||
if (tabScope) {
|
||||
const activeNode = document.activeElement;
|
||||
const nodes = tabScope.getScopedNodes();
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (node !== activeNode) {
|
||||
setElementCanTab(node, false);
|
||||
} else {
|
||||
setElementCanTab(node, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
if (portrait) {
|
||||
const previousListItem = getPreviousListItem(
|
||||
|
||||
@@ -64,6 +64,7 @@ const FocusManager = React.forwardRef(
|
||||
onBlurWithin: function(event) {
|
||||
if (!containFocus) {
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
const lastNode = event.target;
|
||||
if (lastNode) {
|
||||
|
||||
@@ -31,6 +31,7 @@ type FocusTableProps = {|
|
||||
focusTableByID: (id: string) => void,
|
||||
) => void,
|
||||
wrap?: boolean,
|
||||
tabScope?: ReactScope,
|
||||
|};
|
||||
|
||||
const {useRef} = React;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
|
||||
import {
|
||||
createEventTarget,
|
||||
emulateBrowserTab,
|
||||
} from 'react-interactions/events/src/dom/testing-library';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
@@ -156,5 +159,74 @@ describe('FocusList', () => {
|
||||
});
|
||||
expect(document.activeElement.textContent).toBe('Item 3');
|
||||
});
|
||||
|
||||
it('handles keyboard arrow operations mixed with tabbing', () => {
|
||||
const [FocusList, FocusItem] = createFocusList(TabbableScope);
|
||||
const beforeRef = React.createRef();
|
||||
const afterRef = React.createRef();
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<>
|
||||
<input placeholder="Before" ref={beforeRef} />
|
||||
<FocusList tabScope={TabbableScope} portrait={true}>
|
||||
<ul>
|
||||
<FocusItem>
|
||||
<li>
|
||||
<input placeholder="A" />
|
||||
</li>
|
||||
</FocusItem>
|
||||
<FocusItem>
|
||||
<li>
|
||||
<input placeholder="B" />
|
||||
</li>
|
||||
</FocusItem>
|
||||
<FocusItem>
|
||||
<li>
|
||||
<input placeholder="C" />
|
||||
</li>
|
||||
</FocusItem>
|
||||
<FocusItem>
|
||||
<li>
|
||||
<input placeholder="D" />
|
||||
</li>
|
||||
</FocusItem>
|
||||
<FocusItem>
|
||||
<li>
|
||||
<input placeholder="E" />
|
||||
</li>
|
||||
</FocusItem>
|
||||
<FocusItem>
|
||||
<li>
|
||||
<input placeholder="F" />
|
||||
</li>
|
||||
</FocusItem>
|
||||
</ul>
|
||||
</FocusList>
|
||||
<input placeholder="After" ref={afterRef} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
beforeRef.current.focus();
|
||||
|
||||
expect(document.activeElement.placeholder).toBe('Before');
|
||||
emulateBrowserTab();
|
||||
expect(document.activeElement.placeholder).toBe('A');
|
||||
emulateBrowserTab();
|
||||
expect(document.activeElement.placeholder).toBe('After');
|
||||
emulateBrowserTab(true);
|
||||
expect(document.activeElement.placeholder).toBe('A');
|
||||
const a = createEventTarget(document.activeElement);
|
||||
a.keydown({
|
||||
key: 'ArrowDown',
|
||||
});
|
||||
expect(document.activeElement.placeholder).toBe('B');
|
||||
emulateBrowserTab();
|
||||
expect(document.activeElement.placeholder).toBe('After');
|
||||
emulateBrowserTab(true);
|
||||
expect(document.activeElement.placeholder).toBe('B');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
-28
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
|
||||
import {
|
||||
createEventTarget,
|
||||
emulateBrowserTab,
|
||||
} from 'react-interactions/events/src/dom/testing-library';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
@@ -29,33 +32,6 @@ describe('FocusTable', () => {
|
||||
let ReactDOM;
|
||||
let container;
|
||||
|
||||
function emulateBrowserTab(backwards) {
|
||||
const activeElement = document.activeElement;
|
||||
const focusedElem = createEventTarget(activeElement);
|
||||
let defaultPrevented = false;
|
||||
focusedElem.keydown({
|
||||
key: 'Tab',
|
||||
shiftKey: backwards,
|
||||
preventDefault() {
|
||||
defaultPrevented = true;
|
||||
},
|
||||
});
|
||||
if (!defaultPrevented) {
|
||||
// This is not a full spec compliant version, but should be suffice for this test
|
||||
const focusableElems = Array.from(
|
||||
document.querySelectorAll(
|
||||
'input, button, select, textarea, a[href], [tabindex], [contenteditable], iframe, object, embed',
|
||||
),
|
||||
).filter(
|
||||
elem => elem.tabIndex > -1 && !elem.disabled && !elem.contentEditable,
|
||||
);
|
||||
const idx = focusableElems.indexOf(activeElement);
|
||||
if (idx !== -1) {
|
||||
focusableElems[backwards ? idx - 1 : idx + 1].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
ReactDOM = require('react-dom');
|
||||
container = document.createElement('div');
|
||||
|
||||
@@ -158,6 +158,33 @@ function testWithPointerType(message, testFn) {
|
||||
});
|
||||
}
|
||||
|
||||
function emulateBrowserTab(backwards) {
|
||||
const activeElement = document.activeElement;
|
||||
const focusedElem = createEventTarget(activeElement);
|
||||
let defaultPrevented = false;
|
||||
focusedElem.keydown({
|
||||
key: 'Tab',
|
||||
shiftKey: backwards,
|
||||
preventDefault() {
|
||||
defaultPrevented = true;
|
||||
},
|
||||
});
|
||||
if (!defaultPrevented) {
|
||||
// This is not a full spec compliant version, but should be suffice for this test
|
||||
const focusableElems = Array.from(
|
||||
document.querySelectorAll(
|
||||
'input, button, select, textarea, a[href], [tabindex], [contenteditable], iframe, object, embed',
|
||||
),
|
||||
).filter(
|
||||
elem => elem.tabIndex > -1 && !elem.disabled && !elem.contentEditable,
|
||||
);
|
||||
const idx = focusableElems.indexOf(activeElement);
|
||||
if (idx !== -1) {
|
||||
focusableElems[backwards ? idx - 1 : idx + 1].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
buttonsType,
|
||||
createEventTarget,
|
||||
@@ -166,4 +193,5 @@ export {
|
||||
hasPointerEvent,
|
||||
setPointerEvent,
|
||||
testWithPointerType,
|
||||
emulateBrowserTab,
|
||||
};
|
||||
|
||||
@@ -718,6 +718,7 @@ const bundles = [
|
||||
'react',
|
||||
'react-interactions/events/keyboard',
|
||||
'react-interactions/accessibility/tabbable-scope',
|
||||
'react-interactions/accessibility/focus-control',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user