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

SAP / ui5-webcomponents-react / 7089038778

04 Dec 2023 03:47PM CUT coverage: 87.844% (-0.07%) from 87.912%
7089038778

Pull #5315

github

web-flow
Merge abcbb79c8 into 07b868a43
Pull Request #5315: chore(deps): update ui5 web components react (examples) (minor)

2869 of 3833 branches covered (0.0%)

5167 of 5882 relevant lines covered (87.84%)

22830.84 hits per line

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

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

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

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

40
const findParentCell = (target) => {
420✔
41
  if (target === undefined || target === null) return;
3,643!
42
  if (
3,643✔
43
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
7,286!
44
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
45
  ) {
46
    return target;
2,843✔
47
  } else {
48
    return findParentCell(target.parentElement);
800✔
49
  }
50
};
51

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

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

65
const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties, data, columns, state } }) => {
420✔
66
  const { showOverlay, tableRef } = webComponentsReactProperties;
34,072✔
67
  const currentlyFocusedCell = useRef<HTMLDivElement>(null);
34,072✔
68
  const noData = data.length === 0;
34,072✔
69

70
  useEffect(() => {
34,072✔
71
    if (showOverlay && currentlyFocusedCell.current) {
1,886!
72
      currentlyFocusedCell.current.tabIndex = -1;
×
73
      currentlyFocusedCell.current = null;
×
74
    }
75
  }, [showOverlay]);
76

77
  const onTableBlur = (e) => {
34,072✔
78
    if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
3,205✔
79
      currentlyFocusedCell.current = null;
256✔
80
    }
81
  };
82

83
  useEffect(() => {
34,072✔
84
    if (
6,027!
85
      !showOverlay &&
25,189!
86
      data &&
87
      columns &&
88
      currentlyFocusedCell.current &&
89
      tableRef.current &&
90
      tableRef.current.tabIndex !== 0 &&
91
      !tableRef.current.contains(currentlyFocusedCell.current)
92
    ) {
93
      currentlyFocusedCell.current = null;
×
94
      tableRef.current.tabIndex = 0;
×
95
    }
96
  }, [data, columns, showOverlay]);
97

98
  const onTableFocus = useCallback(
34,072✔
99
    (e) => {
100
      if (e.target.dataset?.emptyRowCell === 'true' || e.target.dataset.subcomponentActiveElement) {
2,922✔
101
        return;
31✔
102
      }
103
      if (e.target.dataset.subcomponent) {
2,891✔
104
        e.target.tabIndex = 0;
32✔
105
        e.target.focus();
32✔
106
        currentlyFocusedCell.current = e.target;
32✔
107
        return;
32✔
108
      }
109
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
2,859✔
110
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
2,859✔
111
        e.target.tabIndex = -1;
16✔
112
        if (currentlyFocusedCell.current) {
16!
113
          const { dataset } = currentlyFocusedCell.current;
×
114
          const rowIndex = parseInt(dataset.rowIndex ?? dataset.rowIndexSub, 10);
×
115
          const columnIndex = parseInt(dataset.columnIndex ?? dataset.columnIndexSub, 10);
×
116
          if (
×
117
            e.target.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex}"]`) ||
×
118
            e.target.querySelector(`div[data-column-index-sub="${columnIndex}"][data-row-index-sub="${rowIndex}"]`)
119
          ) {
120
            currentlyFocusedCell.current.tabIndex = 0;
×
121
            currentlyFocusedCell.current.focus({ preventScroll: true });
×
122
          } else {
123
            getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
124
          }
125
        } else if (isFirstCellAvailable) {
16!
126
          const firstCell = e.target.querySelector('div[data-column-index="0"][data-row-index="0"]');
16✔
127
          firstCell.tabIndex = 0;
16✔
128
          firstCell.focus({ preventScroll: true });
16✔
129
          currentlyFocusedCell.current = firstCell;
16✔
130
        } else {
131
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
132
        }
133
      } else {
134
        const tableCell = findParentCell(e.target);
2,843✔
135
        if (tableCell) {
2,843!
136
          currentlyFocusedCell.current = tableCell;
2,843✔
137
        } else {
138
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
139
        }
140
      }
141
    },
142
    [currentlyFocusedCell.current, tableRef.current, noData]
143
  );
144

