Skip to content

Commit 58ef172

Browse files
authored
fix(AnalyticalTable): improve keyboard navigation and subcomponents focus handling (#4829)
1 parent bdfae80 commit 58ef172

File tree

6 files changed

+84
-25
lines changed

6 files changed

+84
-25
lines changed

packages/main/src/components/AnalyticalTable/AnalyticalTable.mdx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,10 @@ const InfiniteScrollTable = (props) => {
278278
Adding custom subcomponents below table rows can be achieved by setting the `renderRowSubComponent` prop.
279279
The prop expects a function with an optional parameter containing the `row` instance, there you can control which row should display subcomponents. If you want to display the subcomponent at the bottom of the row without an expandable container, you can set the `alwaysShowSubComponent` prop to `true`.
280280

281-
**Note:** When `renderRowSubComponent` is set, `grouping` is disabled.
281+
### Notes
282+
283+
- When `renderRowSubComponent` is set, `grouping` is disabled.
284+
- When rendering active elements inside the subcomponent, make sure to add the `data-subcomponent-active-element' attribute, otherwise focus behavior won't be consistent.
282285

283286
<ControlsWithNote of={ComponentStories.Subcomponents} include={['alwaysShowSubComponent', 'renderRowSubComponent']} />
284287

@@ -303,6 +306,12 @@ const TableWithSubcomponents = (props) => {
303306
>
304307
<Badge>height: 300px</Badge>
305308
<Text>This subcomponent will only be displayed below the first row.</Text>
309+
<hr />
310+
<Text>
311+
The button below is rendered with `data-subcomponent-active-element` attribute to ensure consistent focus
312+
behavior
313+
</Text>
314+
<Button data-subcomponent-active-element>Click</Button>
306315
</FlexBox>
307316
);
308317
}

packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,12 @@ export const Subcomponents: Story = {
440440
>
441441
<Badge>height: 300px</Badge>
442442
<Text>This subcomponent will only be displayed below the first row.</Text>
443+
<hr />
444+
<Text>
445+
The button below is rendered with `data-subcomponent-active-element` attribute to ensure consistent focus
446+
behavior
447+
</Text>
448+
<Button data-subcomponent-active-element>Click</Button>
443449
</FlexBox>
444450
);
445451
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,22 @@ interface RowSubComponent {
2525
children: ReactNode | ReactNode[];
2626
rows: Record<string, unknown>[];
2727
alwaysShowSubComponent: boolean;
28+
rowIndex: number;
2829
}
2930

3031
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3132
export const RowSubComponent = (props: RowSubComponent) => {
32-
const { subComponentsHeight, virtualRow, dispatch, row, rowHeight, children, rows, alwaysShowSubComponent } = props;
33+
const {
34+
subComponentsHeight,
35+
virtualRow,
36+
dispatch,
37+
row,
38+
rowHeight,
39+
children,
40+
rows,
41+
alwaysShowSubComponent,
42+
rowIndex
43+
} = props;
3344
const subComponentRef = useRef(null);
3445
const classes = useStyles();
3546

@@ -95,6 +106,7 @@ export const RowSubComponent = (props: RowSubComponent) => {
95106
<div
96107
ref={subComponentRef}
97108
data-subcomponent
109+
data-subcomponent-row-index={rowIndex}
98110
tabIndex={-1}
99111
style={{
100112
boxSizing: 'border-box',

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
203203
rowHeight={rowHeight}
204204
rows={rows}
205205
alwaysShowSubComponent={alwaysShowSubComponent}
206+
rowIndex={visibleRowIndex + 1}
206207
>
207208
{RowSubComponent}
208209
</SubComponent>

packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => {
2424
}
2525
};
2626

27+
function recursiveSubComponentElementSearch(element) {
28+
if (!element.parentElement) {
29+
return null;
30+
}
31+
if (element?.parentElement.dataset.subcomponent) {
32+
return element.parentElement;
33+
}
34+
return recursiveSubComponentElementSearch(element.parentElement);
35+
}
36+
2737
const findParentCell = (target) => {
2838
if (target === undefined || target === null) return;
2939
if (
@@ -45,6 +55,10 @@ const setFocus = (currentlyFocusedCell, nextElement) => {
4555
}
4656
};
4757

58+
const navigateFromActiveSubCompItem = (currentlyFocusedCell, e) => {
59+
setFocus(currentlyFocusedCell, recursiveSubComponentElementSearch(e.target));
60+
};
61+
4862
const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties, data, columns } }) => {
4963
const { showOverlay, tableRef } = webComponentsReactProperties;
5064
const currentlyFocusedCell = useRef<HTMLDivElement>(null);
@@ -80,7 +94,13 @@ const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties
8094

8195
const onTableFocus = useCallback(
8296
(e) => {
83-
if (e.target.dataset?.emptyRowCell === 'true') {
97+
if (e.target.dataset?.emptyRowCell === 'true' || e.target.dataset.subcomponentActiveElement) {
98+
return;
99+
}
100+
if (e.target.dataset.subcomponent) {
101+
e.target.tabIndex = 0;
102+
e.target.focus();
103+
currentlyFocusedCell.current = e.target;
84104
return;
85105
}
86106
const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
@@ -121,6 +141,7 @@ const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties
121141

122142
const onKeyboardNavigation = useCallback(
123143
(e) => {
144+
const isActiveItemInSubComponent = e.target.dataset.subcomponentActiveElement;
124145
// check if target is cell and if so proceed from there
125146
if (
126147
!currentlyFocusedCell.current &&
@@ -129,14 +150,18 @@ const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties
129150
currentlyFocusedCell.current = e.target;
130151
}
131152
if (currentlyFocusedCell.current) {
132-
const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex, 10);
133-
const rowIndex = parseInt(currentlyFocusedCell.current.dataset.rowIndex, 10);
153+
const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex ?? '0', 10);
154+
const rowIndex = parseInt(
155+
currentlyFocusedCell.current.dataset.rowIndex ?? currentlyFocusedCell.current.dataset.subcomponentRowIndex,
156+
10
157+
);
134158
switch (e.key) {
135159
case 'End': {
136160
e.preventDefault();
137161
const visibleColumns: HTMLDivElement[] = tableRef.current.querySelector(
138162
`div[data-component-name="AnalyticalTableHeaderRow"]`
139163
).children;
164+
140165
const lastVisibleColumn = Array.from(visibleColumns)
141166
.slice(0)
142167
.reduceRight((_, cur, index, arr) => {
@@ -149,7 +174,7 @@ const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties
149174
}, 0);
150175

151176
const newElement = tableRef.current.querySelector(
152-
`div[data-visible-column-index="${lastVisibleColumn + 1}"][data-row-index="${rowIndex}"]`
177+
`div[data-visible-column-index="${lastVisibleColumn}"][data-row-index="${rowIndex}"]`
153178
);
154179
setFocus(currentlyFocusedCell, newElement);
155180
break;
@@ -196,6 +221,10 @@ const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties
196221
}
197222
case 'ArrowRight': {
198223
e.preventDefault();
224+
if (isActiveItemInSubComponent) {
225+
navigateFromActiveSubCompItem(currentlyFocusedCell, e);
226+
return;
227+
}
199228
const newElement = tableRef.current.querySelector(
200229
`div[data-column-index="${columnIndex + 1}"][data-row-index="${rowIndex}"]`
201230
);
@@ -208,6 +237,10 @@ const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties
208237
}
209238
case 'ArrowLeft': {
210239
e.preventDefault();
240+
if (isActiveItemInSubComponent) {
241+
navigateFromActiveSubCompItem(currentlyFocusedCell, e);
242+
return;
243+
}
211244
const newElement = tableRef.current.querySelector(
212245
`div[data-column-index="${columnIndex - 1}"][data-row-index="${rowIndex}"]`
213246
);
@@ -220,6 +253,10 @@ const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties
220253
}
221254
case 'ArrowDown': {
222255
e.preventDefault();
256+
if (isActiveItemInSubComponent) {
257+
navigateFromActiveSubCompItem(currentlyFocusedCell, e);
258+
return;
259+
}
223260
const parent = currentlyFocusedCell.current.parentElement as HTMLDivElement;
224261
const firstChildOfParent = parent?.children?.[0] as HTMLDivElement;
225262
const hasSubcomponent = firstChildOfParent?.dataset?.subcomponent;
@@ -235,35 +272,27 @@ const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties
235272
currentlyFocusedCell.current = firstChildOfParent;
236273
} else if (newElement) {
237274
setFocus(currentlyFocusedCell, newElement);
238-
} else if (e.target.dataset.subcomponent) {
239-
const nextElementToSubComp = tableRef.current.querySelector(
240-
`div[data-column-index="${parseInt(e.target.dataset.columnIndexSub)}"][data-row-index="${
241-
parseInt(e.target.dataset.rowIndexSub) + 1
242-
}"]`
243-
);
244-
setFocus(currentlyFocusedCell, nextElementToSubComp);
245275
}
246276
break;
247277
}
248278
case 'ArrowUp': {
249279
e.preventDefault();
280+
if (isActiveItemInSubComponent) {
281+
navigateFromActiveSubCompItem(currentlyFocusedCell, e);
282+
return;
283+
}
284+
let prevRowIndex = rowIndex - 1;
285+
const isSubComponent = e.target.dataset.subcomponent;
286+
if (isSubComponent) {
287+
prevRowIndex++;
288+
}
250289
const previousRowCell = tableRef.current.querySelector(
251-
`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex - 1}"]`
290+
`div[data-column-index="${columnIndex}"][data-row-index="${prevRowIndex}"]`
252291
);
253292
const firstChildPrevRow = previousRowCell?.parentElement.children[0] as HTMLDivElement;
254293
const hasSubcomponent = firstChildPrevRow?.dataset?.subcomponent;
255294

256-
if (currentlyFocusedCell.current?.dataset?.subcomponent) {
257-
currentlyFocusedCell.current.tabIndex = -1;
258-
const newElement = tableRef.current.querySelector(
259-
`div[data-column-index="${parseInt(e.target.dataset.columnIndexSub)}"][data-row-index="${parseInt(
260-
e.target.dataset.rowIndexSub
261-
)}"]`
262-
);
263-
newElement.tabIndex = 0;
264-
newElement.focus();
265-
currentlyFocusedCell.current = newElement;
266-
} else if (hasSubcomponent) {
295+
if (hasSubcomponent && !isSubComponent) {
267296
currentlyFocusedCell.current.tabIndex = -1;
268297
firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
269298
firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
487487
overscanCount?: number;
488488
/**
489489
* Defines the subcomponent that should be displayed below each row.
490+
*
491+
* __Note:__ When rendering active elements inside the subcomponent, make sure to add the `data-subcomponent-active-element' attribute, otherwise focus behavior won't be consistent.
490492
*/
491493
renderRowSubComponent?: (row?: any) => ReactNode;
492494
/**

0 commit comments

Comments
 (0)