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

SAP / ui5-webcomponents-react / 5803032967

pending completion
5803032967

Pull #4955

github

web-flow
Merge c60a1e1b9 into ca3033c02
Pull Request #4955: fix(deps): update dependency recharts to v2.7.3

2672 of 3610 branches covered (74.02%)

4949 of 5637 relevant lines covered (87.79%)

17051.39 hits per line

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

81.2
/packages/main/src/components/AnalyticalTable/hooks/useKeyboardNavigation.ts
1
import { useCallback, useEffect, useRef } from 'react';
2

3
const CELL_DATA_ATTRIBUTES = ['visibleColumnIndex', 'columnIndex', 'rowIndex', 'visibleRowIndex'];
380✔
4

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

27
function recursiveSubComponentElementSearch(element) {
28
  if (!element.parentElement) {
24!
29
    return null;
×
30
  }
31
  if (element?.parentElement.dataset.subcomponent) {
24✔
32
    return element.parentElement;
12✔
33
  }
34
  return recursiveSubComponentElementSearch(element.parentElement);
12✔
35
}
36

37
const findParentCell = (target) => {
380✔
38
  if (target === undefined || target === null) return;
3,092!
39
  if (
3,092✔
40
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
6,184!
41
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
42
  ) {
43
    return target;
2,459✔
44
  } else {
45
    return findParentCell(target.parentElement);
633✔
46
  }
47
};
48

49
const setFocus = (currentlyFocusedCell, nextElement) => {
380✔
50
  currentlyFocusedCell.current.tabIndex = -1;
84✔
51
  if (nextElement) {
84✔
52
    nextElement.tabIndex = 0;
84✔
53
    nextElement.focus();
84✔
54
    currentlyFocusedCell.current = nextElement;
84✔
55
  }
56
};
57

58
const navigateFromActiveSubCompItem = (currentlyFocusedCell, e) => {
380✔
59
  setFocus(currentlyFocusedCell, recursiveSubComponentElementSearch(e.target));
12✔
60
};
61

62
const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties, data, columns } }) => {
380✔
63
  const { showOverlay, tableRef } = webComponentsReactProperties;
31,910✔
64
  const currentlyFocusedCell = useRef<HTMLDivElement>(null);
31,910✔
65
  const noData = data.length === 0;
31,910✔
66

67
  useEffect(() => {
31,910✔
68
    if (showOverlay && currentlyFocusedCell.current) {
1,697!
69
      currentlyFocusedCell.current.tabIndex = -1;
×
70
      currentlyFocusedCell.current = null;
×
71
    }
72
  }, [showOverlay]);
73

74
  const onTableBlur = (e) => {
31,910✔
75
    if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
2,884✔
76
      currentlyFocusedCell.current = null;
327✔
77
    }
78
  };
79

80
  useEffect(() => {
31,910✔
81
    if (
6,791!
82
      !showOverlay &&
28,296!
83
      data &&
84
      columns &&
85
      currentlyFocusedCell.current &&
86
      tableRef.current &&
87
      tableRef.current.tabIndex !== 0 &&
88
      !tableRef.current.contains(currentlyFocusedCell.current)
89
    ) {
90
      currentlyFocusedCell.current = null;
×
91
      tableRef.current.tabIndex = 0;
×
92
    }
93
  }, [data, columns, showOverlay]);
94

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

142
  const onKeyboardNavigation = useCallback(
31,910✔
143
    (e) => {
144
      const isActiveItemInSubComponent = e.target.dataset.subcomponentActiveElement;
211✔
145
      // check if target is cell and if so proceed from there
146
      if (
211!
147
        !currentlyFocusedCell.current &&
267✔
148
        CELL_DATA_ATTRIBUTES.every((item) => Object.keys(e.target.dataset).includes(item))
56✔
149
      ) {
150
        currentlyFocusedCell.current = e.target;
×
151
      }
152
      if (currentlyFocusedCell.current) {
211✔
153
        const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex ?? '0', 10);
155✔
154
        const rowIndex = parseInt(
155✔
155
          currentlyFocusedCell.current.dataset.rowIndex ?? currentlyFocusedCell.current.dataset.subcomponentRowIndex,
191✔
156
          10
157
        );
158
        switch (e.key) {
155✔
159
          case 'End': {
160
            e.preventDefault();
3✔
161
            const visibleColumns: HTMLDivElement[] = tableRef.current.querySelector(
3✔
162
              `div[data-component-name="AnalyticalTableHeaderRow"]`
163
            ).children;
164

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

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

295
            if (hasSubcomponent && !isSubComponent) {
12✔
296
              currentlyFocusedCell.current.tabIndex = -1;
3✔
297
              firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
3✔
298
              firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;
3✔
299
              firstChildPrevRow.tabIndex = 0;
3✔
300
              firstChildPrevRow.focus();
3✔
301
              currentlyFocusedCell.current = firstChildPrevRow;
3✔
302
            } else if (previousRowCell) {
9✔
303
              setFocus(currentlyFocusedCell, previousRowCell);
9✔
304
            }
305
            break;
12✔
306
          }
307
        }
308
      }
309
    },
310
    [currentlyFocusedCell.current, tableRef.current]
311
  );
312
  if (showOverlay) {
31,910✔
313
    return tableProps;
208✔
314
  }
315
  return [
31,702✔
316
    tableProps,
317
    {
318
      onFocus: onTableFocus,
319
      onKeyDown: onKeyboardNavigation,
320
      onBlur: onTableBlur
321
    }
322
  ];
323
};
324

325
export const useKeyboardNavigation = (hooks) => {
380✔
326
  hooks.getTableProps.push(useGetTableProps);
31,910✔
327
};
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