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

SAP / ui5-webcomponents-react / 6846558936

13 Nov 2023 06:39AM CUT coverage: 87.674% (-0.09%) from 87.76%
6846558936

Pull #5236

github

web-flow
Merge 31a7866d2 into 9fa137e8b
Pull Request #5236: chore(deps): update all non-major dependencies (patch)

2789 of 3745 branches covered (0.0%)

5100 of 5817 relevant lines covered (87.67%)

31725.11 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 { getLeafHeaders } from '../util/index.js';
4

5
const CELL_DATA_ATTRIBUTES = ['visibleColumnIndex', 'columnIndex', 'rowIndex', 'visibleRowIndex'];
395✔
6

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

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

39
const findParentCell = (target) => {
395✔
40
  if (target === undefined || target === null) return;
3,547!
41
  if (
3,547✔
42
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
7,094!
43
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
44
  ) {
45
    return target;
2,811✔
46
  } else {
47
    return findParentCell(target.parentElement);
736✔
48
  }
49
};
50

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

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

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

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

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

82
  useEffect(() => {
35,276✔
83
    if (
5,985!
84
      !showOverlay &&
24,935!
85
      data &&
86
      columns &&
87
      currentlyFocusedCell.current &&
88
      tableRef.current &&
89
      tableRef.current.tabIndex !== 0 &&
90
      !tableRef.current.contains(currentlyFocusedCell.current)
91
    ) {
92
      currentlyFocusedCell.current = null;
×
93
      tableRef.current.tabIndex = 0;
×
94
    }
95
  }, [data, columns, showOverlay]);
96

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

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

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

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

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

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

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

362
export const useKeyboardNavigation = (hooks) => {
395✔
363
  hooks.getTableProps.push(useGetTableProps);
35,276✔
364
  hooks.getHeaderProps.push(setHeaderProps);
35,276✔
365
};
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