• 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

47.97
/packages/web-react/src/components/UNSTABLE_Combobox/useComboboxOptionGridKeyboard.ts
1
'use client';
2

3
import { type KeyboardEvent, type RefObject, useCallback } from 'react';
174✔
4
import { getOptionValueFromRow, getVisibleOptionRows } from './utils';
174✔
5

6
export interface UseComboboxOptionGridKeyboardProps {
7
  listboxRef: RefObject<HTMLElement | null>;
8
  inputRef: RefObject<HTMLInputElement | null>;
9
  isOpen: boolean;
10
  isDisabled?: boolean;
11
  inputValue: string;
12
  onInputChange: (value: string) => void;
13
  onOpen: () => void;
14
  onClose: () => void;
15
  onToggleOption: (optionId: string) => void;
16
  setActiveDescendantId: (id: string | undefined) => void;
17
}
18

19
/**
20
 * Focus an option row and sync `aria-activedescendant` on the combobox input.
21
 *
22
 * @param rowEl Option row
23
 * @param inputRef Combobox input
24
 * @param setActiveDescendantId State setter for activedescendant
25
 */
26
const focusOptionRow = (
174✔
27
  rowEl: HTMLElement | null | undefined,
28
  setActiveDescendantId: (id: string | undefined) => void,
29
) => {
30
  if (!rowEl) {
3!
NEW
31
    return;
×
32
  }
33

34
  rowEl.focus();
3✔
35
  rowEl.scrollIntoView({ block: 'nearest' });
3✔
36

37
  if (rowEl.id) {
3!
38
    setActiveDescendantId(rowEl.id);
3✔
39
  }
40
};
41

42
/**
43
 * Keyboard navigation for the combobox option grid and the filter input.
44
 *
45
 * @param props Hook configuration
46
 * @param props.listboxRef
47
 * @param props.inputRef
48
 * @param props.isOpen
49
 * @param props.isDisabled
50
 * @param props.inputValue
51
 * @param props.onInputChange
52
 * @param props.onOpen
53
 * @param props.onClose
54
 * @param props.onToggleOption
55
 * @param props.setActiveDescendantId
56
 * @returns {{ onInputKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void, onListboxKeyDown: (event: KeyboardEvent<HTMLElement>) => void }}
57
 */
58
export const useComboboxOptionGridKeyboard = ({
174✔
59
  listboxRef,
60
  inputRef,
61
  isOpen,
62
  isDisabled = false,
×
63
  inputValue,
64
  onInputChange,
65
  onOpen,
66
  onClose,
67
  onToggleOption,
68
  setActiveDescendantId,
69
}: UseComboboxOptionGridKeyboardProps): {
70
  onInputKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;
71
  onListboxKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
72
} => {
73
  const clearActiveDescendant = useCallback(() => {
51✔
74
    setActiveDescendantId(undefined);
1✔
75
  }, [setActiveDescendantId]);
76

77
  const onInputKeyDown = useCallback(
51✔
78
    (event: KeyboardEvent<HTMLInputElement>) => {
79
      if (isDisabled) {
4!
NEW
80
        return;
×
81
      }
82

83
      if (event.key === 'Escape') {
4✔
84
        event.preventDefault();
1✔
85
        clearActiveDescendant();
1✔
86
        onClose();
1✔
87

88
        return;
1✔
89
      }
90

91
      if (event.key === 'Tab') {
3!
NEW
92
        clearActiveDescendant();
×
NEW
93
        onClose();
×
94

NEW
95
        return;
×
96
      }
97

98
      if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
3!
99
        event.preventDefault();
3✔
100

101
        if (!isOpen) {
3!
NEW
102
          onOpen();
×
103
        }
104

105
        const visible = getVisibleOptionRows(listboxRef.current);
3✔
106

107
        if (!visible.length) {
3!
NEW
108
          return;
×
109
        }
110

111
        focusOptionRow(
3✔
112
          event.key === 'ArrowDown' ? visible[0] : visible[visible.length - 1],
3!
113
          setActiveDescendantId,
114
        );
115
      }
116
    },
117
    [clearActiveDescendant, isDisabled, isOpen, listboxRef, onClose, onOpen, setActiveDescendantId],
118
  );
