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

SAP / ui5-webcomponents-react / 5509926841

pending completion
5509926841

Pull #4862

github

web-flow
Merge e73cbea8e into 81f4200ce
Pull Request #4862: fix(AnalyticalTable): fix nested `manualGroupBy`

2594 of 3584 branches covered (72.38%)

4928 of 5748 relevant lines covered (85.73%)

16537.46 hits per line

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

24.44
/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
function recursiveSubComponentElementSearch(element) {
28
  if (!element.parentElement) {
×
29
    return null;
×
30
  }
31
  if (element?.parentElement.dataset.subcomponent) {
×
32
    return element.parentElement;
×
33
  }
34
  return recursiveSubComponentElementSearch(element.parentElement);
×
35
}
36

37
const findParentCell = (target) => {
378✔
38
  if (target === undefined || target === null) return;
2,368!
39
  if (
2,368✔
40
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
4,736!
41
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
42
  ) {
43
    return target;
1,766✔
44
  } else {
45
    return findParentCell(target.parentElement);
602✔
46
  }
47
};
48

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

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

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

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

74
  const onTableBlur = (e) => {
28,919✔
75
    if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
1,804✔
76
      currentlyFocusedCell.current = null;
132✔
77
    }
78
  };
79

80
  useEffect(() => {
28,919✔
81
    if (
6,305!
82
      !showOverlay &&
26,086!
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(
28,919✔
96
    (e) => {
97
      if (e.target.dataset?.emptyRowCell === 'true' || e.target.dataset.subcomponentActiveElement) {
1,775✔
98
        return;
9✔
99
      }
100
      if (e.target.dataset.subcomponent) {
1,766!
101
        e.target.tabIndex = 0;
×
102
        e.target.focus();
×
103
        currentlyFocusedCell.current = e.target;
×
104
        return;
×
105
      }
106
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
1,766✔
107
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
1,766!
108
        e.target.tabIndex = -1;
×
109
        if (currentlyFocusedCell.current) {
×
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) {
×
123
          const firstCell = e.target.querySelector('div[data-column-index="0"][data-row-index="0"]');
×
124
          firstCell.tabIndex = 0;
×
125
          firstCell.focus({ preventScroll: true });
×
126
          currentlyFocusedCell.current = firstCell;
×
127
        } else {
128
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
129
        }
130
      } else {
131
        const tableCell = findParentCell(e.target);
1,766✔
132
        if (tableCell) {
1,766!
133
          currentlyFocusedCell.current = tableCell;
1,766✔
134
        } else {
135
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
136
        }
137
      }
138
    },
139
    [currentlyFocusedCell.current, tableRef.current, noData]
140
  );
141

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

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

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

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

325
export const useKeyboardNavigation = (hooks) => {
378✔
326
  hooks.getTableProps.push(useGetTableProps);
28,919✔
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