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

SAP / ui5-webcomponents-react / 10551842760

26 Aug 2024 01:19AM CUT coverage: 87.85% (-0.02%) from 87.867%
10551842760

Pull #6261

github

web-flow
Merge a77c67ee6 into a7a441e4c
Pull Request #6261: chore(deps): update all non-major dependencies (examples & templates) (main) (patch)

2815 of 3738 branches covered (75.31%)

5025 of 5720 relevant lines covered (87.85%)

94995.24 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'];
400✔
7

8
const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => {
400✔
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) {
64!
32
    return null;
×
33
  }
34
  if (element?.parentElement.dataset.subcomponent) {
64✔
35
    return element.parentElement;
32✔
36
  }
37
  return recursiveSubComponentElementSearch(element.parentElement);
32✔
38
}
39

40
const findParentCell = (target) => {
400✔
41
  if (target === undefined || target === null) return;
4,702!
42
  if (
4,702✔
43
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
9,404!
44
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
45
  ) {
46
    return target;
3,668✔
47
  } else {
48
    return findParentCell(target.parentElement);
1,034✔
49
  }
50
};
51

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

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

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

73
  useEffect(() => {
46,044✔
74
    if (showOverlay && currentlyFocusedCell.current) {
3,019!
75
      currentlyFocusedCell.current.tabIndex = -1;
×
76
      currentlyFocusedCell.current = null;
×
77
    }
78
  }, [showOverlay]);
79

80
  const onTableBlur = (e) => {
46,044✔
81
    if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
4,644✔
82
      currentlyFocusedCell.current = null;
401✔
83
    }
84
  };
85

86
  useEffect(() => {
46,044✔
87
    if (
9,809!
88
      !showOverlay &&
39,691!
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(
46,044✔
102
    (e) => {
103
      const { dataset } = e.target;
4,217✔
104
      if (
4,217✔
105
        dataset.emptyRowCell === 'true' ||
16,743✔
106
        dataset.hasOwnProperty('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
      ) {
111
        return;
425✔
112
      }
113
      if (e.target.dataset.subcomponent) {
3,792✔
114
        e.target.tabIndex = 0;
76✔
115
        e.target.focus();
76✔
116
        currentlyFocusedCell.current = e.target;
76✔
117
        return;
76✔
118
      }
119
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
3,716✔
120
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
3,716✔
121
        e.target.tabIndex = -1;
48✔
122
        if (currentlyFocusedCell.current) {
48!
123
          const { dataset } = currentlyFocusedCell.current;
×
124
          const rowIndex = parseInt(dataset.rowIndex ?? dataset.rowIndexSub, 10);
×
125
          const columnIndex = parseInt(dataset.columnIndex ?? dataset.columnIndexSub, 10);
×
126
          if (
×
127
            e.target.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex}"]`) ||
×
128
            e.target.querySelector(`div[data-column-index-sub="${columnIndex}"][data-row-index-sub="${rowIndex}"]`)
129
          ) {
130
            currentlyFocusedCell.current.tabIndex = 0;
×
131
            currentlyFocusedCell.current.focus({ preventScroll: true });
×
132
          } else {
133
            getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
134
          }
135
        } else if (isFirstCellAvailable) {
48!
136
          const firstCell = e.target.querySelector(
48✔
137
            'div[data-column-index]:not([data-column-id^="__ui5wcr__internal"][data-row-index="0"])'
138
          );
139
          firstCell.tabIndex = 0;
48✔
140
          firstCell.focus({ preventScroll: true });
48✔
141
          currentlyFocusedCell.current = firstCell;
48✔
142
        } else {
143
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
144
        }
145
      } else {
146
        const tableCell = findParentCell(e.target);
3,668✔
147
        if (tableCell) {
3,668!
148
          currentlyFocusedCell.current = tableCell;
3,668✔
149
        } else {
150
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
151
        }
152
      }
153
    },
154
    [currentlyFocusedCell.current, tableRef.current, noData]
155
  );
156

157
  const onKeyboardNavigation = useCallback(
46,044✔
158
    (e) => {
159
      const { isRtl } = state;
738✔
160
      const isActiveItemInSubComponent = e.target.dataset.hasOwnProperty('subcomponentActiveElement');
738✔
161
      // check if target is cell and if so proceed from there
162
      if (
738!
163
        !currentlyFocusedCell.current &&
829✔
164
        CELL_DATA_ATTRIBUTES.every((item) => Object.keys(e.target.dataset).includes(item))
91✔
165
      ) {
166
        currentlyFocusedCell.current = e.target;
×
167
      }
168
      if (currentlyFocusedCell.current) {
738✔
169
        const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex ?? '0', 10);
647✔
170
        const rowIndex = parseInt(
647✔
171
          currentlyFocusedCell.current.dataset.rowIndex ?? currentlyFocusedCell.current.dataset.subcomponentRowIndex,
743✔
172
          10
173
        );
174
        switch (e.key) {
647✔
175
          case 'End': {
176
            e.preventDefault();
8✔
177
            const visibleColumns = tableRef.current.querySelector(
8✔
178
              `div[data-component-name="AnalyticalTableHeaderRow"]`
179
            ).children;
180

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

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

311
            if (hasSubcomponent && !isSubComponent) {
74✔
312
              currentlyFocusedCell.current.tabIndex = -1;
8✔
313
              firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
8✔
314
              firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;
8✔
315
              firstChildPrevRow.tabIndex = 0;
8✔
316
              firstChildPrevRow.focus();
8✔
317
              currentlyFocusedCell.current = firstChildPrevRow;
8✔
318
            } else if (previousRowCell) {
66✔
319
              setFocus(currentlyFocusedCell, previousRowCell);
66✔
320
            }
321
            break;
74✔
322
          }
323
        }
324
      }
325
    },
326
    [currentlyFocusedCell.current, tableRef.current, state?.isRtl]
327
  );
328
  if (showOverlay) {
46,044✔
329
    return tableProps;
210✔
330
  }
331
  return [
45,834✔
332
    tableProps,
333
    {
334
      onFocus: onTableFocus,
335
      onKeyDown: onKeyboardNavigation,
336
      onBlur: onTableBlur
337
    }
338
  ];
339
};
340

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

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

381
export const useKeyboardNavigation = (hooks: ReactTableHooks) => {
400✔
382
  hooks.getTableProps.push(useGetTableProps);
46,044✔
383
  hooks.getHeaderProps.push(setHeaderProps);
46,044✔
384
};
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