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

SAP / ui5-webcomponents-react / 7244475872

18 Dec 2023 06:29AM CUT coverage: 87.923% (-0.08%) from 88.007%
7244475872

Pull #5357

github

web-flow
Merge db02cbaf0 into b3001567c
Pull Request #5357: chore(deps): update all non-major dependencies (examples) (patch)

2895 of 3860 branches covered (0.0%)

5198 of 5912 relevant lines covered (87.92%)

24079.22 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'];
421✔
7

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

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

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

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

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

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

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

83
  useEffect(() => {
35,588✔
84
    if (
6,303!
85
      !showOverlay &&
26,340!
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(
35,588✔
99
    (e) => {
100
      if (e.target.dataset?.emptyRowCell === 'true' || e.target.dataset.subcomponentActiveElement) {
3,076✔
101
        return;
37✔
102
      }
103
      if (e.target.dataset.subcomponent) {
3,039✔
104
        e.target.tabIndex = 0;
40✔
105
        e.target.focus();
40✔
106
        currentlyFocusedCell.current = e.target;
40✔
107
        return;
40✔
108
      }
109
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
2,999✔
110
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
2,999✔
111
        e.target.tabIndex = -1;
20✔
112
        if (currentlyFocusedCell.current) {
20!
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) {
20!
126
          const firstCell = e.target.querySelector('div[data-column-index="0"][data-row-index="0"]');
20✔
127
          firstCell.tabIndex = 0;
20✔
128
          firstCell.focus({ preventScroll: true });
20✔
129
          currentlyFocusedCell.current = firstCell;
20✔
130
        } else {
131
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
132
        }
133
      } else {
134
        const tableCell = findParentCell(e.target);
2,979✔
135
        if (tableCell) {
2,979!
136
          currentlyFocusedCell.current = tableCell;
2,979✔
137
        } else {
138
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
139
        }
140
      }
141
    },
142
    [currentlyFocusedCell.current, tableRef.current, noData]
143
  );
144

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

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

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

299
            if (hasSubcomponent && !isSubComponent) {
20✔
300
              currentlyFocusedCell.current.tabIndex = -1;
5✔
301
              firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
5✔
302
              firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;
5✔
303
              firstChildPrevRow.tabIndex = 0;
5✔
304
              firstChildPrevRow.focus();
5✔
305
              currentlyFocusedCell.current = firstChildPrevRow;
5✔
306
            } else if (previousRowCell) {
15✔
307
              setFocus(currentlyFocusedCell, previousRowCell);
15✔
308
            }
309
            break;
20✔
310
          }
311
        }
312
      }
313
    },
314
    [currentlyFocusedCell.current, tableRef.current, state?.isRtl]
315
  );
316
  if (showOverlay) {
35,588✔
317
    return tableProps;
216✔
318
  }
319
  return [
35,372✔
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 }) => {
421✔
341
  // resize col with keyboard
342
  const handleKeyDown = (e) => {
115,410✔
343
    if (e.nativeEvent.shiftKey) {
51!
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 }];
115,410✔
361
};
362

363
export const useKeyboardNavigation = (hooks: ReactTableHooks) => {
421✔
364
  hooks.getTableProps.push(useGetTableProps);
35,588✔
365
  hooks.getHeaderProps.push(setHeaderProps);
35,588✔
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