Skip to content

Commit 0f8bfdd

Browse files
authored
feat(AnalyticalTable): add improved keyboard navigation (#1864)
1 parent ba7128c commit 0f8bfdd

File tree

13 files changed

+1767
-178
lines changed

13 files changed

+1767
-178
lines changed

packages/main/src/components/AnalyticalTable/AnayticalTable.jss.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,19 @@ const styles = {
5151
textAlign: 'start',
5252
boxSizing: 'border-box',
5353
'&[data-column-id="__ui5wcr__internal_highlight_column"]': {
54-
borderRight: 'none'
54+
borderRight: '1px solid transparent'
5555
},
5656
'&:last-child': {
5757
'& [data-resizer]': {
5858
transform: 'translateX(0px)'
5959
}
60+
},
61+
'&:focus': {
62+
'&[data-column-id="__ui5wcr__internal_selection_column"]': {
63+
borderLeft: '1px solid transparent'
64+
},
65+
outlineOffset: `calc(-1 * ${ThemingParameters.sapContent_FocusWidth})`,
66+
outline: `${ThemingParameters.sapContent_FocusWidth} ${ThemingParameters.sapContent_FocusStyle} ${ThemingParameters.sapContent_FocusColor}`
6067
}
6168
},
6269
tbody: {

packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderContainer.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,13 @@ export const ColumnHeaderContainer = forwardRef((props: ColumnHeaderContainerPro
8383
const classes = useStyles();
8484

8585
return (
86-
<div {...headerProps} style={{ width: `${columnVirtualizer.totalSize}px` }} ref={ref}>
87-
{columnVirtualizer.virtualItems.map((virtualColumn: VirtualItem) => {
86+
<div
87+
{...headerProps}
88+
style={{ width: `${columnVirtualizer.totalSize}px` }}
89+
ref={ref}
90+
data-component-name="AnalyticalTableHeaderRow"
91+
>
92+
{columnVirtualizer.virtualItems.map((virtualColumn: VirtualItem, index) => {
8893
const column = headerGroup.headers[virtualColumn.index];
8994
if (!column) {
9095
return null;
@@ -113,6 +118,8 @@ export const ColumnHeaderContainer = forwardRef((props: ColumnHeaderContainerPro
113118
)}
114119
<ColumnHeader
115120
{...rest}
121+
visibleColumnIndex={index}
122+
columnIndex={virtualColumn.index}
116123
onSort={onSort}
117124
onGroupBy={onGroupByChanged}
118125
onDragStart={onDragStart}

packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderModal.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SORT_DESCENDING,
1010
UNGROUP
1111
} from '@ui5/webcomponents-react/dist/assets/i18n/i18n-defaults';
12+
import { CustomListItem } from '@ui5/webcomponents-react/dist/CustomListItem';
1213
import { FlexBox } from '@ui5/webcomponents-react/dist/FlexBox';
1314
import { FlexBoxAlignItems } from '@ui5/webcomponents-react/dist/FlexBoxAlignItems';
1415
import { Icon } from '@ui5/webcomponents-react/dist/Icon';
@@ -18,10 +19,10 @@ import { PopoverPlacementType } from '@ui5/webcomponents-react/dist/PopoverPlace
1819
import { Popover } from '@ui5/webcomponents-react/dist/Popover';
1920
import { PopoverHorizontalAlign } from '@ui5/webcomponents-react/dist/PopoverHorizontalAlign';
2021
import { StandardListItem } from '@ui5/webcomponents-react/dist/StandardListItem';
22+
import { Ui5PopoverDomRef } from '@ui5/webcomponents-react/interfaces/Ui5PopoverDomRef';
2123
import React, { RefObject, useCallback, useEffect, useRef } from 'react';
2224
import { createPortal } from 'react-dom';
2325
import { createUseStyles } from 'react-jss';
24-
import { Ui5PopoverDomRef } from '@ui5/webcomponents-react/interfaces/Ui5PopoverDomRef';
2526
import { stopPropagation } from '../../../internal/stopPropagation';
2627
import { ColumnType } from '../types/ColumnType';
2728

@@ -55,6 +56,7 @@ export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
5556
const showSort = column.canSort;
5657

5758
const ref = useRef<Ui5PopoverDomRef>(null);
59+
const listRef = useRef(null);
5860

5961
const { Filter } = column;
6062

@@ -143,6 +145,13 @@ export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
143145
[setPopoverOpen]
144146
);
145147

148+
const onAfterOpen = useCallback(
149+
(e) => {
150+
listRef.current.children[0].focus();
151+
},
152+
[listRef.current]
153+
);
154+
146155
if (!open) return null;
147156
return createPortal(
148157
<Popover
@@ -152,8 +161,9 @@ export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
152161
ref={ref}
153162
className={classes.popover}
154163
onAfterClose={onAfterClose}
164+
onAfterOpen={onAfterOpen}
155165
>
156-
<List onItemClick={handleSort}>
166+
<List onItemClick={handleSort} ref={listRef}>
157167
{isSortedAscending && (
158168
<StandardListItem type={ListItemType.Active} icon="decline" data-sort="clear">
159169
{clearSortingText}
@@ -175,10 +185,13 @@ export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
175185
</StandardListItem>
176186
)}
177187
{showFilter && !column.isGrouped && (
178-
<FlexBox alignItems={FlexBoxAlignItems.Center} className={classes.filter}>
179-
<Icon name="filter" className={classes.filterIcon} />
180-
<Filter column={column} popoverRef={ref} />
181-
</FlexBox>
188+
//todo maybe need to enhance Input selection after ui5-webcomponents issue has been fixed
189+
<CustomListItem type={ListItemType.Inactive}>
190+
<FlexBox alignItems={FlexBoxAlignItems.Center} className={classes.filter}>
191+
<Icon name="filter" className={classes.filterIcon} />
192+
<Filter column={column} popoverRef={ref} />
193+
</FlexBox>
194+
</CustomListItem>
182195
)}
183196
{showGroup && (
184197
<StandardListItem type={ListItemType.Active} icon="group-2" data-sort={'group'}>

packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import React, {
1010
CSSProperties,
1111
DragEventHandler,
1212
FC,
13+
MouseEventHandler,
14+
KeyboardEventHandler,
1315
ReactNode,
1416
ReactNodeArray,
1517
useCallback,
@@ -22,13 +24,8 @@ import { ColumnType } from '../types/ColumnType';
2224
import { ColumnHeaderModal } from './ColumnHeaderModal';
2325

2426
export interface ColumnHeaderProps {
25-
id: string;
26-
defaultSortDesc: boolean;
27-
children: ReactNode | ReactNodeArray;
28-
grouping: string;
29-
className: string;
30-
column: ColumnType;
31-
style: CSSProperties;
27+
visibleColumnIndex: number;
28+
columnIndex: number;
3229
onSort?: (e: CustomEvent<{ column: unknown; sortDirection: string }>) => void;
3330
onGroupBy?: (e: CustomEvent<{ column: unknown; isGrouped: boolean }>) => void;
3431
onDragStart: DragEventHandler<HTMLDivElement>;
@@ -37,13 +34,20 @@ export interface ColumnHeaderProps {
3734
onDragEnter: DragEventHandler<HTMLDivElement>;
3835
onDragEnd: DragEventHandler<HTMLDivElement>;
3936
dragOver: boolean;
40-
isResizing: boolean;
41-
headerTooltip: string;
4237
isDraggable: boolean;
43-
role: string;
44-
isLastColumn: boolean;
38+
headerTooltip: string;
4539
virtualColumn: VirtualItem;
4640
isRtl: boolean;
41+
children: ReactNode | ReactNodeArray;
42+
43+
//getHeaderProps()
44+
id: string;
45+
onClick: MouseEventHandler<HTMLDivElement> | undefined;
46+
onKeyDown?: KeyboardEventHandler<HTMLDivElement> | undefined;
47+
className: string;
48+
style: CSSProperties;
49+
column: ColumnType;
50+
role: string;
4751
}
4852

4953
const styles = {
@@ -105,7 +109,11 @@ export const ColumnHeader: FC<ColumnHeaderProps> = (props: ColumnHeaderProps) =>
105109
dragOver,
106110
role,
107111
virtualColumn,
108-
isRtl
112+
isRtl,
113+
columnIndex,
114+
visibleColumnIndex,
115+
onClick,
116+
onKeyDown
109117
} = props;
110118

111119
const isFiltered = column.filterValue && column.filterValue.length > 0;
@@ -146,19 +154,43 @@ export const ColumnHeader: FC<ColumnHeaderProps> = (props: ColumnHeaderProps) =>
146154

147155
const hasPopover = column.canGroupBy || column.canSort || column.canFilter;
148156

149-
const onOpenPopover = useCallback(() => {
150-
if (hasPopover) {
151-
setPopoverOpen(true);
152-
}
153-
}, [hasPopover]);
157+
const handleHeaderCellClick = useCallback(
158+
(e) => {
159+
onClick?.(e);
160+
if (hasPopover) {
161+
setPopoverOpen(true);
162+
}
163+
},
164+
[hasPopover, onClick]
165+
);
154166
const directionStyles = isRtl
155167
? { right: 0, transform: `translateX(-${virtualColumn.start}px)` }
156168
: { left: 0, transform: `translateX(${virtualColumn.start}px)` };
157169

158170
const iconContainerDirectionStyles = isRtl ? { left: '0.5rem' } : { right: '0.5rem' };
159171

172+
const handleHeaderCellKeyDown = useCallback(
173+
(e) => {
174+
onKeyDown?.(e);
175+
if (hasPopover && /*e.code === 'Space' ||*/ e.code === 'Enter') {
176+
setPopoverOpen(true);
177+
}
178+
},
179+
[hasPopover, onKeyDown]
180+
);
181+
182+
const handleHeaderCellKeyUp = useCallback(
183+
(e) => {
184+
if (hasPopover && e.code === 'Space') {
185+
setPopoverOpen(true);
186+
}
187+
},
188+
[hasPopover]
189+
);
190+
160191
const targetRef = useRef();
161192
if (!column) return null;
193+
162194
return (
163195
<div
164196
ref={targetRef}
@@ -170,6 +202,11 @@ export const ColumnHeader: FC<ColumnHeaderProps> = (props: ColumnHeaderProps) =>
170202
}}
171203
>
172204
<div
205+
data-visible-column-index={visibleColumnIndex}
206+
data-visible-row-index={0}
207+
data-row-index={0}
208+
data-column-index={columnIndex}
209+
tabIndex={-1}
173210
id={id}
174211
className={className}
175212
style={{
@@ -185,7 +222,9 @@ export const ColumnHeader: FC<ColumnHeaderProps> = (props: ColumnHeaderProps) =>
185222
onDrop={onDrop}
186223
onDragEnd={onDragEnd}
187224
data-column-id={id}
188-
onClick={onOpenPopover}
225+
onClick={handleHeaderCellClick}
226+
onKeyDown={handleHeaderCellKeyDown}
227+
onKeyUp={handleHeaderCellKeyUp}
189228
>
190229
<div className={classes.header} data-h-align={column.hAlign}>
191230
<Text tooltip={tooltip} wrapping={false} style={textStyle} className={classes.text}>

packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBody.tsx

Lines changed: 7 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -97,106 +97,6 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
9797
scrollToIndex: rowVirtualizer.scrollToIndex
9898
};
9999

100-
const currentlyFocusedCell = useRef<HTMLDivElement>(null);
101-
const onTableFocus = useCallback(
102-
(e) => {
103-
const firstCell: HTMLDivElement = e.target.querySelector(
104-
'div[role="row"]:first-child div[role="cell"]:first-child'
105-
);
106-
const isCellOrSubComp = e.target.getAttribute('role') === 'cell' || e.target?.dataset.subcomponent;
107-
if (firstCell) {
108-
firstCell.tabIndex = 0;
109-
firstCell.focus();
110-
currentlyFocusedCell.current = firstCell;
111-
} else if (isCellOrSubComp) {
112-
currentlyFocusedCell.current = e.target;
113-
e.target.tabIndex = 0;
114-
}
115-
},
116-
[currentlyFocusedCell]
117-
);
118-
119-
const onKeyboardNavigation = useCallback(
120-
(e) => {
121-
if (currentlyFocusedCell.current) {
122-
switch (e.key) {
123-
case 'ArrowRight': {
124-
const newElement = currentlyFocusedCell.current.nextElementSibling as HTMLDivElement;
125-
if (newElement) {
126-
currentlyFocusedCell.current.tabIndex = -1;
127-
newElement.tabIndex = 0;
128-
newElement.focus();
129-
currentlyFocusedCell.current = newElement;
130-
}
131-
break;
132-
}
133-
case 'ArrowLeft': {
134-
const newElement = currentlyFocusedCell.current.previousElementSibling as HTMLDivElement;
135-
if (newElement) {
136-
currentlyFocusedCell.current.tabIndex = -1;
137-
newElement.tabIndex = 0;
138-
newElement.focus();
139-
currentlyFocusedCell.current = newElement;
140-
}
141-
break;
142-
}
143-
case 'ArrowDown': {
144-
const parent = currentlyFocusedCell.current.parentElement as HTMLDivElement;
145-
const firstChildOfParent = parent.children[0] as HTMLDivElement;
146-
const hasSubcomponent = firstChildOfParent?.dataset?.subcomponent;
147-
const nextRow = parent.nextElementSibling;
148-
if (hasSubcomponent && !currentlyFocusedCell.current?.dataset?.subcomponent) {
149-
currentlyFocusedCell.current.tabIndex = -1;
150-
firstChildOfParent.tabIndex = 0;
151-
firstChildOfParent.focus();
152-
currentlyFocusedCell.current = firstChildOfParent;
153-
} else if (nextRow) {
154-
currentlyFocusedCell.current.tabIndex = -1;
155-
const currentColumnIndex = currentlyFocusedCell.current.getAttribute('aria-colindex') || '1';
156-
const newElement: HTMLDivElement = nextRow.querySelector(`div[aria-colindex="${currentColumnIndex}"]`);
157-
newElement.tabIndex = 0;
158-
newElement.focus();
159-
currentlyFocusedCell.current = newElement;
160-
}
161-
break;
162-
}
163-
case 'ArrowUp': {
164-
const previousRow = currentlyFocusedCell.current.parentElement.previousElementSibling as HTMLDivElement;
165-
const firstChildPrevRow = previousRow?.children[0] as HTMLDivElement;
166-
const hasSubcomponent = firstChildPrevRow?.dataset?.subcomponent;
167-
168-
if (currentlyFocusedCell.current?.dataset?.subcomponent) {
169-
currentlyFocusedCell.current.tabIndex = -1;
170-
const newElement: HTMLDivElement =
171-
currentlyFocusedCell.current.parentElement.querySelector(`div[aria-colindex="1"]`);
172-
newElement.tabIndex = 0;
173-
newElement.focus();
174-
currentlyFocusedCell.current = newElement;
175-
} else if (hasSubcomponent) {
176-
currentlyFocusedCell.current.tabIndex = -1;
177-
firstChildPrevRow.tabIndex = 0;
178-
firstChildPrevRow.focus();
179-
currentlyFocusedCell.current = firstChildPrevRow;
180-
} else if (previousRow) {
181-
currentlyFocusedCell.current.tabIndex = -1;
182-
const currentColumnIndex = currentlyFocusedCell.current.getAttribute('aria-colindex') || '1';
183-
const newElement: HTMLDivElement = previousRow.querySelector(
184-
`div[aria-colindex="${currentColumnIndex}"]`
185-
);
186-
if (newElement) {
187-
newElement.tabIndex = 0;
188-
newElement.focus();
189-
currentlyFocusedCell.current = newElement;
190-
}
191-
}
192-
break;
193-
}
194-
}
195-
}
196-
},
197-
[currentlyFocusedCell]
198-
);
199-
200100
const popInColumn = useMemo(
201101
() =>
202102
visibleColumns.filter(
@@ -210,17 +110,15 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
210110

211111
return (
212112
<div
213-
tabIndex={0}
214-
onFocus={onTableFocus}
215-
onKeyDown={onKeyboardNavigation}
216113
style={{
217114
position: 'relative',
218115
height: `${rowVirtualizer.totalSize}px`,
219116
width: `${columnVirtualizer.totalSize}px`
220117
}}
221118
>
222-
{rowVirtualizer.virtualItems.map((virtualRow) => {
119+
{rowVirtualizer.virtualItems.map((virtualRow, visibleRowIndex) => {
223120
const row = rows[virtualRow.index];
121+
const rowIndexWithHeader = virtualRow.index + 1;
224122
if (!row) {
225123
return (
226124
<div
@@ -270,7 +168,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
270168
{RowSubComponent}
271169
</SubComponent>
272170
)}
273-
{columnVirtualizer.virtualItems.map((virtualColumn, index) => {
171+
{columnVirtualizer.virtualItems.map((virtualColumn, visibleColumnIndex) => {
274172
const cell = row.cells[virtualColumn.index];
275173
const directionStyles = isRtl
276174
? {
@@ -307,6 +205,10 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
307205
return (
308206
<div
309207
{...cellProps}
208+
data-visible-column-index={visibleColumnIndex}
209+
data-column-index={virtualColumn.index}
210+
data-visible-row-index={visibleRowIndex + 1}
211+
data-row-index={rowIndexWithHeader}
310212
style={{
311213
...cellProps.style,
312214
position: 'absolute',

0 commit comments

Comments
 (0)