119

120
  const onListboxKeyDown = useCallback(
51✔
121
    (event: KeyboardEvent<HTMLElement>) => {
122
      if (isDisabled) {
1!
NEW
123
        return;
×
124
      }
125

126
      const visible = getVisibleOptionRows(listboxRef.current);
1✔
127
      const focused = document.activeElement;
1✔
128
      const currentIndex = focused instanceof HTMLElement ? visible.indexOf(focused) : -1;
1!
129

130
      if (currentIndex === -1) {
1!
NEW
131
        return;
×
132
      }
133

134
      if (event.key === 'Escape') {
1!
NEW
135
        event.preventDefault();
×
NEW
136
        clearActiveDescendant();
×
NEW
137
        onClose();
×
NEW
138
        inputRef.current?.focus();
×
139

NEW
140
        return;
×
141
      }
142

143
      if (event.key === 'Tab') {
1!
NEW
144
        if (event.shiftKey) {
×
NEW
145
          event.preventDefault();
×
NEW
146
          clearActiveDescendant();
×
NEW
147
          onClose();
×
NEW
148
          inputRef.current?.focus();
×
149
        } else {
NEW
150
          clearActiveDescendant();
×
NEW
151
          onClose();
×
152
        }
153

NEW
154
        return;
×
155
      }
156

157
      if (event.key === 'ArrowDown') {
1!
NEW
158
        event.preventDefault();
×
159

NEW
160
        if (currentIndex < visible.length - 1) {
×
NEW
161
          focusOptionRow(visible[currentIndex + 1], setActiveDescendantId);
×
162
        }
163

NEW
164
        return;
×
165
      }
166

167
      if (event.key === 'ArrowUp') {
1!
NEW
168
        event.preventDefault();
×
169

NEW
170
        if (currentIndex > 0) {
×
NEW
171
          focusOptionRow(visible[currentIndex - 1], setActiveDescendantId);
×
172
        } else {
NEW
173
          clearActiveDescendant();
×
NEW
174
          inputRef.current?.focus();
×
175
        }
176

NEW
177
        return;
×
178
      }
179

180
      if (event.key === 'Home') {
1!
NEW
181
        event.preventDefault();
×
NEW
182
        focusOptionRow(visible[0], setActiveDescendantId);
×
183

NEW
184
        return;
×
185
      }
186

187
      if (event.key === 'End') {
1!
NEW
188
        event.preventDefault();
×
NEW
189
        focusOptionRow(visible[visible.length - 1], setActiveDescendantId);
×
190

NEW
191
        return;
×
192
      }
193

194
      if (event.key === 'Enter' || event.key === ' ') {
1!
195
        event.preventDefault();
1✔
196

197
        const row = visible[currentIndex];
1✔
198
        const optionValue = row ? getOptionValueFromRow(row) : '';
1!
199

200
        if (optionValue && row?.getAttribute('aria-disabled') !== 'true') {
1!
201
          onToggleOption(optionValue);
1✔
202
        }
203

204
        return;
1✔
205
      }
206

NEW
207
      if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
×
NEW
208
        onInputChange(inputValue + event.key);
×
NEW
209
        clearActiveDescendant();
×
NEW
210
        inputRef.current?.focus();
×
211
      }
212
    },
213
    [
214
      clearActiveDescendant,
215
      inputRef,
216
      inputValue,
217
      isDisabled,
218
      listboxRef,
219
      onClose,
220
      onInputChange,
221
      onToggleOption,
222
      setActiveDescendantId,
223
    ],
224
  );
225

226
  return { onInputKeyDown, onListboxKeyDown };
51✔
227
};
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