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

alma-oss / spirit-design-system / 29926312974

22 Jul 2026 01:57PM UTC coverage: 85.422% (-1.0%) from 86.381%
29926312974

Pull #2814

github

pavelklibani
test(e2e): update web-react demo homepage snapshot #DS-2304
Pull Request #2814: 🚧 [WIP] feat(web-react): introduce `UNSTABLE_Combobox` component #DS-2304

2868 of 3885 branches covered (73.82%)

Branch coverage included in aggregate %.

401 of 512 new or added lines in 16 files covered. (78.32%)

7738 of 8531 relevant lines covered (90.7%)

226.17 hits per line

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

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

3
/** Attribute holding the selection key on an option row (DOM `id` is namespaced). */
4
export const COMBOBOX_OPTION_VALUE_ATTR = 'data-spirit-value';
175✔
5

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

16
  if (typeof node === 'string' || typeof node === 'number') {
164✔
17
    return String(node);
87✔
18
  }
19

20
  if (Array.isArray(node)) {
77!
NEW
21
    return node.map(getNodeText).join('');
×
22
  }
23

24
  if (isValidElement<{ children?: ReactNode }>(node)) {
77!
25
    return getNodeText(node.props.children);
77✔
26
  }
27

NEW
28
  return '';
×
29
};
30

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

34
/**
35
 * Namespaced DOM id for an option row (unique per Combobox instance).
36
 *
37
 * @param comboboxId Combobox instance id prefix from `useComboboxId`
38
 * @param value Option selection key
39
 */
40
export const getComboboxOptionDomId = (comboboxId: string, value: string): string => `${comboboxId}-${value}`;
175✔
41

42
/**
43
 * Selection key from an option row (`data-spirit-value`, else raw `id` for custom rows).
44
 *
45
 * @param rowEl Option row
46
 */
47
export const getOptionValueFromRow = (rowEl: HTMLElement): string =>
175✔
48
  rowEl.getAttribute(COMBOBOX_OPTION_VALUE_ATTR) || rowEl.id;
91✔
49

50
/**
51
 * Collects option id → label from Combobox children (`UNSTABLE_ComboboxOption` or `role="row"` items).
52
 * Used so selection tags show readable labels before/without relying on DOM.
53
 *
54
 * @param children Combobox option children
55
 */
56
export const collectComboboxOptionLabels = (children: ReactNode): Record<string, string> => {
175✔
57
  const labels: Record<string, string> = {};
40✔
58

59
  const traverse = (node: ReactNode) => {
40✔
60
    Children.forEach(node, (child) => {
40✔
61
      if (!isValidElement<{ id?: string; role?: string; value?: string; children?: ReactNode }>(child)) {
77!
NEW
62
        return;
×
63
      }
64

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

67
      if (isComboboxOption(child) && value) {
77!
68
        labels[value] = getNodeText(childChildren);
77✔
69

70
        return;
77✔
71
      }
72

NEW
73
      if (role === 'row' && id) {
×
NEW
74
        labels[id] = getNodeText(childChildren);
×
75

NEW
76
        return;
×
77
      }
78

NEW
79
      if (childChildren != null) {
×
NEW
80
        traverse(childChildren);
×
81
      }
82
    });
83
  };
84

85
  traverse(children);
40✔
86

87
  return labels;
40✔
88
};
89

90
/**
91
 * Collects option selection keys from Combobox children (insertion order, unique).
92
 *
93
 * @param children Combobox option children
94
 */
95
export const collectComboboxOptionKeys = (children: ReactNode): string[] => {
175✔
NEW
96
  const keys: string[] = [];
×
NEW
97
  const seen = new Set<string>();
×
98

NEW
99
  const push = (key: string) => {
×
NEW
100
    if (!key || seen.has(key)) {
×
NEW
101
      return;
×
102
    }
103

NEW
104
    seen.add(key);
×
NEW
105
    keys.push(key);
×
106
  };
107

NEW
108
  const traverse = (node: ReactNode) => {
×
NEW
109
    Children.forEach(node, (child) => {
×
NEW
110
      if (!isValidElement<{ id?: string; role?: string; value?: string; children?: ReactNode }>(child)) {
×
NEW
111
        return;
×
112
      }
113

NEW
114
      const { id, role, value, children: childChildren } = child.props;
×
115

NEW
116
      if (isComboboxOption(child) && value) {
×
NEW
117
        push(value);
×
118

NEW
119
        return;
×
120
      }
121

NEW
122
      if (role === 'row' && id) {
×
NEW
123
        push(id);
×
124

NEW
125
        return;
×
126
      }
127

NEW
128
      if (childChildren != null) {
×
NEW
129
        traverse(childChildren);
×
130
      }
131
    });
132
  };
133

NEW
134
  traverse(children);
×
135

NEW
136
  return keys;
×
137
};
138

139
/**
140
 * Readable label for an option row element.
141
 *
142
 * @param rowEl Option row
143
 */
144
export const getRowLabel = (rowEl: HTMLElement): string => {
175✔
145
  const firstCell = rowEl.querySelector('[role="gridcell"]');
79✔
146

147
  return (firstCell?.textContent ?? rowEl.textContent ?? '').trim();
79!
148
};
149

150
/**
151
 * Visible option rows inside the listbox grid (not hidden via `display: none` / `hidden`).
152
 *
153
 * @param listboxEl Listbox grid element
154
 */
155
export const getVisibleOptionRows = (listboxEl: HTMLElement | null): HTMLElement[] => {
175✔
156
  if (!listboxEl) {
4!
NEW
157
    return [];
×
158
  }
159

160
  return Array.from(listboxEl.querySelectorAll<HTMLElement>('[role="row"]')).filter((row) => {
4✔
161
    if (row.hasAttribute('hidden') || row.getAttribute('aria-hidden') === 'true') {
8!
NEW
162
      return false;
×
163
    }
164

165
    return row.style.display !== 'none';
8✔
166
  });
167
};
168

169
/**
170
 * Whether every option id in `optionKeys` is selected.
171
 *
172
 * @param selectedKeys Selected keys
173
 * @param optionKeys Full option key set
174
 */
175
export const areAllOptionsSelected = (selectedKeys: string[], optionKeys: string[]): boolean =>
175✔
176
  optionKeys.length > 0 && optionKeys.every((key) => selectedKeys.includes(key));
57✔
177

178
/**
179
 * Find an option row by selection key within a listbox (scoped lookup).
180
 *
181
 * @param listboxEl Listbox grid element
182
 * @param optionValue Option selection key
183
 */
184
export const getOptionRowEl = (listboxEl: HTMLElement | null, optionValue: string): HTMLElement | null => {
175✔
185
  if (!listboxEl || !optionValue) {
8✔
186
    return null;
2✔
187
  }
188

189
  return (
6✔
190
    Array.from(listboxEl.querySelectorAll<HTMLElement>('[role="row"]')).find(
7✔
191
      (row) => getOptionValueFromRow(row) === optionValue,
8✔
192
    ) ?? null
193
  );
194
};
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