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

SAP / ui5-webcomponents-react / 9092421211

15 May 2024 08:25AM CUT coverage: 88.495% (-0.08%) from 88.577%
9092421211

Pull #5798

github

web-flow
Merge 2b5edc38d into 833dae9c6
Pull Request #5798: fix(VariantManagement): fix "Manage Views" default validation

3023 of 3985 branches covered (75.86%)

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

5 existing lines in 1 file now uncovered.

5423 of 6128 relevant lines covered (88.5%)

29034.26 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'];
444✔
7

8
const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => {
444✔
UNCOV
9
  if (
×
10
    target.dataset.componentName === 'AnalyticalTableContainer' &&
×
11
    target.querySelector('[data-component-name="AnalyticalTableBodyScrollableContainer"]')
12
  ) {
UNCOV
13
    const rowElements = target.querySelector('[data-component-name="AnalyticalTableBodyScrollableContainer"]').children;
×
UNCOV
14
    const middleRowCell = target.querySelector(
×
15
      `div[data-visible-column-index="0"][data-visible-row-index="${Math.round(rowElements.length / 2)}"]`
16
    );
UNCOV
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) => {
444✔
41
  if (target === undefined || target === null) return;
4,383!
42
  if (
4,383✔
43
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
8,766!
44
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
45
  ) {
46
    return target;
3,443✔
47
  } else {
48
    return findParentCell(target.parentElement);
940✔
49
  }
50
};
51

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

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

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

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

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

83
  useEffect(() => {
44,520✔
84
    if (
8,193!
85
      !showOverlay &&
34,201!
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(
44,520✔
99
    (e) => {
100
      if (e.target.dataset?.emptyRowCell === 'true' || e.target.dataset.subcomponentActiveElement) {
3,622✔
101
        return;
55✔
102
      }
103
      if (e.target.dataset.subcomponent) {
3,567✔
104
        e.target.tabIndex = 0;
76✔
105
        e.target.focus();
76✔
106
        currentlyFocusedCell.current = e.target;
76✔
107
        return;
76✔
108
      }
109
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
3,491✔
110
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
3,491✔
111
        e.target.tabIndex = -1;
48✔
112
        if (currentlyFocusedCell.current) {
48!
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) {
48!
126
          const firstCell = e.target.querySelector(
48✔
127
            'div[data-column-index]:not([data-column-id^="__ui5wcr__internal"][data-row-index="0"])'
128
          );
129
          firstCell.tabIndex = 0;
48✔
130
          firstCell.focus({ preventScroll: true });
48✔
131
          currentlyFocusedCell.current = firstCell;
48✔
132
        } else {
133
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
134
        }
135
      } else {
136
        const tableCell = findParentCell(e.target);
3,443✔
137
        if (tableCell) {
3,443!
138
          currentlyFocusedCell.current = tableCell;
3,443✔
139
        } else {
UNCOV
140
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
141
        }
142
      }
143
    },
144
    [currentlyFocusedCell.current, tableRef.current, noData]
145
  );
146

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

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

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

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

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

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

365
export const useKeyboardNavigation = (hooks: ReactTableHooks) => {
444✔
366
  hooks.getTableProps.push(useGetTableProps);
44,520✔
367
  hooks.getHeaderProps.push(setHeaderProps);
44,520✔
368
};
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