• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAP / ui5-webcomponents-react / 14902210870

08 May 2025 08:32AM CUT coverage: 88.206% (-0.4%) from 88.625%
14902210870

Pull #7308

github

web-flow
Merge 7ea0c9eb2 into 5b75245a7
Pull Request #7308: feat: update to UI5 Web Components 2.10.0

3016 of 3985 branches covered (75.68%)

1 of 1 new or added line in 1 file covered. (100.0%)

28 existing lines in 3 files now uncovered.

5295 of 6003 relevant lines covered (88.21%)

104231.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.83
/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts
1
import { useCallback, useEffect, useRef } from 'react';
2
import { actions } from 'react-table';
3
import type { ColumnType, ReactTableHooks, TableInstance } from '../types/index.js';
4
import { getLeafHeaders } from '../util/index.js';
5

6
const CELL_DATA_ATTRIBUTES = ['visibleColumnIndex', 'columnIndex', 'rowIndex', 'visibleRowIndex'];
444✔
7

8
const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => {
444✔
9
  if (
×
10
    target.dataset.componentName === 'AnalyticalTableContainer' &&
×
11
    target.querySelector('[data-component-name="AnalyticalTableBodyScrollableContainer"]')
12
  ) {
13
    const rowElements = target.querySelector('[data-component-name="AnalyticalTableBodyScrollableContainer"]').children;
×
14
    const middleRowCell = target.querySelector(
×
15
      `div[data-visible-column-index="0"][data-visible-row-index="${Math.round(rowElements.length / 2)}"]`
16
    );
17
    middleRowCell?.focus({ preventScroll: true });
×
18
  } else {
19
    const firstVisibleCell = noData
×
20
      ? target.querySelector(`div[data-visible-column-index="0"][data-visible-row-index="0"]`)
21
      : target.querySelector(`div[data-visible-column-index="0"][data-visible-row-index="1"]`);
22
    if (firstVisibleCell) {
×
23
      firstVisibleCell.tabIndex = 0;
×
24
      firstVisibleCell.focus();
×
25
      currentlyFocusedCell.current = firstVisibleCell;
×
26
    }
27
  }
28
};
29

30
function recursiveSubComponentElementSearch(element) {
31
  if (!element.parentElement) {
96!
32
    return null;
×
33
  }
34
  if (element?.parentElement.dataset.subcomponent) {
96✔
35
    return element.parentElement;
48✔
36
  }
37
  return recursiveSubComponentElementSearch(element.parentElement);
48✔
38
}
39

40
const findParentCell = (target) => {
444✔
41
  if (target === undefined || target === null) return;
13,714!
42
  if (
13,714✔
43
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
27,428!
44
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
45
  ) {
46
    return target;
6,121✔
47
  } else {
48
    return findParentCell(target.parentElement);
7,593✔
49
  }
50
};
51

52
const setFocus = (currentlyFocusedCell, nextElement) => {
444✔
53
  currentlyFocusedCell.current.tabIndex = -1;
461✔
54
  if (nextElement) {
461✔
55
    nextElement.tabIndex = 0;
461✔
56
    nextElement.focus();
461✔
57
    currentlyFocusedCell.current = nextElement;
461✔
58
  }
59
};
60

61
const navigateFromActiveSubCompItem = (currentlyFocusedCell, e) => {
444✔
62
  setFocus(currentlyFocusedCell, recursiveSubComponentElementSearch(e.target));
48✔
63
};
64

65
const useGetTableProps = (
444✔
66
  tableProps,
67
  { instance: { webComponentsReactProperties, data, columns, state } }: { instance: TableInstance }
68
) => {
69
  const { showOverlay, tableRef } = webComponentsReactProperties;
98,903✔
70
  const currentlyFocusedCell = useRef<HTMLDivElement>(null);
98,903✔
71
  const noData = data.length === 0;
98,903✔
72

73
  useEffect(() => {
98,903✔
74
    if (showOverlay && currentlyFocusedCell.current) {
7,290!
75
      currentlyFocusedCell.current.tabIndex = -1;
×
76
      currentlyFocusedCell.current = null;
×
77
    }
78
  }, [showOverlay]);
79

80
  const onTableBlur = (e) => {
98,903✔
81
    if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
5,897✔
82
      currentlyFocusedCell.current = null;
431✔
83
    }
84
  };
85

86
  useEffect(() => {
98,903✔
87
    if (
15,394!
88
      !showOverlay &&
62,474!
89
      data &&
90
      columns &&
91
      currentlyFocusedCell.current &&
92
      tableRef.current &&
93
      tableRef.current.tabIndex !== 0 &&
94
      !tableRef.current.contains(currentlyFocusedCell.current)
95
    ) {
96
      currentlyFocusedCell.current = null;
×
97
      tableRef.current.tabIndex = 0;
×
98
    }
99
  }, [data, columns, showOverlay]);
