Skip to content

Commit 15876b2

Browse files
authored
chore(Table): Update keyboard interactions (#6171)
* only missing small fix to actions * fix build * only apply keyboard handling updates to tables with a tbody * apply roving tab index behavior to tree tables only * update snapshots
1 parent f2e5e4c commit 15876b2

File tree

12 files changed

+253
-35
lines changed

12 files changed

+253
-35
lines changed

packages/react-core/src/components/Dropdown/DropdownMenu.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,13 @@ export class DropdownMenu extends React.Component<DropdownMenuProps> {
9696
if (event.key === 'ArrowDown') {
9797
const firstFocusTargetCollection = refs.find(ref => ref && ref[0] && !ref[0].hasAttribute('disabled'));
9898
DropdownMenu.focusFirstRef(firstFocusTargetCollection);
99+
event.stopPropagation();
99100
} else if (event.key === 'ArrowUp') {
100101
const collectionLength = refs.length;
101102
const lastFocusTargetCollection = refs.slice(collectionLength - 1, collectionLength);
102103
const lastFocusTarget = lastFocusTargetCollection && lastFocusTargetCollection[0];
103104
DropdownMenu.focusFirstRef(lastFocusTarget);
105+
event.stopPropagation();
104106
}
105107
};
106108

packages/react-core/src/components/Dropdown/InternalDropdownItem.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,16 @@ export class InternalDropdownItem extends React.Component<InternalDropdownItemPr
120120
}
121121
if (event.key === 'ArrowUp') {
122122
this.props.context.keyHandler(this.props.index, innerIndex, KEYHANDLER_DIRECTION.UP);
123+
event.stopPropagation();
123124
} else if (event.key === 'ArrowDown') {
124125
this.props.context.keyHandler(this.props.index, innerIndex, KEYHANDLER_DIRECTION.DOWN);
126+
event.stopPropagation();
125127
} else if (event.key === 'ArrowRight') {
126128
this.props.context.keyHandler(this.props.index, innerIndex, KEYHANDLER_DIRECTION.RIGHT);
129+
event.stopPropagation();
127130
} else if (event.key === 'ArrowLeft') {
128131
this.props.context.keyHandler(this.props.index, innerIndex, KEYHANDLER_DIRECTION.LEFT);
132+
event.stopPropagation();
129133
} else if (event.key === 'Enter' || event.key === ' ') {
130134
event.target.click();
131135
this.props.enterTriggersArrowDown &&

packages/react-core/src/components/Dropdown/Toggle.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export class Toggle extends React.Component<ToggleProps> {
109109
event.preventDefault();
110110

111111
this.props.onToggle(!this.props.isOpen, event);
112-
} else if ((event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') && !this.props.isOpen) {
112+
} else if ((event.key === 'Enter' || event.key === ' ') && !this.props.isOpen) {
113113
if (!this.props.bubbleEvent) {
114114
event.stopPropagation();
115115
}

packages/react-core/src/components/Dropdown/examples/Dropdown.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
Dropdown,
2727
DropdownToggle,
2828
DropdownItem,
29-
DropdownSeparator,
29+
DropdownSeparator
3030
} from '@patternfly/react-core';
3131
import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-icon';
3232

packages/react-core/src/helpers/KeyboardHandler.tsx

+62-27
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,22 @@ export interface KeyboardHandlerProps {
3939
* @param {boolean} noVerticalArrowHandling Flag indicating that the included vertical arrow key handling should be ignored
4040
* @param {boolean} noHorizontalArrowHandling Flag indicating that the included horizontal arrow key handling should be ignored
4141
* @param {boolean} updateTabIndex Flag indicating that the tabIndex of the currently focused element and next focused element should be updated, in the case of using a roving tabIndex
42+
* @param {boolean} onlyTraverseSiblings Flag indicating that next focusable element of a horizontal movement will be this element's sibling
4243
*/
4344
export const handleArrows = (
4445
event: KeyboardEvent,
4546
navigableElements: Element[],
4647
isActiveElement: (element: Element) => boolean = element => document.activeElement.contains(element),
4748
getFocusableElement: (element: Element) => Element = element => element,
48-
validSiblingTags: string[] = ['A', 'BUTTON'],
49+
validSiblingTags: string[] = ['A', 'BUTTON', 'INPUT'],
4950
noVerticalArrowHandling: boolean = false,
5051
noHorizontalArrowHandling: boolean = false,
51-
updateTabIndex: boolean = true
52+
updateTabIndex: boolean = true,
53+
onlyTraverseSiblings: boolean = true
5254
) => {
5355
const activeElement = document.activeElement;
5456
const key = event.key;
55-
let moveTarget = null;
57+
let moveTarget: Element = null;
5658

5759
// Handle vertical arrow keys. If noVerticalArrowHandling is passed, skip this block
5860
if (!noVerticalArrowHandling) {
@@ -62,23 +64,30 @@ export const handleArrows = (
6264

6365
// Traverse navigableElements to find the element which is currently active
6466
let currentIndex = -1;
67+
// while (currentIndex === -1) {
6568
navigableElements.forEach((element, index) => {
6669
if (isActiveElement(element)) {
6770
// Once found, move up or down the array by 1. Determined by the vertical arrow key direction
68-
const increment = key === 'ArrowUp' ? -1 : 1;
69-
currentIndex = index + increment;
71+
let increment = 0;
7072

71-
if (currentIndex >= navigableElements.length) {
72-
currentIndex = 0;
73-
}
74-
if (currentIndex < 0) {
75-
currentIndex = navigableElements.length - 1;
76-
}
73+
// keep increasing the increment until you've tried the whole navigableElement
74+
while (!moveTarget && increment < navigableElements.length && increment * -1 < navigableElements.length) {
75+
key === 'ArrowUp' ? increment-- : increment++;
76+
currentIndex = index + increment;
77+
78+
if (currentIndex >= navigableElements.length) {
79+
currentIndex = 0;
80+
}
81+
if (currentIndex < 0) {
82+
currentIndex = navigableElements.length - 1;
83+
}
7784

78-
// Set the next target element
79-
moveTarget = getFocusableElement(navigableElements[currentIndex]);
85+
// Set the next target element (undefined if none found)
86+
moveTarget = getFocusableElement(navigableElements[currentIndex]);
87+
}
8088
}
8189
});
90+
// }
8291
}
8392
}
8493

@@ -87,21 +96,47 @@ export const handleArrows = (
8796
if (['ArrowLeft', 'ArrowRight'].includes(key)) {
8897
event.preventDefault();
8998
event.stopImmediatePropagation(); // For menus in menus
90-
let nextSibling = activeElement;
91-
92-
// While a sibling exists, check each sibling to determine if it should be focussed
93-
while (nextSibling) {
94-
// Set the next checked sibling, determined by the horizontal arrow key direction
95-
nextSibling = key === 'ArrowLeft' ? nextSibling.previousElementSibling : nextSibling.nextElementSibling;
96-
if (nextSibling) {
97-
if (validSiblingTags.includes(nextSibling.tagName)) {
98-
// If the sibling's tag is included in validSiblingTags, set the next target element and break the loop
99-
moveTarget = nextSibling;
100-
break;
99+
100+
let currentIndex = -1;
101+
navigableElements.forEach((element, index) => {
102+
if (isActiveElement(element)) {
103+
const activeRow = navigableElements[index].querySelectorAll(validSiblingTags.join(',')); // all focusable elements in my row
104+
105+
if (!activeRow.length || onlyTraverseSiblings) {
106+
let nextSibling = activeElement;
107+
// While a sibling exists, check each sibling to determine if it should be focussed
108+
while (nextSibling) {
109+
// Set the next checked sibling, determined by the horizontal arrow key direction
110+
nextSibling = key === 'ArrowLeft' ? nextSibling.previousElementSibling : nextSibling.nextElementSibling;
111+
if (nextSibling) {
112+
if (validSiblingTags.includes(nextSibling.tagName)) {
113+
// If the sibling's tag is included in validSiblingTags, set the next target element and break the loop
114+
moveTarget = nextSibling;
115+
break;
116+
}
117+
// If the sibling's tag is not valid, skip to the next sibling if possible
118+
}
119+
}
120+
} else {
121+
activeRow.forEach((focusableElement, index) => {
122+
if (event.target === focusableElement) {
123+
// Once found, move up or down the array by 1. Determined by the vertical arrow key direction
124+
const increment = key === 'ArrowLeft' ? -1 : 1;
125+
currentIndex = index + increment;
126+
if (currentIndex >= activeRow.length) {
127+
currentIndex = 0;
128+
}
129+
if (currentIndex < 0) {
130+
currentIndex = activeRow.length - 1;
131+
}
132+
133+
// Set the next target element
134+
moveTarget = activeRow[currentIndex];
135+
}
136+
});
101137
}
102-
// If the sibling's tag is not valid, skip to the next sibling if possible
103138
}
104-
}
139+
});
105140
}
106141
}
107142

@@ -112,7 +147,7 @@ export const handleArrows = (
112147
(activeElement as HTMLElement).tabIndex = -1;
113148
(moveTarget as HTMLElement).tabIndex = 0;
114149
}
115-
// If a move target has been set by eithe arrow handler, focus that target
150+
// If a move target has been set by either arrow handler, focus that target
116151
(moveTarget as HTMLElement).focus();
117152
}
118153
};

packages/react-table/src/components/Table/ActionsColumn.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ActionsColumnState {
3535

3636
export class ActionsColumn extends React.Component<ActionsColumnProps, ActionsColumnState> {
3737
static displayName = 'ActionsColumn';
38+
private toggleRef = React.createRef<HTMLButtonElement>();
3839
static defaultProps = {
3940
children: null as React.ReactNode,
4041
items: [] as IAction[],

packages/react-table/src/components/Table/Table.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ export interface TableProps extends OUIAProps {
109109
canSortFavorites?: boolean;
110110
/** Flag indicating table is a tree table */
111111
isTreeTable?: boolean;
112+
/** Flag indicating this table is nested within another table */
113+
isNested?: boolean;
112114
}
113115

114116
export class Table extends React.Component<TableProps, {}> {
@@ -134,7 +136,8 @@ export class Table extends React.Component<TableProps, {}> {
134136
ouiaSafe: true,
135137
isStickyHeader: false,
136138
canSortFavorites: true,
137-
isTreeTable: false
139+
isTreeTable: false,
140+
isNested: false
138141
};
139142
state = {
140143
ouiaStateId: getDefaultOUIAId(Table.displayName)

0 commit comments

Comments
 (0)