@compa11y/react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/chunk-2S4C6FGA.js +380 -0
- package/dist/chunk-2S4C6FGA.js.map +1 -0
- package/dist/chunk-52J4Z3QD.cjs +45 -0
- package/dist/chunk-52J4Z3QD.cjs.map +1 -0
- package/dist/chunk-C7QK2I7H.js +373 -0
- package/dist/chunk-C7QK2I7H.js.map +1 -0
- package/dist/chunk-D2UMS62N.cjs +245 -0
- package/dist/chunk-D2UMS62N.cjs.map +1 -0
- package/dist/chunk-E265U2RK.js +234 -0
- package/dist/chunk-E265U2RK.js.map +1 -0
- package/dist/chunk-E4XJRXWM.js +215 -0
- package/dist/chunk-E4XJRXWM.js.map +1 -0
- package/dist/chunk-GDLOJH6K.cjs +110 -0
- package/dist/chunk-GDLOJH6K.cjs.map +1 -0
- package/dist/chunk-IR46CNNY.cjs +329 -0
- package/dist/chunk-IR46CNNY.cjs.map +1 -0
- package/dist/chunk-JXYOE7SH.js +103 -0
- package/dist/chunk-JXYOE7SH.js.map +1 -0
- package/dist/chunk-O3YYQZ5O.js +317 -0
- package/dist/chunk-O3YYQZ5O.js.map +1 -0
- package/dist/chunk-OIVTOU4Z.cjs +386 -0
- package/dist/chunk-OIVTOU4Z.cjs.map +1 -0
- package/dist/chunk-OND5B7UG.js +85 -0
- package/dist/chunk-OND5B7UG.js.map +1 -0
- package/dist/chunk-R4FR6M6I.cjs +383 -0
- package/dist/chunk-R4FR6M6I.cjs.map +1 -0
- package/dist/chunk-RBDQCIS7.cjs +89 -0
- package/dist/chunk-RBDQCIS7.cjs.map +1 -0
- package/dist/chunk-SOBS7MIH.cjs +220 -0
- package/dist/chunk-SOBS7MIH.cjs.map +1 -0
- package/dist/chunk-WURPAE3R.js +41 -0
- package/dist/chunk-WURPAE3R.js.map +1 -0
- package/dist/components/combobox/index.cjs +31 -0
- package/dist/components/combobox/index.cjs.map +1 -0
- package/dist/components/combobox/index.d.cts +55 -0
- package/dist/components/combobox/index.d.ts +55 -0
- package/dist/components/combobox/index.js +6 -0
- package/dist/components/combobox/index.js.map +1 -0
- package/dist/components/dialog/index.cjs +46 -0
- package/dist/components/dialog/index.cjs.map +1 -0
- package/dist/components/dialog/index.d.cts +84 -0
- package/dist/components/dialog/index.d.ts +84 -0
- package/dist/components/dialog/index.js +5 -0
- package/dist/components/dialog/index.js.map +1 -0
- package/dist/components/menu/index.cjs +46 -0
- package/dist/components/menu/index.cjs.map +1 -0
- package/dist/components/menu/index.d.cts +80 -0
- package/dist/components/menu/index.d.ts +80 -0
- package/dist/components/menu/index.js +5 -0
- package/dist/components/menu/index.js.map +1 -0
- package/dist/components/tabs/index.cjs +35 -0
- package/dist/components/tabs/index.cjs.map +1 -0
- package/dist/components/tabs/index.d.cts +65 -0
- package/dist/components/tabs/index.d.ts +65 -0
- package/dist/components/tabs/index.js +6 -0
- package/dist/components/tabs/index.js.map +1 -0
- package/dist/components/toast/index.cjs +24 -0
- package/dist/components/toast/index.cjs.map +1 -0
- package/dist/components/toast/index.d.cts +49 -0
- package/dist/components/toast/index.d.ts +49 -0
- package/dist/components/toast/index.js +3 -0
- package/dist/components/toast/index.js.map +1 -0
- package/dist/index.cjs +702 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +402 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.js +430 -0
- package/dist/index.js.map +1 -0
- package/package.json +99 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Trajkovski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# @compa11y/react
|
|
2
|
+
|
|
3
|
+
Accessible React components that just work.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @compa11y/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Components
|
|
12
|
+
|
|
13
|
+
### Dialog
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Dialog } from '@compa11y/react';
|
|
17
|
+
|
|
18
|
+
function ConfirmDialog({ open, onClose, onConfirm }) {
|
|
19
|
+
return (
|
|
20
|
+
<Dialog open={open} onOpenChange={onClose}>
|
|
21
|
+
<Dialog.Title>Confirm Delete</Dialog.Title>
|
|
22
|
+
<Dialog.Description>
|
|
23
|
+
Are you sure you want to delete this item?
|
|
24
|
+
</Dialog.Description>
|
|
25
|
+
<Dialog.Actions>
|
|
26
|
+
<button onClick={onClose}>Cancel</button>
|
|
27
|
+
<button onClick={onConfirm}>Delete</button>
|
|
28
|
+
</Dialog.Actions>
|
|
29
|
+
</Dialog>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Menu
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { Menu } from '@compa11y/react';
|
|
38
|
+
|
|
39
|
+
function ActionMenu() {
|
|
40
|
+
return (
|
|
41
|
+
<Menu>
|
|
42
|
+
<Menu.Trigger>Actions</Menu.Trigger>
|
|
43
|
+
<Menu.Content>
|
|
44
|
+
<Menu.Item onSelect={() => console.log('Edit')}>Edit</Menu.Item>
|
|
45
|
+
<Menu.Item onSelect={() => console.log('Copy')}>Copy</Menu.Item>
|
|
46
|
+
<Menu.Separator />
|
|
47
|
+
<Menu.Item onSelect={() => console.log('Delete')}>Delete</Menu.Item>
|
|
48
|
+
</Menu.Content>
|
|
49
|
+
</Menu>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Tabs
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { Tabs } from '@compa11y/react';
|
|
58
|
+
|
|
59
|
+
function SettingsTabs() {
|
|
60
|
+
return (
|
|
61
|
+
<Tabs defaultValue="general">
|
|
62
|
+
<Tabs.List>
|
|
63
|
+
<Tabs.Tab value="general">General</Tabs.Tab>
|
|
64
|
+
<Tabs.Tab value="security">Security</Tabs.Tab>
|
|
65
|
+
<Tabs.Tab value="notifications">Notifications</Tabs.Tab>
|
|
66
|
+
</Tabs.List>
|
|
67
|
+
<Tabs.Panel value="general">General settings...</Tabs.Panel>
|
|
68
|
+
<Tabs.Panel value="security">Security settings...</Tabs.Panel>
|
|
69
|
+
<Tabs.Panel value="notifications">Notification settings...</Tabs.Panel>
|
|
70
|
+
</Tabs>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Toast
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import {
|
|
79
|
+
ToastProvider,
|
|
80
|
+
ToastViewport,
|
|
81
|
+
useToastHelpers,
|
|
82
|
+
} from '@compa11y/react';
|
|
83
|
+
|
|
84
|
+
function App() {
|
|
85
|
+
return (
|
|
86
|
+
<ToastProvider>
|
|
87
|
+
<Content />
|
|
88
|
+
<ToastViewport position="bottom-right" />
|
|
89
|
+
</ToastProvider>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function Content() {
|
|
94
|
+
const { success, error } = useToastHelpers();
|
|
95
|
+
|
|
96
|
+
return <button onClick={() => success('Settings saved!')}>Save</button>;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Combobox
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { Combobox } from '@compa11y/react';
|
|
104
|
+
|
|
105
|
+
const countries = [
|
|
106
|
+
{ value: 'us', label: 'United States' },
|
|
107
|
+
{ value: 'uk', label: 'United Kingdom' },
|
|
108
|
+
{ value: 'ca', label: 'Canada' },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
function CountrySelect() {
|
|
112
|
+
const [value, setValue] = useState(null);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Combobox
|
|
116
|
+
options={countries}
|
|
117
|
+
value={value}
|
|
118
|
+
onValueChange={setValue}
|
|
119
|
+
aria-label="Select country"
|
|
120
|
+
>
|
|
121
|
+
<Combobox.Input placeholder="Choose a country..." clearable />
|
|
122
|
+
<Combobox.Listbox emptyMessage="No countries found" />
|
|
123
|
+
</Combobox>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Hooks
|
|
129
|
+
|
|
130
|
+
### useFocusTrap
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { useFocusTrap } from '@compa11y/react';
|
|
134
|
+
|
|
135
|
+
function Modal({ isOpen }) {
|
|
136
|
+
const trapRef = useFocusTrap({ active: isOpen });
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div ref={trapRef} role="dialog">
|
|
140
|
+
<button>Close</button>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### useAnnouncer
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useAnnouncer } from '@compa11y/react';
|
|
150
|
+
|
|
151
|
+
function SearchResults({ count }) {
|
|
152
|
+
const { announce } = useAnnouncer();
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
announce(`Found ${count} results`);
|
|
156
|
+
}, [count, announce]);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### useKeyboard
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { useKeyboard } from '@compa11y/react';
|
|
164
|
+
|
|
165
|
+
function CustomList() {
|
|
166
|
+
const keyboardProps = useKeyboard({
|
|
167
|
+
ArrowDown: () => focusNext(),
|
|
168
|
+
ArrowUp: () => focusPrevious(),
|
|
169
|
+
Enter: () => selectItem(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return <ul {...keyboardProps}>...</ul>;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### useFocusVisible
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { useFocusVisible } from '@compa11y/react';
|
|
180
|
+
|
|
181
|
+
function Button({ children }) {
|
|
182
|
+
const { isFocusVisible, focusProps } = useFocusVisible();
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<button {...focusProps} className={isFocusVisible ? 'focus-ring' : ''}>
|
|
186
|
+
{children}
|
|
187
|
+
</button>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### useRovingTabindex
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { useRovingTabindex } from '@compa11y/react';
|
|
196
|
+
|
|
197
|
+
function Toolbar() {
|
|
198
|
+
const { getItemProps } = useRovingTabindex({
|
|
199
|
+
itemCount: 3,
|
|
200
|
+
orientation: 'horizontal',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div role="toolbar">
|
|
205
|
+
<button {...getItemProps(0)}>Cut</button>
|
|
206
|
+
<button {...getItemProps(1)}>Copy</button>
|
|
207
|
+
<button {...getItemProps(2)}>Paste</button>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Styling
|
|
214
|
+
|
|
215
|
+
All components are unstyled. Use `data-*` attributes for state-based styling:
|
|
216
|
+
|
|
217
|
+
```css
|
|
218
|
+
/* Dialog */
|
|
219
|
+
[data-compa11y-dialog-overlay] {
|
|
220
|
+
background: rgba(0, 0, 0, 0.5);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
[data-compa11y-dialog] {
|
|
224
|
+
background: white;
|
|
225
|
+
padding: 1.5rem;
|
|
226
|
+
border-radius: 8px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Menu */
|
|
230
|
+
[data-compa11y-menu-content] {
|
|
231
|
+
background: white;
|
|
232
|
+
border: 1px solid #e0e0e0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
[data-compa11y-menu-item][data-highlighted='true'] {
|
|
236
|
+
background: #f0f0f0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Tabs */
|
|
240
|
+
[data-compa11y-tab][data-selected='true'] {
|
|
241
|
+
border-bottom: 2px solid blue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Combobox */
|
|
245
|
+
[data-compa11y-combobox-option][data-highlighted='true'] {
|
|
246
|
+
background: #f0f0f0;
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { useAnnouncer } from './chunk-OND5B7UG.js';
|
|
2
|
+
import { useKeyboard } from './chunk-JXYOE7SH.js';
|
|
3
|
+
import { useId } from './chunk-WURPAE3R.js';
|
|
4
|
+
import { createContext, forwardRef, useRef, useState, useEffect, useLayoutEffect, useCallback, useContext, useMemo } from 'react';
|
|
5
|
+
import { createComponentWarnings } from '@compa11y/core';
|
|
6
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var warnings = createComponentWarnings("Combobox");
|
|
9
|
+
var ComboboxContext = createContext(null);
|
|
10
|
+
function useComboboxContext() {
|
|
11
|
+
const context = useContext(ComboboxContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Combobox compound components must be used within a Combobox component"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
19
|
+
function Combobox({
|
|
20
|
+
options,
|
|
21
|
+
value: controlledValue,
|
|
22
|
+
onValueChange,
|
|
23
|
+
onInputChange,
|
|
24
|
+
defaultInputValue = "",
|
|
25
|
+
disabled = false,
|
|
26
|
+
filterFn,
|
|
27
|
+
"aria-label": ariaLabel,
|
|
28
|
+
"aria-labelledby": ariaLabelledBy,
|
|
29
|
+
children
|
|
30
|
+
}) {
|
|
31
|
+
const [inputValue, setInputValueState] = useState(defaultInputValue);
|
|
32
|
+
const [selectedValue, setSelectedValueState] = useState(
|
|
33
|
+
controlledValue ?? null
|
|
34
|
+
);
|
|
35
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
36
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
37
|
+
const inputId = useId("combobox-input");
|
|
38
|
+
const listboxId = useId("combobox-listbox");
|
|
39
|
+
const baseOptionId = useId("combobox-option");
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (controlledValue !== void 0) {
|
|
42
|
+
setSelectedValueState(controlledValue);
|
|
43
|
+
const option = options.find((o) => o.value === controlledValue);
|
|
44
|
+
if (option) {
|
|
45
|
+
setInputValueState(option.label);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}, [controlledValue, options]);
|
|
49
|
+
const setInputValue = useCallback(
|
|
50
|
+
(value) => {
|
|
51
|
+
setInputValueState(value);
|
|
52
|
+
onInputChange?.(value);
|
|
53
|
+
},
|
|
54
|
+
[onInputChange]
|
|
55
|
+
);
|
|
56
|
+
const setSelectedValue = useCallback(
|
|
57
|
+
(value) => {
|
|
58
|
+
if (controlledValue === void 0) {
|
|
59
|
+
setSelectedValueState(value);
|
|
60
|
+
}
|
|
61
|
+
onValueChange?.(value);
|
|
62
|
+
},
|
|
63
|
+
[controlledValue, onValueChange]
|
|
64
|
+
);
|
|
65
|
+
const filteredOptions = useMemo(() => {
|
|
66
|
+
if (!inputValue) return options;
|
|
67
|
+
const defaultFilter = (opt, input) => opt.label.toLowerCase().includes(input.toLowerCase());
|
|
68
|
+
const filter = filterFn ?? defaultFilter;
|
|
69
|
+
return options.filter((opt) => filter(opt, inputValue));
|
|
70
|
+
}, [options, inputValue, filterFn]);
|
|
71
|
+
const getOptionId = useCallback(
|
|
72
|
+
(index) => `${baseOptionId}-${index}`,
|
|
73
|
+
[baseOptionId]
|
|
74
|
+
);
|
|
75
|
+
const onSelect = useCallback(
|
|
76
|
+
(option) => {
|
|
77
|
+
setSelectedValue(option.value);
|
|
78
|
+
setInputValue(option.label);
|
|
79
|
+
setIsOpen(false);
|
|
80
|
+
setHighlightedIndex(-1);
|
|
81
|
+
},
|
|
82
|
+
[setSelectedValue, setInputValue]
|
|
83
|
+
);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!ariaLabel && !ariaLabelledBy) {
|
|
86
|
+
warnings.warning(
|
|
87
|
+
"Combobox has no accessible label.",
|
|
88
|
+
"Add aria-label or aria-labelledby prop."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}, [ariaLabel, ariaLabelledBy]);
|
|
92
|
+
const contextValue = {
|
|
93
|
+
inputValue,
|
|
94
|
+
setInputValue,
|
|
95
|
+
selectedValue,
|
|
96
|
+
setSelectedValue,
|
|
97
|
+
isOpen,
|
|
98
|
+
setIsOpen,
|
|
99
|
+
highlightedIndex,
|
|
100
|
+
setHighlightedIndex,
|
|
101
|
+
options,
|
|
102
|
+
filteredOptions,
|
|
103
|
+
inputId,
|
|
104
|
+
listboxId,
|
|
105
|
+
getOptionId,
|
|
106
|
+
onSelect
|
|
107
|
+
};
|
|
108
|
+
return /* @__PURE__ */ jsx(ComboboxContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { "data-compa11y-combobox": true, "data-disabled": disabled, children }) });
|
|
109
|
+
}
|
|
110
|
+
var ComboboxInput = forwardRef(
|
|
111
|
+
function ComboboxInput2({ clearable = false, onKeyDown, onFocus, onBlur, ...props }, ref) {
|
|
112
|
+
const {
|
|
113
|
+
inputValue,
|
|
114
|
+
setInputValue,
|
|
115
|
+
setSelectedValue,
|
|
116
|
+
isOpen,
|
|
117
|
+
setIsOpen,
|
|
118
|
+
highlightedIndex,
|
|
119
|
+
setHighlightedIndex,
|
|
120
|
+
filteredOptions,
|
|
121
|
+
inputId,
|
|
122
|
+
listboxId,
|
|
123
|
+
getOptionId,
|
|
124
|
+
onSelect
|
|
125
|
+
} = useComboboxContext();
|
|
126
|
+
const { announce } = useAnnouncer();
|
|
127
|
+
const keyboardProps = useKeyboard(
|
|
128
|
+
{
|
|
129
|
+
ArrowDown: () => {
|
|
130
|
+
if (!isOpen) {
|
|
131
|
+
setIsOpen(true);
|
|
132
|
+
setHighlightedIndex(0);
|
|
133
|
+
} else {
|
|
134
|
+
const nextIndex = (highlightedIndex + 1) % filteredOptions.length;
|
|
135
|
+
setHighlightedIndex(nextIndex);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
ArrowUp: () => {
|
|
139
|
+
if (!isOpen) {
|
|
140
|
+
setIsOpen(true);
|
|
141
|
+
setHighlightedIndex(filteredOptions.length - 1);
|
|
142
|
+
} else {
|
|
143
|
+
const prevIndex = (highlightedIndex - 1 + filteredOptions.length) % filteredOptions.length;
|
|
144
|
+
setHighlightedIndex(prevIndex);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
Enter: () => {
|
|
148
|
+
if (isOpen && highlightedIndex >= 0) {
|
|
149
|
+
const option = filteredOptions[highlightedIndex];
|
|
150
|
+
if (option && !option.disabled) {
|
|
151
|
+
onSelect(option);
|
|
152
|
+
announce(`${option.label} selected`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
Escape: () => {
|
|
157
|
+
if (isOpen) {
|
|
158
|
+
setIsOpen(false);
|
|
159
|
+
setHighlightedIndex(-1);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
Tab: () => {
|
|
163
|
+
if (isOpen) {
|
|
164
|
+
setIsOpen(false);
|
|
165
|
+
setHighlightedIndex(-1);
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
},
|
|
169
|
+
Home: () => {
|
|
170
|
+
if (isOpen) {
|
|
171
|
+
setHighlightedIndex(0);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
End: () => {
|
|
175
|
+
if (isOpen) {
|
|
176
|
+
setHighlightedIndex(filteredOptions.length - 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{ preventDefault: true, stopPropagation: false }
|
|
181
|
+
);
|
|
182
|
+
const handleKeyDown = (event) => {
|
|
183
|
+
onKeyDown?.(event);
|
|
184
|
+
if (!event.defaultPrevented) {
|
|
185
|
+
keyboardProps.onKeyDown(event);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const handleChange = (event) => {
|
|
189
|
+
const value = event.target.value;
|
|
190
|
+
setInputValue(value);
|
|
191
|
+
setIsOpen(true);
|
|
192
|
+
setHighlightedIndex(0);
|
|
193
|
+
if (value === "") {
|
|
194
|
+
setSelectedValue(null);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const handleFocus = (event) => {
|
|
198
|
+
onFocus?.(event);
|
|
199
|
+
setIsOpen(true);
|
|
200
|
+
};
|
|
201
|
+
const handleBlur = (event) => {
|
|
202
|
+
onBlur?.(event);
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
setIsOpen(false);
|
|
205
|
+
}, 150);
|
|
206
|
+
};
|
|
207
|
+
const handleClear = () => {
|
|
208
|
+
setInputValue("");
|
|
209
|
+
setSelectedValue(null);
|
|
210
|
+
setIsOpen(false);
|
|
211
|
+
};
|
|
212
|
+
const activeDescendant = isOpen && highlightedIndex >= 0 ? getOptionId(highlightedIndex) : void 0;
|
|
213
|
+
return /* @__PURE__ */ jsxs("div", { "data-compa11y-combobox-input-wrapper": true, children: [
|
|
214
|
+
/* @__PURE__ */ jsx(
|
|
215
|
+
"input",
|
|
216
|
+
{
|
|
217
|
+
ref,
|
|
218
|
+
id: inputId,
|
|
219
|
+
type: "text",
|
|
220
|
+
role: "combobox",
|
|
221
|
+
value: inputValue,
|
|
222
|
+
onChange: handleChange,
|
|
223
|
+
onKeyDown: handleKeyDown,
|
|
224
|
+
onFocus: handleFocus,
|
|
225
|
+
onBlur: handleBlur,
|
|
226
|
+
"aria-expanded": isOpen,
|
|
227
|
+
"aria-controls": listboxId,
|
|
228
|
+
"aria-activedescendant": activeDescendant,
|
|
229
|
+
"aria-autocomplete": "list",
|
|
230
|
+
"aria-haspopup": "listbox",
|
|
231
|
+
autoComplete: "off",
|
|
232
|
+
"data-compa11y-combobox-input": true,
|
|
233
|
+
...props
|
|
234
|
+
}
|
|
235
|
+
),
|
|
236
|
+
clearable && inputValue && /* @__PURE__ */ jsx(
|
|
237
|
+
"button",
|
|
238
|
+
{
|
|
239
|
+
type: "button",
|
|
240
|
+
onClick: handleClear,
|
|
241
|
+
"aria-label": "Clear selection",
|
|
242
|
+
tabIndex: -1,
|
|
243
|
+
"data-compa11y-combobox-clear": true,
|
|
244
|
+
children: "\xD7"
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
] });
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
var ComboboxListbox = forwardRef(function ComboboxListbox2({ emptyMessage = "No results found", children, style, ...props }, forwardedRef) {
|
|
251
|
+
const { isOpen, filteredOptions, listboxId, inputId } = useComboboxContext();
|
|
252
|
+
const { announce } = useAnnouncer();
|
|
253
|
+
const internalRef = useRef(null);
|
|
254
|
+
const [position, setPosition] = useState("bottom");
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (isOpen) {
|
|
257
|
+
const count = filteredOptions.length;
|
|
258
|
+
announce(
|
|
259
|
+
count === 0 ? "No results" : `${count} result${count === 1 ? "" : "s"} available`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}, [isOpen, filteredOptions.length, announce]);
|
|
263
|
+
useLayoutEffect(() => {
|
|
264
|
+
if (isOpen && internalRef.current) {
|
|
265
|
+
const listbox = internalRef.current;
|
|
266
|
+
const rect = listbox.getBoundingClientRect();
|
|
267
|
+
const viewportHeight = window.innerHeight;
|
|
268
|
+
const spaceBelow = viewportHeight - rect.top;
|
|
269
|
+
const spaceAbove = rect.top;
|
|
270
|
+
const listboxHeight = Math.min(rect.height, 200);
|
|
271
|
+
if (spaceBelow < listboxHeight + 50 && spaceAbove > spaceBelow) {
|
|
272
|
+
setPosition("top");
|
|
273
|
+
} else {
|
|
274
|
+
setPosition("bottom");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}, [isOpen]);
|
|
278
|
+
const setRefs = useCallback(
|
|
279
|
+
(node) => {
|
|
280
|
+
internalRef.current = node;
|
|
281
|
+
if (typeof forwardedRef === "function") {
|
|
282
|
+
forwardedRef(node);
|
|
283
|
+
} else if (forwardedRef) {
|
|
284
|
+
forwardedRef.current = node;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
[forwardedRef]
|
|
288
|
+
);
|
|
289
|
+
if (!isOpen) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const positionStyle = position === "top" ? { bottom: "100%", top: "auto", marginBottom: "4px", marginTop: 0 } : {};
|
|
293
|
+
return /* @__PURE__ */ jsx(
|
|
294
|
+
"ul",
|
|
295
|
+
{
|
|
296
|
+
ref: setRefs,
|
|
297
|
+
id: listboxId,
|
|
298
|
+
role: "listbox",
|
|
299
|
+
"aria-labelledby": inputId,
|
|
300
|
+
style: { ...style, ...positionStyle },
|
|
301
|
+
"data-compa11y-combobox-listbox": true,
|
|
302
|
+
"data-position": position,
|
|
303
|
+
...props,
|
|
304
|
+
children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx("li", { role: "presentation", "data-compa11y-combobox-empty": true, children: emptyMessage }) : children ?? filteredOptions.map((option, index) => /* @__PURE__ */ jsx(ComboboxOption, { option, index }, option.value))
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
var ComboboxOption = forwardRef(
|
|
309
|
+
function ComboboxOption2({ option, index, onClick, onMouseEnter, ...props }, forwardedRef) {
|
|
310
|
+
const {
|
|
311
|
+
selectedValue,
|
|
312
|
+
highlightedIndex,
|
|
313
|
+
setHighlightedIndex,
|
|
314
|
+
getOptionId,
|
|
315
|
+
onSelect
|
|
316
|
+
} = useComboboxContext();
|
|
317
|
+
const internalRef = useRef(null);
|
|
318
|
+
const isSelected = selectedValue === option.value;
|
|
319
|
+
const isHighlighted = highlightedIndex === index;
|
|
320
|
+
const optionId = getOptionId(index);
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
if (isHighlighted && internalRef.current) {
|
|
323
|
+
internalRef.current.scrollIntoView({
|
|
324
|
+
block: "nearest",
|
|
325
|
+
behavior: "smooth"
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}, [isHighlighted]);
|
|
329
|
+
const handleClick = (event) => {
|
|
330
|
+
onClick?.(event);
|
|
331
|
+
if (!event.defaultPrevented && !option.disabled) {
|
|
332
|
+
onSelect(option);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const handleMouseEnter = (event) => {
|
|
336
|
+
onMouseEnter?.(event);
|
|
337
|
+
if (!option.disabled) {
|
|
338
|
+
setHighlightedIndex(index);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const setRefs = useCallback(
|
|
342
|
+
(node) => {
|
|
343
|
+
internalRef.current = node;
|
|
344
|
+
if (typeof forwardedRef === "function") {
|
|
345
|
+
forwardedRef(node);
|
|
346
|
+
} else if (forwardedRef) {
|
|
347
|
+
forwardedRef.current = node;
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
[forwardedRef]
|
|
351
|
+
);
|
|
352
|
+
return /* @__PURE__ */ jsx(
|
|
353
|
+
"li",
|
|
354
|
+
{
|
|
355
|
+
ref: setRefs,
|
|
356
|
+
id: optionId,
|
|
357
|
+
role: "option",
|
|
358
|
+
"aria-selected": isSelected,
|
|
359
|
+
"aria-disabled": option.disabled,
|
|
360
|
+
"data-highlighted": isHighlighted,
|
|
361
|
+
"data-selected": isSelected,
|
|
362
|
+
"data-disabled": option.disabled,
|
|
363
|
+
onClick: handleClick,
|
|
364
|
+
onMouseEnter: handleMouseEnter,
|
|
365
|
+
"data-compa11y-combobox-option": true,
|
|
366
|
+
...props,
|
|
367
|
+
children: option.label
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
var ComboboxCompound = Object.assign(Combobox, {
|
|
373
|
+
Input: ComboboxInput,
|
|
374
|
+
Listbox: ComboboxListbox,
|
|
375
|
+
Option: ComboboxOption
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
export { Combobox, ComboboxCompound, ComboboxInput, ComboboxListbox, ComboboxOption };
|
|
379
|
+
//# sourceMappingURL=chunk-2S4C6FGA.js.map
|
|
380
|
+
//# sourceMappingURL=chunk-2S4C6FGA.js.map
|