100

101
  const onTableFocus = useCallback(
98,903✔
102
    (e) => {
103
      const { dataset } = e.target;
6,893✔
104
      if (
6,893✔
105
        dataset.emptyRowCell === 'true' ||
33,713✔
106
        Object.prototype.hasOwnProperty.call(dataset, 'subcomponentActiveElement') ||
107
        // todo: with the new popover API of ui5wc this might not be necessary anymore
108
        dataset.componentName === 'ATHeaderPopoverList' ||
109
        dataset.componentName === 'ATHeaderPopover' ||
110
        dataset.componentName === 'AnalyticalTableNoDataContainer'
111
      ) {
112
        return;
575✔
113
      }
114
      if (e.target.dataset.subcomponent) {
6,318✔
115
        e.target.tabIndex = 0;
120✔
116
        e.target.focus();
120✔
117
        currentlyFocusedCell.current = e.target;
120✔
118
        return;
120✔
119
      }
120
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
6,198✔
121
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
6,198✔
122
        e.target.tabIndex = -1;
77✔
123
        if (currentlyFocusedCell.current) {
77!
124
          const { dataset } = currentlyFocusedCell.current;
×
125
          const rowIndex = parseInt(dataset.rowIndex ?? dataset.rowIndexSub, 10);
×
126
          const columnIndex = parseInt(dataset.columnIndex ?? dataset.columnIndexSub, 10);
×
UNCOV
127
          if (
×
128
            e.target.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex}"]`) ||
×
129
            e.target.querySelector(`div[data-column-index-sub="${columnIndex}"][data-row-index-sub="${rowIndex}"]`)
130
          ) {
131
            currentlyFocusedCell.current.tabIndex = 0;
×
UNCOV
132
            currentlyFocusedCell.current.focus({ preventScroll: true });
×
133
          } else {
UNCOV
134
            getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
135
          }
136
        } else if (isFirstCellAvailable) {
77!
137
          const firstCell = e.target.querySelector(
77✔
138
            'div[data-column-index]:not([data-column-id^="__ui5wcr__internal"][data-row-index="0"])'
139
          );
140
          firstCell.tabIndex = 0;
77✔
141
          firstCell.focus({ preventScroll: true });
77✔
142
          currentlyFocusedCell.current = firstCell;
77✔
143
        } else {
UNCOV
144
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
145
        }
146
      } else {
147
        const tableCell = findParentCell(e.target);
6,121✔
148
        if (tableCell) {
6,121!
149
          currentlyFocusedCell.current = tableCell;
6,121✔
150
        } else {
UNCOV
151
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
152
        }
153
      }
154
    },
155
    [currentlyFocusedCell.current, tableRef.current, noData]
156
  );
157

