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

SAP / ui5-webcomponents-react / 3713573366

pending completion
3713573366

Pull #3886

github

GitHub
Merge 6b4a91112 into 658607c34
Pull Request #3886: feat: update `@ui5/webcomponents` to 1.9.3

3074 of 6054 branches covered (50.78%)

3 of 3 new or added lines in 1 file covered. (100.0%)

4249 of 5265 relevant lines covered (80.7%)

4079.92 hits per line

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

19.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'];
21✔
4

5
const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => {
21✔
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) => {
21✔
28
  if (target === undefined || target === null) return;
181!
29
  if (
181!
30
    (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined) ||
362!
31
    (target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined)
32
  ) {
33
    return target;
137✔
34
  } else {
35
    return findParentCell(target.parentElement);
44✔
36
  }
37
};
38

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

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

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

60
  const onTableBlur = (e) => {
2,232✔
61
    if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') {
187!
62
      currentlyFocusedCell.current = null;
41✔
63
    }
64
  };
65

66
  useEffect(() => {
2,232✔
67
    if (
376!
68
      !showOverlay &&
1,820!
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(
2,232✔
82
    (e) => {
83
      const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]');
137✔
84
      if (e.target.dataset.componentName === 'AnalyticalTableContainer') {
137!
85
        e.target.tabIndex = -1;
×
86
        if (currentlyFocusedCell.current) {
×
87
          const { dataset } = currentlyFocusedCell.current;
×
88
          const rowIndex = parseInt(dataset.rowIndex ?? dataset.rowIndexSub, 10);
×
89
          const columnIndex = parseInt(dataset.columnIndex ?? dataset.columnIndexSub, 10);
×
90
          if (
×
91
            e.target.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex}"]`) ||
×
92
            e.target.querySelector(`div[data-column-index-sub="${columnIndex}"][data-row-index-sub="${rowIndex}"]`)
93
          ) {
94
            currentlyFocusedCell.current.tabIndex = 0;
×
95
            currentlyFocusedCell.current.focus({ preventScroll: true });
×
96
          } else {
97
            getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
98
          }
99
        } else if (isFirstCellAvailable) {
×
100
          const firstCell = e.target.querySelector('div[data-column-index="0"][data-row-index="0"]');
×
101
          firstCell.tabIndex = 0;
×
102
          firstCell.focus({ preventScroll: true });
×
103
          currentlyFocusedCell.current = firstCell;
×
104
        } else {
105
          getFirstVisibleCell(e.target, currentlyFocusedCell, noData);
×
106
        }
107
      } else {
108
        const tableCell = findParentCell(e.target);
137✔
109
        if (tableCell) {
137!
110
          currentlyFocusedCell.current = tableCell;
137✔
111
        } else {
112
          getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData);
×
113
        }
114
      }
115
    },
116
    [currentlyFocusedCell.current, tableRef.current, noData]
117
  );
118

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

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

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

293
export const useKeyboardNavigation = (hooks) => {
21✔
294
  hooks.getTableProps.push(getTableProps);
2,232✔
295
};
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