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

SAP / ui5-webcomponents-react / 12009185407

25 Nov 2024 11:35AM CUT coverage: 87.142% (-0.02%) from 87.16%
12009185407

push

github

web-flow
fix(NavigationLayout): add root export (#6657)

2901 of 3864 branches covered (75.08%)

5056 of 5802 relevant lines covered (87.14%)

50139.75 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'];
418✔
7

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

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

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

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

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

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

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

86
  useEffect(() => {
48,580✔
87
    if (
10,172!
88
      !showOverlay &&
41,164!
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(
48,580✔
102
    (e) => {
103
      const { dataset } = e.target;
4,421✔
104
      if (
4,421✔
105
        dataset.emptyRowCell === 'true' ||
17,546✔
106
        Object.prototype.hasOwnProperty.call(dataset, '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;
453✔
112
      }
113
      if (e.target.dataset.subcomponent) {
3,968✔
114
        e.target.tabIndex = 0;
87✔
115
        e.target.focus();
87✔
116
        currentlyFocusedCell.current = e.target;
87✔
117
        return;
87✔
118
      }
119
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
3,881✔
120
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
3,881✔
121
        e.target.tabIndex = -1;
54✔
122
        if (currentlyFocusedCell.current) {
54!
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) {
54!
136
          const firstCell = e.target.querySelector(
54✔
137
            'div[data-column-index]:not([data-column-id^="__ui5wcr__internal"][data-row-index="0"])'
138
          );
139
          firstCell.tabIndex = 0;
54✔
140
          firstCell.focus({ preventScroll: true });
54✔
141
          currentlyFocusedCell.current = firstCell;
54✔
142
        } else {
143
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
144
        }
145
      } else {
146
        const tableCell = findParentCell(e.target);
3,827✔
147
        if (tableCell) {
3,827!
148
          currentlyFocusedCell.current = tableCell;
3,827✔
149
        } else {
150
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
151
        }
152
      }
153
    },
154
    [currentlyFocusedCell.current, tableRef.current, noData]
155
  );
156

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

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

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

314
            if (hasSubcomponent && !isSubComponent) {
79✔
315
              currentlyFocusedCell.current.tabIndex = -1;
9✔
316
              firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
9✔
317
              firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;
9✔
318
              firstChildPrevRow.tabIndex = 0;
9✔
319
              firstChildPrevRow.focus();
9✔
320
              currentlyFocusedCell.current = firstChildPrevRow;
9✔
321
            } else if (previousRowCell) {
70✔
322
              setFocus(currentlyFocusedCell, previousRowCell);
70✔
323
            }
324
            break;
79✔
325
          }
326
        }
327
      }
328
    },
329
    [currentlyFocusedCell.current, tableRef.current, state?.isRtl]
330
  );
331
  if (showOverlay) {
48,580✔
332
    return tableProps;
220✔
333
  }
334
  return [
48,360✔
335
    tableProps,
336
    {
337
      onFocus: onTableFocus,
338
      onKeyDown: onKeyboardNavigation,
339
      onBlur: onTableBlur
340
    }
341
  ];
342
};
343

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

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

384
export const useKeyboardNavigation = (hooks: ReactTableHooks) => {
418✔
385
  hooks.getTableProps.push(useGetTableProps);
48,580✔
386
  hooks.getHeaderProps.push(setHeaderProps);
48,580✔
387
};
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