mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[react-interactions] Add allowModifiers flag to FocusList + FocusTable (#16971)
This commit is contained in:
+36
-15
@@ -24,6 +24,7 @@ type FocusListProps = {|
|
||||
portrait: boolean,
|
||||
wrap?: boolean,
|
||||
tabScope?: ReactScope,
|
||||
allowModifiers?: boolean,
|
||||
|};
|
||||
|
||||
const {useRef} = React;
|
||||
@@ -82,6 +83,13 @@ function getListProps(currentCell: ReactScopeMethods): Object {
|
||||
return {};
|
||||
}
|
||||
|
||||
function hasModifierKey(event: KeyboardEvent): boolean {
|
||||
const {altKey, ctrlKey, metaKey, shiftKey} = event;
|
||||
return (
|
||||
altKey === true || ctrlKey === true || metaKey === true || shiftKey === true
|
||||
);
|
||||
}
|
||||
|
||||
export function createFocusList(scope: ReactScope): Array<React.Component> {
|
||||
const TableScope = React.unstable_createScope(scope.fn);
|
||||
|
||||
@@ -90,6 +98,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
|
||||
portrait,
|
||||
wrap,
|
||||
tabScope: TabScope,
|
||||
allowModifiers,
|
||||
}): FocusListProps {
|
||||
const tabScopeRef = useRef(null);
|
||||
return (
|
||||
@@ -97,7 +106,8 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
|
||||
type="list"
|
||||
portrait={portrait}
|
||||
wrap={wrap}
|
||||
tabScopeRef={tabScopeRef}>
|
||||
tabScopeRef={tabScopeRef}
|
||||
allowModifiers={allowModifiers}>
|
||||
{TabScope ? (
|
||||
<TabScope ref={tabScopeRef}>{children}</TabScope>
|
||||
) : (
|
||||
@@ -117,25 +127,36 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
|
||||
const listProps = list && list.getProps();
|
||||
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);
|
||||
}
|
||||
const key = event.key;
|
||||
|
||||
if (key === '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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
// Using modifier keys with keyboard arrow events should be no-ops
|
||||
// unless an explicit allowModifiers flag is set on the FocusList.
|
||||
if (hasModifierKey(event)) {
|
||||
const allowModifiers = getListProps(currentItem).allowModifiers;
|
||||
if (!allowModifiers) {
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (key) {
|
||||
case 'ArrowUp': {
|
||||
if (portrait) {
|
||||
const previousListItem = getPreviousListItem(
|
||||
|
||||
+35
-15
@@ -32,6 +32,7 @@ type FocusTableProps = {|
|
||||
) => void,
|
||||
wrap?: boolean,
|
||||
tabScope?: ReactScope,
|
||||
allowModifiers?: boolean,
|
||||
|};
|
||||
|
||||
const {useRef} = React;
|
||||
@@ -152,6 +153,13 @@ function getTableProps(currentCell: ReactScopeMethods): Object {
|
||||
return {};
|
||||
}
|
||||
|
||||
function hasModifierKey(event: KeyboardEvent): boolean {
|
||||
const {altKey, ctrlKey, metaKey, shiftKey} = event;
|
||||
return (
|
||||
altKey === true || ctrlKey === true || metaKey === true || shiftKey === true
|
||||
);
|
||||
}
|
||||
|
||||
export function createFocusTable(scope: ReactScope): Array<React.Component> {
|
||||
const TableScope = React.unstable_createScope(scope.fn);
|
||||
|
||||
@@ -161,6 +169,7 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
|
||||
id,
|
||||
wrap,
|
||||
tabScope: TabScope,
|
||||
allowModifiers,
|
||||
}): FocusTableProps {
|
||||
const tabScopeRef = useRef(null);
|
||||
return (
|
||||
@@ -169,7 +178,8 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
|
||||
onKeyboardOut={onKeyboardOut}
|
||||
id={id}
|
||||
wrap={wrap}
|
||||
tabScopeRef={tabScopeRef}>
|
||||
tabScopeRef={tabScopeRef}
|
||||
allowModifiers={allowModifiers}>
|
||||
{TabScope ? (
|
||||
<TabScope ref={tabScopeRef}>{children}</TabScope>
|
||||
) : (
|
||||
@@ -192,25 +202,35 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case 'Tab': {
|
||||
const tabScope = getTableProps(currentCell).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);
|
||||
}
|
||||
const key = event.key;
|
||||
if (key === 'Tab') {
|
||||
const tabScope = getTableProps(currentCell).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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
// Using modifier keys with keyboard arrow events should be no-ops
|
||||
// unless an explicit allowModifiers flag is set on the FocusTable.
|
||||
if (hasModifierKey(event)) {
|
||||
const allowModifiers = getTableProps(currentCell).allowModifiers;
|
||||
if (!allowModifiers) {
|
||||
event.continuePropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (key) {
|
||||
case 'ArrowUp': {
|
||||
const [cells, cellIndex] = getRowCells(currentCell);
|
||||
if (cells !== null) {
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {
|
||||
createEventTarget,
|
||||
emulateBrowserTab,
|
||||
} from 'react-interactions/events/src/dom/testing-library';
|
||||
import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
|
||||
import {emulateBrowserTab} from '../emulateBrowserTab';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
@@ -46,8 +44,11 @@ describe('FocusList', () => {
|
||||
function createFocusListComponent() {
|
||||
const [FocusList, FocusItem] = createFocusList(TabbableScope);
|
||||
|
||||
return ({portrait, wrap}) => (
|
||||
<FocusList portrait={portrait} wrap={wrap}>
|
||||
return ({portrait, wrap, allowModifiers}) => (
|
||||
<FocusList
|
||||
portrait={portrait}
|
||||
wrap={wrap}
|
||||
allowModifiers={allowModifiers}>
|
||||
<ul>
|
||||
<FocusItem>
|
||||
<li tabIndex={0}>Item 1</li>
|
||||
@@ -94,6 +95,12 @@ describe('FocusList', () => {
|
||||
key: 'ArrowLeft',
|
||||
});
|
||||
expect(document.activeElement.textContent).toBe('Item 3');
|
||||
// Should be a no-op due to modifier
|
||||
thirdListItem.keydown({
|
||||
key: 'ArrowUp',
|
||||
altKey: true,
|
||||
});
|
||||
expect(document.activeElement.textContent).toBe('Item 3');
|
||||
});
|
||||
|
||||
it('handles keyboard arrow operations (landscape)', () => {
|
||||
@@ -160,6 +167,23 @@ describe('FocusList', () => {
|
||||
expect(document.activeElement.textContent).toBe('Item 3');
|
||||
});
|
||||
|
||||
it('handles keyboard arrow operations (portrait) with allowModifiers', () => {
|
||||
const Test = createFocusListComponent();
|
||||
|
||||
ReactDOM.render(
|
||||
<Test portrait={true} allowModifiers={true} />,
|
||||
container,
|
||||
);
|
||||
const listItems = document.querySelectorAll('li');
|
||||
let firstListItem = createEventTarget(listItems[0]);
|
||||
firstListItem.focus();
|
||||
firstListItem.keydown({
|
||||
key: 'ArrowDown',
|
||||
altKey: true,
|
||||
});
|
||||
expect(document.activeElement.textContent).toBe('Item 2');
|
||||
});
|
||||
|
||||
it('handles keyboard arrow operations mixed with tabbing', () => {
|
||||
const [FocusList, FocusItem] = createFocusList(TabbableScope);
|
||||
const beforeRef = React.createRef();
|
||||
|
||||
+31
-7
@@ -7,10 +7,8 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {
|
||||
createEventTarget,
|
||||
emulateBrowserTab,
|
||||
} from 'react-interactions/events/src/dom/testing-library';
|
||||
import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
|
||||
import {emulateBrowserTab} from '../emulateBrowserTab';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
@@ -48,8 +46,12 @@ describe('FocusTable', () => {
|
||||
TabbableScope,
|
||||
);
|
||||
|
||||
return ({onKeyboardOut, id, wrap}) => (
|
||||
<FocusTable onKeyboardOut={onKeyboardOut} id={id} wrap={wrap}>
|
||||
return ({onKeyboardOut, id, wrap, allowModifiers}) => (
|
||||
<FocusTable
|
||||
onKeyboardOut={onKeyboardOut}
|
||||
id={id}
|
||||
wrap={wrap}
|
||||
allowModifiers={allowModifiers}>
|
||||
<table>
|
||||
<tbody>
|
||||
<FocusTableRow>
|
||||
@@ -115,6 +117,20 @@ describe('FocusTable', () => {
|
||||
);
|
||||
}
|
||||
|
||||
it('handles keyboard arrow operations with allowModifiers', () => {
|
||||
const Test = createFocusTableComponent();
|
||||
|
||||
ReactDOM.render(<Test allowModifiers={true} />, container);
|
||||
const buttons = document.querySelectorAll('button');
|
||||
const a1 = createEventTarget(buttons[0]);
|
||||
a1.focus();
|
||||
a1.keydown({
|
||||
key: 'ArrowRight',
|
||||
altKey: true,
|
||||
});
|
||||
expect(document.activeElement.textContent).toBe('A2');
|
||||
});
|
||||
|
||||
it('handles keyboard arrow operations', () => {
|
||||
const Test = createFocusTableComponent();
|
||||
|
||||
@@ -133,7 +149,7 @@ describe('FocusTable', () => {
|
||||
});
|
||||
expect(document.activeElement.textContent).toBe('B2');
|
||||
|
||||
const b2 = createEventTarget(document.activeElement);
|
||||
let b2 = createEventTarget(document.activeElement);
|
||||
b2.keydown({
|
||||
key: 'ArrowLeft',
|
||||
});
|
||||
@@ -154,6 +170,14 @@ describe('FocusTable', () => {
|
||||
key: 'ArrowUp',
|
||||
});
|
||||
expect(document.activeElement.textContent).toBe('B1');
|
||||
// Should be a no-op due to modifier
|
||||
b2 = createEventTarget(document.activeElement);
|
||||
b2.keydown({
|
||||
key: 'ArrowUp',
|
||||
altKey: true,
|
||||
});
|
||||
b2 = createEventTarget(document.activeElement);
|
||||
expect(document.activeElement.textContent).toBe('B1');
|
||||
});
|
||||
|
||||
it('handles keyboard arrow operations between tables', () => {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
|
||||
|
||||
// This function is used by the a11y modules for testing
|
||||
export 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,33 +158,6 @@ 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,
|
||||
@@ -193,5 +166,4 @@ export {
|
||||
hasPointerEvent,
|
||||
setPointerEvent,
|
||||
testWithPointerType,
|
||||
emulateBrowserTab,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user