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

SAP / ui5-webcomponents-react / 9773804215

03 Jul 2024 07:39AM CUT coverage: 81.216% (-0.02%) from 81.233%
9773804215

Pull #6012

github

web-flow
Merge eb1fd64d2 into c2c3730e7
Pull Request #6012: refactor(FilterGroupItem): api alignment

2629 of 3824 branches covered (68.75%)

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

1 existing line in 1 file now uncovered.

4743 of 5840 relevant lines covered (81.22%)

68573.13 hits per line

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

77.61
/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'];
399✔
7

8
const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => {
399✔
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) {
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) => {
399✔
41
  if (target === undefined || target === null) return;
4,547!
42
  if (
4,547✔
43
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
9,094!
44
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
45
  ) {
46
    return target;
3,513✔
47
  } else {
48
    return findParentCell(target.parentElement);
1,034✔
49
  }
50
};
51

52
const setFocus = (currentlyFocusedCell, nextElement) => {
399✔
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) => {
399✔
62
  setFocus(currentlyFocusedCell, recursiveSubComponentElementSearch(e.target));
32✔
63
};
64

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

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

77
  const onTableBlur = (e) => {
45,924✔
78
    if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
5,067✔
79
      currentlyFocusedCell.current = null;
675✔
80
    }
81
  };
82

83
  useEffect(() => {
45,924✔
84
    if (
9,503!
85
      !showOverlay &&
38,447!
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(
45,924✔
99
    (e) => {
100
      const { dataset } = e.target;
4,094✔
101
      if (
4,094✔
102
        dataset.emptyRowCell === 'true' ||
16,251✔
103
        dataset.hasOwnProperty('subcomponentActiveElement') ||
104
        // todo: with the new popover API of ui5wc this might not be necessary anymore
105
        dataset.componentName === 'ATHeaderPopoverList' ||
106
        dataset.componentName === 'ATHeaderPopover'
107
      ) {
108
        return;
457✔
109
      }
110
      if (e.target.dataset.subcomponent) {
3,637✔
111
        e.target.tabIndex = 0;
76✔
112
        e.target.focus();
76✔
113
        currentlyFocusedCell.current = e.target;
76✔
114
        return;
76✔
115
      }
116
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
3,561✔
117
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
3,561✔
118
        e.target.tabIndex = -1;
48✔
119
        if (currentlyFocusedCell.current) {
48!
120
          const { dataset } = currentlyFocusedCell.current;
×
121
          const rowIndex = parseInt(dataset.rowIndex ?? dataset.rowIndexSub, 10);
×
122
          const columnIndex = parseInt(dataset.columnIndex ?? dataset.columnIndexSub, 10);
×
123
          if (
×
124
            e.target.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex}"]`) ||
×
125
            e.target.querySelector(`div[data-column-index-sub="${columnIndex}"][data-row-index-sub="${rowIndex}"]`)
126
          ) {
127
            currentlyFocusedCell.current.tabIndex = 0;
×
128
            currentlyFocusedCell.current.focus({ preventScroll: true });
×
129
          } else {
130
            getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
131
          }
132
        } else if (isFirstCellAvailable) {
48!
133
          const firstCell = e.target.querySelector(
48✔
134
            'div[data-column-index]:not([data-column-id^="__ui5wcr__internal"][data-row-index="0"])'
135
          );
136
          firstCell.tabIndex = 0;
48✔
137
          firstCell.focus({ preventScroll: true });
48✔
138
          currentlyFocusedCell.current = firstCell;
48✔
139
        } else {
140
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
141
        }
142
      } else {
143
        const tableCell = findParentCell(e.target);
3,513✔
144
        if (tableCell) {
3,513!
145
          currentlyFocusedCell.current = tableCell;
3,513✔
146
        } else {
147
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
148
        }
149
      }
150
    },
151
    [currentlyFocusedCell.current, tableRef.current, noData]
152
  );
153

154
  const onKeyboardNavigation = useCallback(
45,924✔
155
    (e) => {
156
      const { isRtl } = state;
528✔
157
      const isActiveItemInSubComponent = e.target.dataset.hasOwnProperty('subcomponentActiveElement');
528✔
158
      // check if target is cell and if so proceed from there
159
      if (
528!
160
        !currentlyFocusedCell.current &&
671✔
161
        CELL_DATA_ATTRIBUTES.every((item) => Object.keys(e.target.dataset).includes(item))
143✔
162
      ) {
UNCOV
163
        currentlyFocusedCell.current = e.target;
×
164
      }
165
      if (currentlyFocusedCell.current) {
528✔
166
        const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex ?? '0', 10);
385✔
167
        const rowIndex = parseInt(
385✔
168
          currentlyFocusedCell.current.dataset.rowIndex ?? currentlyFocusedCell.current.dataset.subcomponentRowIndex,
481✔
169
          10
170
        );
171
        switch (e.key) {
385✔
172
          case 'End': {
173
            e.preventDefault();
8✔
174
            const visibleColumns: HTMLDivElement[] = tableRef.current.querySelector(
8✔
175
              `div[data-component-name="AnalyticalTableHeaderRow"]`
176
            ).children;
177

178
            const lastVisibleColumn = Array.from(visibleColumns)
8✔
179
              .slice(0)
180
              .reduceRight((_, cur, index, arr) => {
181
                const columnIndex = parseInt((cur.children?.[0] as HTMLDivElement)?.dataset.columnIndex, 10);
8✔
182
                if (!isNaN(columnIndex)) {
8✔
183
                  arr.length = 0;
8✔
184
                  return columnIndex;
8✔
185
                }
186
                return 0;
×
187
              }, 0);
188

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

308
            if (hasSubcomponent && !isSubComponent) {
32✔
309
              currentlyFocusedCell.current.tabIndex = -1;
8✔
310
              firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
8✔
311
              firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;
8✔
312
              firstChildPrevRow.tabIndex = 0;
8✔
313
              firstChildPrevRow.focus();
8✔
314
              currentlyFocusedCell.current = firstChildPrevRow;
8✔
315
            } else if (previousRowCell) {
24✔
316
              setFocus(currentlyFocusedCell, previousRowCell);
24✔
317
            }
318
            break;
32✔
319
          }
320
        }
321
      }
322
    },
323
    [currentlyFocusedCell.current, tableRef.current, state?.isRtl]
324
  );
