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

alma-oss / spirit-design-system / 29920932531

22 Jul 2026 12:38PM UTC coverage: 85.616% (-0.8%) from 86.381%
29920932531

Pull #2814

github

pavelklibani
fixup! feat(web-react): introduce `UNSTABLE_Combobox` component #DS-2304
Pull Request #2814: 🚧 [WIP] feat(web-react): introduce `UNSTABLE_Combobox` component #DS-2304

2869 of 3878 branches covered (73.98%)

Branch coverage included in aggregate %.

401 of 492 new or added lines in 16 files covered. (81.5%)

7738 of 8511 relevant lines covered (90.92%)

226.44 hits per line

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

71.11
/packages/web-react/src/components/UNSTABLE_Combobox/utils.ts
1
import { Children, type ReactNode, isValidElement } from 'react';
175✔
2

3
/**
4
 * Flattens a ReactNode to plain text (for aria-labels).
5
 *
6
 * @param node React node
7
 */
8
export const getNodeText = (node: ReactNode): string => {
175✔
9
  if (node == null || typeof node === 'boolean') {
164!
NEW
10
    return '';
×
11
  }
12

13
  if (typeof node === 'string' || typeof node === 'number') {
164✔
14
    return String(node);
87✔
15
  }
16

17
  if (Array.isArray(node)) {
77!
NEW
18
    return node.map(getNodeText).join('');
×
19
  }
20

21
  if (isValidElement<{ children?: ReactNode }>(node)) {
77!
22
    return getNodeText(node.props.children);
77✔
23
  }
24

NEW
25
  return '';
×
26
};
27

28
const isComboboxOption = (node: ReactNode) =>
175✔
29
  isValidElement(node) && (node.type as { spiritComponent?: string })?.spiritComponent === 'UNSTABLE_ComboboxOption';
77✔
30

31
/**
32
 * Collects option id → label from Combobox children (`UNSTABLE_ComboboxOption` or `role="row"` items).
33
 * Used so selection tags show readable labels before/without relying on DOM.
34
 *
35
 * @param children Combobox option children
36
 */
37
export const collectComboboxOptionLabels = (children: ReactNode): Record<string, string> => {
175✔
38
  const labels: Record<string, string> = {};
40✔
39

40
  const traverse = (node: ReactNode) => {
40✔
41
    Children.forEach(node, (child) => {
40✔
42
      if (!isValidElement<{ id?: string; role?: string; value?: string; children?: ReactNode }>(child)) {
77!
NEW
43
        return;
×
44
      }
45

46
      const { id, role, value, children: childChildren } = child.props;
77✔
47

48
      if (isComboboxOption(child) && value) {
77!
49
        labels[value] = getNodeText(childChildren);
77✔
50

51
        return;
77✔
52
      }
53

NEW
54
      if (role === 'row' && id) {
×
NEW
55
        labels[id] = getNodeText(childChildren);
×
56

NEW
57
        return;
×
58
      }
59

NEW
60
      if (childChildren != null) {
×
NEW
61
        traverse(childChildren);
×
62
      }
63
    });
64
  };
65

66
  traverse(children);
40✔
67

68
  return labels;
40✔
69
};
70

71
/**
72
 * Readable label for an option row element.
73
 *
74
 * @param rowEl Option row
75
 */
76
export const getRowLabel = (rowEl: HTMLElement): string => {
175✔
77
  const firstCell = rowEl.querySelector('[role="gridcell"]');
79✔
78

79
  return (firstCell?.textContent ?? rowEl.textContent ?? '').trim();
79!
80
};
81

82
/**
83
 * Visible option rows inside the listbox grid (not hidden via `display: none` / `hidden`).
84
 *
85
 * @param listboxEl Listbox grid element
86
 */
87
export const getVisibleOptionRows = (listboxEl: HTMLElement | null): HTMLElement[] => {
175✔
88
  if (!listboxEl) {
4!
NEW
89
    return [];
×
90
  }
91

92
  return Array.from(listboxEl.querySelectorAll<HTMLElement>('[role="row"]')).filter((row) => {
4✔
93
    if (row.hasAttribute('hidden') || row.getAttribute('aria-hidden') === 'true') {
8!
NEW
94
      return false;
×
95
    }
96

97
    return row.style.display !== 'none';
8✔
98
  });
99
};
100

101
/**
102
 * Whether every option id in `optionKeys` is selected.
103
 *
104
 * @param selectedKeys Selected keys
105
 * @param optionKeys Full option key set
106
 */
107
export const areAllOptionsSelected = (selectedKeys: string[], optionKeys: string[]): boolean =>
175✔
108
  optionKeys.length > 0 && optionKeys.every((key) => selectedKeys.includes(key));
57✔
109

110
/**
111
 * Find an option row by id within a listbox (scoped lookup).
112
 *
113
 * @param listboxEl Listbox grid element
114
 * @param optionId Option row id
115
 */
116
export const getOptionRowEl = (listboxEl: HTMLElement | null, optionId: string): HTMLElement | null => {
175✔
117
  if (!listboxEl || !optionId) {
7✔
118
    return null;
2✔
119
  }
120

121
  return (
5✔
122
    Array.from(listboxEl.querySelectorAll<HTMLElement>('[role="row"][id]')).find((row) => row.id === optionId) ?? null
7✔
123
  );
124
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc