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

SAP / ui5-webcomponents-react / 5256780421

pending completion
5256780421

Pull #4714

github

web-flow
Merge de3ab2640 into d4ddb9c0c
Pull Request #4714: docs: add NextJS app router example

2642 of 3617 branches covered (73.04%)

5123 of 5923 relevant lines covered (86.49%)

16227.92 hits per line

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

25.64
/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'];
378✔
4

5
const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => {
378✔
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
const findParentCell = (target) => {
378✔
28
  if (target === undefined || target === null) return;
2,368!
29
  if (
2,368✔
30
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
4,736!
31
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
32
  ) {
33
    return target;
1,766✔
34
  } else {
35
    return findParentCell(target.parentElement);
602✔
36
  }
37
};
38

39
const setFocus = (currentlyFocusedCell, nextElement) => {
378✔
40
  currentlyFocusedCell.current.tabIndex = -1;
×
41
  if (nextElement) {
×
42
    nextElement.tabIndex = 0;
×
43
    nextElement.focus();
×
44
    currentlyFocusedCell.current = nextElement;
×
45
  }
46
};
47

48
const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties, data, columns } }) => {
378✔
49
  const { showOverlay, tableRef } = webComponentsReactProperties;
28,802✔
50
  const currentlyFocusedCell = useRef<HTMLDivElement>(null);
28,802✔
51
  const noData = data.length === 0;
28,802✔
52

53
  useEffect(() => {
28,802✔
54
    if (showOverlay && currentlyFocusedCell.current) {
1,612!
55
      currentlyFocusedCell.current.tabIndex = -1;
×
56
      currentlyFocusedCell.current = null;
×
57
    }
58
  }, [showOverlay]);
59

60
  const onTableBlur = (e) => {
28,802✔
61
    if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
1,804✔
62
      currentlyFocusedCell.current = null;
132✔
63
    }
64
  };
65

66
  useEffect(() => {
28,802✔
67
    if (
6,290!
68
      !showOverlay &&
26,026!
69
      data &&
70
      columns &&
71
      currentlyFocusedCell.current &&
72
      tableRef.current &&
73
      tableRef.current.tabIndex !== 0 &&
74
      !tableRef.current.contains(currentlyFocusedCell.current)
75
    ) {
76
      currentlyFocusedCell.current = null;
×
77
      tableRef.current.tabIndex = 0;
×
78
    }
79
  }, [data, columns, showOverlay]);
80

81
  const onTableFocus = useCallback(
28,802✔
82
    (e) => {
83
      if (e.target.dataset?.emptyRowCell === 'true') {
1,775✔
84
        return;
9✔
85
      }
86
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
1,766✔
87
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
1,766!
88
        e.target.tabIndex = -1;
×
89
        if (currentlyFocusedCell.current) {
×
90
          const { dataset } = currentlyFocusedCell.current;
×
91
          const rowIndex = parseInt(dataset.rowIndex ?? dataset.rowIndexSub, 10);
×
92
          const columnIndex = parseInt(dataset.columnIndex ?? dataset.columnIndexSub, 10);
×
93
          if (
×
94
            e.target.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex}"]`) ||
×
95
            e.target.querySelector(`div[data-column-index-sub="${columnIndex}"][data-row-index-sub="${rowIndex}"]`)
96
          ) {
97
            currentlyFocusedCell.current.tabIndex = 0;
×
98
            currentlyFocusedCell.current.focus({ preventScroll: true });
×
99
          } else {
100
            getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
101
          }
102
        } else if (isFirstCellAvailable) {
×
103
          const firstCell = e.target.querySelector('div[data-column-index="0"][data-row-index="0"]');
×
104
          firstCell.tabIndex = 0;
×
105
          firstCell.focus({ preventScroll: true });
×
106
          currentlyFocusedCell.current = firstCell;
×
107
        } else {
108
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
109
        }
110
      } else {
111
        const tableCell = findParentCell(e.target);
1,766✔
112
        if (tableCell) {
1,766!
113
          currentlyFocusedCell.current = tableCell;
1,766✔
114
        } else {
115
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
116
        }
117
      }
118
    },
119
    [currentlyFocusedCell.current, tableRef.current, noData]
120
  );