325
  if (showOverlay) {
45,924✔
326
    return tableProps;
210✔
327
  }
328
  return [
45,714✔
329
    tableProps,
330
    {
331
      onFocus: onTableFocus,
332
      onKeyDown: onKeyboardNavigation,
333
      onBlur: onTableBlur
334
    }
335
  ];
336
};
337

338
function getPayload(e, column) {
339
  e.preventDefault();
×
340
  e.stopPropagation();
×
341
  const clientX = e.target.getBoundingClientRect().x + e.target.getBoundingClientRect().width;
×
342
  const columnId = column.id;
×
343
  const columnWidth = column.totalWidth;
×
344
  const headersToResize = getLeafHeaders(column);
×
345
  const headerIdWidths = headersToResize.map((d) => [d.id, d.totalWidth]);
×
346
  return { clientX, columnId, columnWidth, headerIdWidths };
×
347
}
348

349
const setHeaderProps = (headerProps, { instance: { dispatch }, column }) => {
399✔
350
  // resize col with keyboard
351
  const handleKeyDown = (e) => {
146,477✔
352
    if (e.nativeEvent.shiftKey) {
78!
353
      if (e.key === 'ArrowRight') {
×
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
      if (e.key === 'ArrowLeft') {
×
361
        const payload = getPayload(e, column);
×
362
        dispatch({ type: actions.columnStartResizing, ...payload });
×
363
        dispatch({ type: actions.columnResizing, clientX: payload.clientX - 16 });
×
364
        dispatch({ type: actions.columnDoneResizing });
×
365
        return;
×
366
      }
367
    }
368
  };
369
  return [headerProps, { onKeyDown: handleKeyDown }];
146,477✔
370
};
371

372
export const useKeyboardNavigation = (hooks: ReactTableHooks) => {
399✔
373
  hooks.getTableProps.push(useGetTableProps);
45,924✔
374
  hooks.getHeaderProps.push(setHeaderProps);
45,924✔
375
};
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