145
  const onKeyboardNavigation = useCallback(
34,072✔
146
    (e) => {
147
      const { isRtl } = state;
312✔
148
      const isActiveItemInSubComponent = e.target.dataset.subcomponentActiveElement;
312✔
149
      // check if target is cell and if so proceed from there
150
      if (
312✔
151
        !currentlyFocusedCell.current &&
385✔
152
        CELL_DATA_ATTRIBUTES.every((item) => Object.keys(e.target.dataset).includes(item))
103✔
153
      ) {
154
        currentlyFocusedCell.current = e.target;
10✔
155
      }
156
      if (currentlyFocusedCell.current) {
312✔
157
        const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex ?? '0', 10);
249✔
158
        const rowIndex = parseInt(
249✔
159
          currentlyFocusedCell.current.dataset.rowIndex ?? currentlyFocusedCell.current.dataset.subcomponentRowIndex,
297✔
160
          10
161
        );
162
        switch (e.key) {
249✔
163
          case 'End': {
164
            e.preventDefault();
4✔
165
            const visibleColumns: HTMLDivElement[] = tableRef.current.querySelector(
4✔
166
              `div[data-component-name="AnalyticalTableHeaderRow"]`
167
            ).children;
168

169
            const lastVisibleColumn = Array.from(visibleColumns)
4✔
170
              .slice(0)
171
              .reduceRight((_, cur, index, arr) => {
172
                const columnIndex = parseInt((cur.children?.[0] as HTMLDivElement)?.dataset.columnIndex, 10);
4✔
173
                if (!isNaN(columnIndex)) {
4✔
174
                  arr.length = 0;
4✔
175
                  return columnIndex;
4✔
176
                }
177
                return 0;
×
178
              }, 0);
179

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

299
            if (hasSubcomponent && !isSubComponent) {
16✔
300
              currentlyFocusedCell.current.tabIndex = -1;
4✔
301
              firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
4✔
302
              firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;
4✔
303
              firstChildPrevRow.tabIndex = 0;
4✔
304
              firstChildPrevRow.focus();
4✔
305
              currentlyFocusedCell.current = firstChildPrevRow;
4✔
306
            } else if (previousRowCell) {
12✔
307
              setFocus(currentlyFocusedCell, previousRowCell);
12✔
308
            }
309
            break;
16✔
310
          }
311
        }
312
      }
313
    },
314
    [currentlyFocusedCell.current, tableRef.current, state?.isRtl]
315
  );
316
  if (showOverlay) {
34,072✔
317
    return tableProps;
204✔
318
  }
319
  return [
33,868✔
320
    tableProps,
321
    {
322
      onFocus: onTableFocus,
323
      onKeyDown: onKeyboardNavigation,
324
      onBlur: onTableBlur
325
    }
326
  ];
327
};
328

329
function getPayload(e, column) {
330
  e.preventDefault();
×
331
  e.stopPropagation();
×
332
  const clientX = e.target.getBoundingClientRect().x + e.target.getBoundingClientRect().width;
×
333
  const columnId = column.id;
×
334
  const columnWidth = column.totalWidth;
×
335
  const headersToResize = getLeafHeaders(column);
×
336
  const headerIdWidths = headersToResize.map((d) => [d.id, d.totalWidth]);
×
337
  return { clientX, columnId, columnWidth, headerIdWidths };
×
338
}
339

340
const setHeaderProps = (headerProps, { instance: { dispatch }, column }) => {
420✔
341
  // resize col with keyboard
342
  const handleKeyDown = (e) => {
110,056✔
343
    if (e.nativeEvent.shiftKey) {
42!
344
      if (e.key === 'ArrowRight') {
×
345
        const payload = getPayload(e, column);
×
346
        dispatch({ type: actions.columnStartResizing, ...payload });
×
347
        dispatch({ type: actions.columnResizing, clientX: payload.clientX + 16 });
×
348
        dispatch({ type: actions.columnDoneResizing });
×
349
        return;
×
350
      }
351
      if (e.key === 'ArrowLeft') {
×
352
        const payload = getPayload(e, column);
×
353
        dispatch({ type: actions.columnStartResizing, ...payload });
×
354
        dispatch({ type: actions.columnResizing, clientX: payload.clientX - 16 });
×
355
        dispatch({ type: actions.columnDoneResizing });
×
356
        return;
×
357
      }
358
    }
359
  };
360
  return [headerProps, { onKeyDown: handleKeyDown }];
110,056✔
361
};
362

363
export const useKeyboardNavigation = (hooks: ReactTableHooks) => {
420✔
364
  hooks.getTableProps.push(useGetTableProps);
34,072✔
365
  hooks.getHeaderProps.push(setHeaderProps);
34,072✔
366
};
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