121

122
  const onKeyboardNavigation = useCallback(
28,802✔
123
    (e) => {
124
      // check if target is cell and if so proceed from there
125
      if (
77!
126
        !currentlyFocusedCell.current &&
154✔
127
        CELL_DATA_ATTRIBUTES.every((item) => Object.keys(e.target.dataset).includes(item))
77✔
128
      ) {
129
        currentlyFocusedCell.current = e.target;
×
130
      }
131
      if (currentlyFocusedCell.current) {
77!
132
        const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex, 10);
×
133
        const rowIndex = parseInt(currentlyFocusedCell.current.dataset.rowIndex, 10);
×
134
        switch (e.key) {
×
135
          case 'End': {
136
            e.preventDefault();
×
137
            const visibleColumns: HTMLDivElement[] = tableRef.current.querySelector(
×
138
              `div[data-component-name="AnalyticalTableHeaderRow"]`
139
            ).children;
140
            const lastVisibleColumn = Array.from(visibleColumns)
×
141
              .slice(0)
142
              .reduceRight((_, cur, index, arr) => {
143
                const columnIndex = parseInt((cur.children?.[0] as HTMLDivElement)?.dataset.columnIndex, 10);
×
144
                if (!isNaN(columnIndex)) {
×
145
                  arr.length = 0;
×
146
                  return columnIndex;
×
147
                }
148
                return 0;
×
149
              }, 0);
150

151
            const newElement = tableRef.current.querySelector(
×
152
              `div[data-visible-column-index="${lastVisibleColumn + 1}"][data-row-index="${rowIndex}"]`
153
            );
154
            setFocus(currentlyFocusedCell, newElement);
×
155
            break;
×
156
          }
157
          case 'Home': {
158
            e.preventDefault();
×
159
            const newElement = tableRef.current.querySelector(
×
160
              `div[data-visible-column-index="0"][data-row-index="${rowIndex}"]`
161
            );
162
            setFocus(currentlyFocusedCell, newElement);
×
163
            break;
×
164
          }
165
          case 'PageDown': {
166
            e.preventDefault();
×
167
            if (currentlyFocusedCell.current.dataset.rowIndex === '0') {
×
168
              const newElement = tableRef.current.querySelector(
×
169
                `div[data-column-index="${columnIndex}"][data-row-index="${rowIndex + 1}"]`
170
              );
171
              setFocus(currentlyFocusedCell, newElement);
×
172
            } else {
173
              const lastVisibleRow = tableRef.current.querySelector(`div[data-component-name="AnalyticalTableBody"]`)
×
174
                ?.children?.[0].children.length;
175
              const newElement = tableRef.current.querySelector(
×
176
                `div[data-column-index="${columnIndex}"][data-visible-row-index="${lastVisibleRow}"]`
177
              );
178
              setFocus(currentlyFocusedCell, newElement);
×
179
            }
180
            break;
×
181
          }
182
          case 'PageUp': {
183
            e.preventDefault();
×
184
            if (currentlyFocusedCell.current.dataset.rowIndex <= '1') {
×
185
              const newElement = tableRef.current.querySelector(
×
186
                `div[data-column-index="${columnIndex}"][data-row-index="0"]`
187
              );
188
              setFocus(currentlyFocusedCell, newElement);
×
189
            } else {
190
              const newElement = tableRef.current.querySelector(
×
191
                `div[data-column-index="${columnIndex}"][data-visible-row-index="1"]`
192
              );
193
              setFocus(currentlyFocusedCell, newElement);
×
194
            }
195
            break;
×
196
          }
197
          case 'ArrowRight': {
198
            e.preventDefault();
×
199
            const newElement = tableRef.current.querySelector(
×
200
              `div[data-column-index="${columnIndex + 1}"][data-row-index="${rowIndex}"]`
201
            );
202
            if (newElement) {
×
203
              setFocus(currentlyFocusedCell, newElement);
×
204
              // scroll to show full cell if it's only partial visible
205
              newElement.scrollIntoView({ block: 'nearest' });
×
206
            }
207
            break;
×
208
          }
209
          case 'ArrowLeft': {
210
            e.preventDefault();
×
211
            const newElement = tableRef.current.querySelector(
×
212
              `div[data-column-index="${columnIndex - 1}"][data-row-index="${rowIndex}"]`
213
            );
214
            if (newElement) {
×
215
              setFocus(currentlyFocusedCell, newElement);
×
216
              // scroll to show full cell if it's only partial visible
217
              newElement.scrollIntoView({ block: 'nearest' });
×
218
            }
219
            break;
×
220
          }
221
          case 'ArrowDown': {
222
            e.preventDefault();
×
223
            const parent = currentlyFocusedCell.current.parentElement as HTMLDivElement;
×
224
            const firstChildOfParent = parent?.children?.[0] as HTMLDivElement;
×
225
            const hasSubcomponent = firstChildOfParent?.dataset?.subcomponent;
×
226
            const newElement = tableRef.current.querySelector(
×
227
              `div[data-column-index="${columnIndex}"][data-row-index="${rowIndex + 1}"]`
228
            );
229
            if (hasSubcomponent && !currentlyFocusedCell.current?.dataset?.subcomponent) {
×
230
              currentlyFocusedCell.current.tabIndex = -1;
×
231
              firstChildOfParent.tabIndex = 0;
×
232
              firstChildOfParent.dataset.rowIndexSub = `${rowIndex}`;
×
233
              firstChildOfParent.dataset.columnIndexSub = `${columnIndex}`;
×
234
              firstChildOfParent.focus();
×
235
              currentlyFocusedCell.current = firstChildOfParent;
×
236
            } else if (newElement) {
×
237
              setFocus(currentlyFocusedCell, newElement);
×
238
            } else if (e.target.dataset.subcomponent) {
×
239
              const nextElementToSubComp = tableRef.current.querySelector(
×
240
                `div[data-column-index="${parseInt(e.target.dataset.columnIndexSub)}"][data-row-index="${
241
                  parseInt(e.target.dataset.rowIndexSub) + 1
242
                }"]`
243
              );
244
              setFocus(currentlyFocusedCell, nextElementToSubComp);
×
245
            }
246
            break;
×
247
          }
248
          case 'ArrowUp': {
249
            e.preventDefault();
×
250
            const previousRowCell = tableRef.current.querySelector(
×
251
              `div[data-column-index="${columnIndex}"][data-row-index="${rowIndex - 1}"]`
252
            );
253
            const firstChildPrevRow = previousRowCell?.parentElement.children[0] as HTMLDivElement;
×
254
            const hasSubcomponent = firstChildPrevRow?.dataset?.subcomponent;
×
255

256
            if (currentlyFocusedCell.current?.dataset?.subcomponent) {
×
257
              currentlyFocusedCell.current.tabIndex = -1;
×
258
              const newElement = tableRef.current.querySelector(
×
259
                `div[data-column-index="${parseInt(e.target.dataset.columnIndexSub)}"][data-row-index="${parseInt(
260
                  e.target.dataset.rowIndexSub
261
                )}"]`
262
              );
263
              newElement.tabIndex = 0;
×
264
              newElement.focus();
×
265
              currentlyFocusedCell.current = newElement;
×
266
            } else if (hasSubcomponent) {
×
267
              currentlyFocusedCell.current.tabIndex = -1;
×
268
              firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`;
×
269
              firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`;
×
270
              firstChildPrevRow.tabIndex = 0;
×
271
              firstChildPrevRow.focus();
×
272
              currentlyFocusedCell.current = firstChildPrevRow;
×
273
            } else if (previousRowCell) {
×
274
              setFocus(currentlyFocusedCell, previousRowCell);
×
275
            }
276
            break;
×
277
          }
278
        }
279
      }
280
    },
281
    [currentlyFocusedCell.current, tableRef.current]
282
  );
283
  if (showOverlay) {
28,802✔
284
    return tableProps;
195✔
285
  }
286
  return [
28,607✔
287
    tableProps,
288
    {
289
      onFocus: onTableFocus,
290
      onKeyDown: onKeyboardNavigation,
291
      onBlur: onTableBlur
292
    }
293
  ];
294
};
295

296
export const useKeyboardNavigation = (hooks) => {
378✔
297
  hooks.getTableProps.push(useGetTableProps);
28,802✔
298
};
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