158
  const onKeyboardNavigation = useCallback(
98,903✔
159
    (e) => {
160
      const { isRtl } = state;
1,121✔
161
      const isActiveItemInSubComponent = Object.prototype.hasOwnProperty.call(
1,121✔
162
        e.target.dataset,
163
        'subcomponentActiveElement'
164
      );
165
      // check if target is cell and if so proceed from there
166
      if (
1,121!
167
        !currentlyFocusedCell.current &&
1,240✔
168
        CELL_DATA_ATTRIBUTES.every((item) => Object.keys(e.target.dataset).includes(item))
119✔
169
      ) {
UNCOV
170
        currentlyFocusedCell.current = e.target;
×
171
      }
172
      if (currentlyFocusedCell.current) {
1,121✔
173
        const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex ?? '0', 10);
1,002✔
174
        const rowIndex = parseInt(
1,002✔
175
          currentlyFocusedCell.current.dataset.rowIndex ?? currentlyFocusedCell.current.dataset.subcomponentRowIndex,
1,146✔
176
          10
177
        );
178
        switch (e.key) {
1,002✔
179
          case 'End': {
180
            e.preventDefault();
12✔
181
            const visibleColumns = tableRef.current.querySelector(
12✔
182
              `div[data-component-name="AnalyticalTableHeaderRow"]`
183
            ).children;
184

185
            const lastVisibleColumn = Array.from(visibleColumns)
12✔
186
              .slice(0)
187
              .reduceRight((_, cur, index, arr) => {
188
                const columnIndex = parseInt((cur.children?.[0] as HTMLDivElement)?.dataset.columnIndex, 10);
12✔
189
                if (!isNaN(columnIndex)) {
12✔
190
                  arr.length = 0;
12✔
191
                  return columnIndex;
12✔
192
                }
UNCOV
193
                return 0;
×
194
              }, 0);
195

196
            const newElement = tableRef.current.querySelector(
12✔
197
              `div[data-visible-column-index="${lastVisibleColumn}"][data-row-index="${rowIndex}"]`
198
            );
199
            setFocus(currentlyFocusedCell, newElement);
12✔
200
            break;
12✔
201
          }
202
          case 'Home': {
203
            e.preventDefault();
12✔
204
            const newElement = tableRef.current.querySelector(
12✔
205
              `div[data-visible-column-index="0"][data-row-index="${rowIndex}"]`
206
            );
207
            setFocus(currentlyFocusedCell, newElement);
12✔
208
            break;
12✔
209
          }
210
          case 'PageDown': {
211
            e.preventDefault();
48✔
212
            if (currentlyFocusedCell.current.dataset.rowIndex === '0') {
48✔
213
              const newElement = tableRef.current.querySelector(
12✔
214
                `div[data-column-index="${columnIndex}"][data-row-index="${rowIndex + 1}"]`
215
              );
216
              setFocus(currentlyFocusedCell, newElement);
12✔
217
            } else {
218
              const lastVisibleRow = tableRef.current.querySelector(`div[data-component-name="AnalyticalTableBody"]`)
36✔
219
                ?.children?.[0].children.length;
220
              const newElement = tableRef.current.querySelector(
36✔
221
                `div[data-column-index="${columnIndex}"][data-visible-row-index="${lastVisibleRow}"]`
222
              );
223
              setFocus(currentlyFocusedCell, newElement);
36✔
224
            }
225
            break;
48✔
226
          }
227
          case 'PageUp': {
228
            e.preventDefault();
48✔
229
            if (currentlyFocusedCell.current.dataset.rowIndex <= '1') {
48✔
230
              const newElement = tableRef.current.querySelector(
12✔
231
                `div[data-column-index="${columnIndex}"][data-row-index="0"]`
232
              );
233
              setFocus(currentlyFocusedCell, newElement);
12✔
234
            } else {
235
              const newElement = tableRef.current.querySelector(
36✔
236
                `div[data-column-index="${columnIndex}"][data-visible-row-index="1"]`
237
              );
238
              setFocus(currentlyFocusedCell, newElement);
36✔
239
            }
240
            break;
48✔
241
          }
242
          case 'ArrowRight': {
243
            e.preventDefault();
41✔
244
            if (isActiveItemInSubComponent) {
41✔
245
              navigateFromActiveSubCompItem(currentlyFocusedCell, e);
12✔
246
              return;
12✔
247
            }
248
            const newElement = tableRef.current.querySelector(
29✔
249
              `div[data-column-index="${columnIndex + (isRtl ? -1 : 1)}"][data-row-index="${rowIndex}"]`
29!
250
            );
251
            if (newElement) {
29✔
252
              setFocus(currentlyFocusedCell, newElement);
29✔
253
              // scroll to show full cell if it's only partial visible
254
              newElement.scrollIntoView({ block: 'nearest' });
29✔
255
            }
256
            break;
29✔
257
          }
258
          case 'ArrowLeft': {
259
            e.preventDefault();
36✔
260
            if (isActiveItemInSubComponent) {
36✔
261
              navigateFromActiveSubCompItem(currentlyFocusedCell, e);
12✔
262
              return;
12✔
263
            }
264
            const newElement = tableRef.current.querySelector(
24✔
265
              `div[data-column-index="${columnIndex - (isRtl ? -1 : 1)}"][data-row-index="${rowIndex}"]`
24!
266
            );
267
            if (newElement) {
24✔
268
              setFocus(currentlyFocusedCell, newElement);
24✔
269
              // scroll to show full cell if it's only partial visible
270
              newElement.scrollIntoView({ block: 'nearest' });
24✔
271
            }
272
            break;
24✔
273
          }
274
          case 'ArrowDown': {
275
            e.preventDefault();
206✔
276
            if (isActiveItemInSubComponent) {
206✔
277
              navigateFromActiveSubCompItem(currentlyFocusedCell, e);
12✔
278
              return;
12✔
279
            }
280
            const parent = currentlyFocusedCell.current.parentElement as HTMLDivElement;
194✔
281
            const firstChildOfParent = parent?.children?.[0] as HTMLDivElement;
194✔
282
            const hasSubcomponent = firstChildOfParent?.dataset?.subcomponent;
194✔
283
            const newElement = tableRef.current.querySelector(
194✔
284
              `div[data-column-index="${columnIndex}"][data-row-index="${rowIndex + 1}"]`
285
            );
286
            if (hasSubcomponent && !currentlyFocusedCell.current?.dataset?.subcomponent) {
194✔
287
              currentlyFocusedCell.current.tabIndex = -1;
36✔
288
              firstChildOfParent.tabIndex = 0;
36✔
289
              firstChildOfParent.dataset.rowIndexSub = `${rowIndex}`;
36✔
290
              firstChildOfParent.dataset.columnIndexSub = `${columnIndex}`;
36✔
291
              firstChildOfParent.focus();
36✔
292
              currentlyFocusedCell.current = firstChildOfParent;
36✔
293
            } else if (newElement) {
158✔
294
              setFocus(currentlyFocusedCell, newElement);
158✔
295
            }
296
            break;
194✔
297
          }
298
          case 'ArrowUp': {
299
            e.preventDefault();
106✔
300
            if (isActiveItemInSubComponent) {
106✔
301
              navigateFromActiveSubCompItem(currentlyFocusedCell, e);
12✔
302
              return;
12✔
303
            }
304
            let prevRowIndex = rowIndex - 1;
94✔
305
            const isSubComponent = e.target.dataset.subcomponent;
94✔
306
            if (isSubComponent) {
94✔
307
              prevRowIndex++;
12✔
308
            }
309
            const previousRowCell = tableRef.current.querySelector(
94✔
310
              `div[data-column-index="${columnIndex}"][data-row-index="${prevRowIndex}"]`
311
            );
312
            const firstChildPrevRow = previousRowCell?.parentElement.children[0] as HTMLDivElement;
94✔
313
            const hasSubcomponent = firstChildPrevRow?.dataset?.subcomponent;
94✔
314

315
            if (hasSubcomponent && !isSubComponent) {
94✔
316
              currentlyFocusedCell.current.tabIndex = -1;
12✔
317
              firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
12✔
318
              firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;
12✔
319
              firstChildPrevRow.tabIndex = 0;
12✔
320
              firstChildPrevRow.focus();
12✔
321
              currentlyFocusedCell.current = firstChildPrevRow;
12✔
322
            } else if (previousRowCell) {
82✔
323
              setFocus(currentlyFocusedCell, previousRowCell);
82✔
324
            }
325
            break;
94✔
326
          }
327
        }
328
      }
329
    },
330
    [currentlyFocusedCell.current, tableRef.current, state?.isRtl]
331
  );
332
  if (showOverlay) {
98,903✔
333
    return tableProps;
450✔
334
  }
335
  return [
98,453✔
336
    tableProps,
337
    {
338
      onFocus: onTableFocus,
339
      onKeyDown: onKeyboardNavigation,
340
      onBlur: onTableBlur
341
    }
342
  ];
343
};
344

345
function getPayload(e, column) {
346
  e.preventDefault();
×
347
  e.stopPropagation();
×
348
  const clientX = e.target.getBoundingClientRect().x + e.target.getBoundingClientRect().width;
×
349
  const columnId = column.id;
×
350
  const columnWidth = column.totalWidth;
×
351
  const headersToResize = getLeafHeaders(column);
×
352
  const headerIdWidths = headersToResize.map((d) => [d.id, d.totalWidth]);
×
UNCOV
353
  return { clientX, columnId, columnWidth, headerIdWidths };
×
354
}
355

356
const setHeaderProps = (
444✔
357
  headerProps,
358
  { instance: { dispatch }, column }: { instance: TableInstance; column: ColumnType }
359
) => {
360
  // resize col with keyboard
361
  const handleKeyDown = (e) => {
360,010✔
362
    if (typeof headerProps.onKeyDown === 'function') {
269✔
363
      headerProps.onKeyDown(e);
138✔
364
    }
365
    if (e.nativeEvent.shiftKey) {
269!
366
      if (e.key === 'ArrowRight') {
×
367
        const payload = getPayload(e, column);
×
368
        dispatch({ type: actions.columnStartResizing, ...payload });
×
369
        dispatch({ type: actions.columnResizing, clientX: payload.clientX + 16 });
×
370
        dispatch({ type: actions.columnDoneResizing });
×
UNCOV
371
        return;
×
372
      }
373
      if (e.key === 'ArrowLeft') {
×
374
        const payload = getPayload(e, column);
×
375
        dispatch({ type: actions.columnStartResizing, ...payload });
×
376
        dispatch({ type: actions.columnResizing, clientX: payload.clientX - 16 });
×
377
        dispatch({ type: actions.columnDoneResizing });
×
UNCOV
378
        return;
×
379
      }
380
    }
381
  };
382
  return [headerProps, { onKeyDown: handleKeyDown }];
360,010✔
383
};
384

385
export const useKeyboardNavigation = (hooks: ReactTableHooks) => {
444✔
386
  hooks.getTableProps.push(useGetTableProps);
98,903✔
387
  hooks.getHeaderProps.push(setHeaderProps);
98,903✔
388
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc