@ariakit/components 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/CHANGELOG.md +19 -0
- package/dist/checkbox/checkbox-store.d.ts +47 -0
- package/dist/checkbox/checkbox-store.d.ts.map +1 -0
- package/dist/checkbox/checkbox-store.js +16 -0
- package/dist/checkbox/checkbox-store.js.map +1 -0
- package/dist/collection/collection-store.d.ts +2 -0
- package/dist/collection/collection-store.js +132 -0
- package/dist/collection/collection-store.js.map +1 -0
- package/dist/collection-store-yNe83BiS.d.ts +81 -0
- package/dist/collection-store-yNe83BiS.d.ts.map +1 -0
- package/dist/combobox/combobox-store.d.ts +150 -0
- package/dist/combobox/combobox-store.d.ts.map +1 -0
- package/dist/combobox/combobox-store.js +83 -0
- package/dist/combobox/combobox-store.js.map +1 -0
- package/dist/composite/composite-overflow-store.d.ts +16 -0
- package/dist/composite/composite-overflow-store.d.ts.map +1 -0
- package/dist/composite/composite-overflow-store.js +12 -0
- package/dist/composite/composite-overflow-store.js.map +1 -0
- package/dist/composite/composite-store.d.ts +2 -0
- package/dist/composite/composite-store.js +167 -0
- package/dist/composite/composite-store.js.map +1 -0
- package/dist/composite-store-B-iDEtZZ.d.ts +331 -0
- package/dist/composite-store-B-iDEtZZ.d.ts.map +1 -0
- package/dist/dialog/dialog-store.d.ts +2 -0
- package/dist/dialog/dialog-store.js +12 -0
- package/dist/dialog/dialog-store.js.map +1 -0
- package/dist/dialog-store-BOLvw2IX.d.ts +16 -0
- package/dist/dialog-store-BOLvw2IX.d.ts.map +1 -0
- package/dist/disclosure/disclosure-store.d.ts +2 -0
- package/dist/disclosure/disclosure-store.js +47 -0
- package/dist/disclosure/disclosure-store.js.map +1 -0
- package/dist/disclosure-store-xKlQffR0.d.ts +142 -0
- package/dist/disclosure-store-xKlQffR0.d.ts.map +1 -0
- package/dist/form/form-store.d.ts +247 -0
- package/dist/form/form-store.d.ts.map +1 -0
- package/dist/form/form-store.js +211 -0
- package/dist/form/form-store.js.map +1 -0
- package/dist/form/types.d.ts +37 -0
- package/dist/form/types.d.ts.map +1 -0
- package/dist/form/types.js +0 -0
- package/dist/hovercard/hovercard-store.d.ts +65 -0
- package/dist/hovercard/hovercard-store.d.ts.map +1 -0
- package/dist/hovercard/hovercard-store.js +31 -0
- package/dist/hovercard/hovercard-store.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/menu/menu-bar-store.d.ts +16 -0
- package/dist/menu/menu-bar-store.d.ts.map +1 -0
- package/dist/menu/menu-bar-store.js +12 -0
- package/dist/menu/menu-bar-store.js.map +1 -0
- package/dist/menu/menu-store.d.ts +100 -0
- package/dist/menu/menu-store.d.ts.map +1 -0
- package/dist/menu/menu-store.js +74 -0
- package/dist/menu/menu-store.js.map +1 -0
- package/dist/menubar/menubar-store.d.ts +2 -0
- package/dist/menubar/menubar-store.js +24 -0
- package/dist/menubar/menubar-store.js.map +1 -0
- package/dist/menubar-store-CD3YDYfW.d.ts +16 -0
- package/dist/menubar-store-CD3YDYfW.d.ts.map +1 -0
- package/dist/popover/popover-store.d.ts +2 -0
- package/dist/popover/popover-store.js +44 -0
- package/dist/popover/popover-store.js.map +1 -0
- package/dist/popover-store-DoCiTmUQ.d.ts +106 -0
- package/dist/popover-store-DoCiTmUQ.d.ts.map +1 -0
- package/dist/radio/radio-store.d.ts +42 -0
- package/dist/radio/radio-store.d.ts.map +1 -0
- package/dist/radio/radio-store.js +27 -0
- package/dist/radio/radio-store.js.map +1 -0
- package/dist/select/select-store.d.ts +116 -0
- package/dist/select/select-store.d.ts.map +1 -0
- package/dist/select/select-store.js +93 -0
- package/dist/select/select-store.js.map +1 -0
- package/dist/tab/tab-store.d.ts +127 -0
- package/dist/tab/tab-store.d.ts.map +1 -0
- package/dist/tab/tab-store.js +107 -0
- package/dist/tab/tab-store.js.map +1 -0
- package/dist/tag/tag-store.d.ts +2 -0
- package/dist/tag/tag-store.js +60 -0
- package/dist/tag/tag-store.js.map +1 -0
- package/dist/tag-store-D47X5_zA.d.ts +83 -0
- package/dist/tag-store-D47X5_zA.d.ts.map +1 -0
- package/dist/toolbar/toolbar-store.d.ts +21 -0
- package/dist/toolbar/toolbar-store.d.ts.map +1 -0
- package/dist/toolbar/toolbar-store.js +18 -0
- package/dist/toolbar/toolbar-store.js.map +1 -0
- package/dist/tooltip/tooltip-store.d.ts +35 -0
- package/dist/tooltip/tooltip-store.d.ts.map +1 -0
- package/dist/tooltip/tooltip-store.js +29 -0
- package/dist/tooltip/tooltip-store.js.map +1 -0
- package/license +21 -0
- package/package.json +121 -0
- package/readme.md +19 -0
- package/src/checkbox/checkbox-store.ts +93 -0
- package/src/collection/collection-store.ts +301 -0
- package/src/combobox/combobox-store.ts +382 -0
- package/src/composite/composite-overflow-store.ts +30 -0
- package/src/composite/composite-store.ts +711 -0
- package/src/dialog/dialog-store.ts +26 -0
- package/src/disclosure/disclosure-store.ts +226 -0
- package/src/form/form-store.ts +608 -0
- package/src/form/types.ts +44 -0
- package/src/hovercard/hovercard-store.ts +112 -0
- package/src/index.ts +1 -0
- package/src/menu/menu-bar-store.ts +28 -0
- package/src/menu/menu-store.ts +263 -0
- package/src/menubar/menubar-store.ts +51 -0
- package/src/popover/popover-store.ts +170 -0
- package/src/radio/radio-store.ts +80 -0
- package/src/select/select-store.ts +323 -0
- package/src/tab/tab-store.ts +330 -0
- package/src/tag/tag-store.ts +170 -0
- package/src/toolbar/toolbar-store.ts +47 -0
- package/src/tooltip/tooltip-store.ts +93 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import {
|
|
2
|
+
batch,
|
|
3
|
+
createStore,
|
|
4
|
+
mergeStore,
|
|
5
|
+
omit,
|
|
6
|
+
setup,
|
|
7
|
+
sync,
|
|
8
|
+
throwOnConflictingProps,
|
|
9
|
+
} from "@ariakit/store";
|
|
10
|
+
import type { Store, StoreOptions, StoreProps } from "@ariakit/store";
|
|
11
|
+
import { toArray, defaultValue } from "@ariakit/utils";
|
|
12
|
+
import type { PickRequired, SetState } from "@ariakit/utils";
|
|
13
|
+
import type { ComboboxStore } from "../combobox/combobox-store.ts";
|
|
14
|
+
import type {
|
|
15
|
+
CompositeStoreFunctions,
|
|
16
|
+
CompositeStoreItem,
|
|
17
|
+
CompositeStoreOptions,
|
|
18
|
+
CompositeStoreState,
|
|
19
|
+
} from "../composite/composite-store.ts";
|
|
20
|
+
import { createCompositeStore } from "../composite/composite-store.ts";
|
|
21
|
+
import type {
|
|
22
|
+
PopoverStoreFunctions,
|
|
23
|
+
PopoverStoreOptions,
|
|
24
|
+
PopoverStoreState,
|
|
25
|
+
} from "../popover/popover-store.ts";
|
|
26
|
+
import { createPopoverStore } from "../popover/popover-store.ts";
|
|
27
|
+
|
|
28
|
+
type MutableValue<T extends SelectStoreValue = SelectStoreValue> =
|
|
29
|
+
T extends string ? string : T;
|
|
30
|
+
|
|
31
|
+
export function createSelectStore<
|
|
32
|
+
T extends SelectStoreValue = SelectStoreValue,
|
|
33
|
+
>(
|
|
34
|
+
props: PickRequired<SelectStoreProps<T>, "value" | "defaultValue">,
|
|
35
|
+
): SelectStore<T>;
|
|
36
|
+
|
|
37
|
+
export function createSelectStore(props?: SelectStoreProps): SelectStore;
|
|
38
|
+
|
|
39
|
+
export function createSelectStore({
|
|
40
|
+
combobox,
|
|
41
|
+
...props
|
|
42
|
+
}: SelectStoreProps = {}): SelectStore {
|
|
43
|
+
const store = mergeStore(
|
|
44
|
+
props.store,
|
|
45
|
+
omit(combobox, [
|
|
46
|
+
"value",
|
|
47
|
+
"items",
|
|
48
|
+
"renderedItems",
|
|
49
|
+
"baseElement",
|
|
50
|
+
"arrowElement",
|
|
51
|
+
"anchorElement",
|
|
52
|
+
"contentElement",
|
|
53
|
+
"popoverElement",
|
|
54
|
+
"disclosureElement",
|
|
55
|
+
]),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
throwOnConflictingProps(props, store);
|
|
59
|
+
|
|
60
|
+
const syncState = store.getState();
|
|
61
|
+
|
|
62
|
+
const composite = createCompositeStore({
|
|
63
|
+
...props,
|
|
64
|
+
store,
|
|
65
|
+
virtualFocus: defaultValue(
|
|
66
|
+
props.virtualFocus,
|
|
67
|
+
syncState.virtualFocus,
|
|
68
|
+
true,
|
|
69
|
+
),
|
|
70
|
+
includesBaseElement: defaultValue(
|
|
71
|
+
props.includesBaseElement,
|
|
72
|
+
syncState.includesBaseElement,
|
|
73
|
+
false,
|
|
74
|
+
),
|
|
75
|
+
activeId: defaultValue(
|
|
76
|
+
props.activeId,
|
|
77
|
+
syncState.activeId,
|
|
78
|
+
props.defaultActiveId,
|
|
79
|
+
null,
|
|
80
|
+
),
|
|
81
|
+
orientation: defaultValue(
|
|
82
|
+
props.orientation,
|
|
83
|
+
syncState.orientation,
|
|
84
|
+
"vertical" as const,
|
|
85
|
+
),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const popover = createPopoverStore({
|
|
89
|
+
...props,
|
|
90
|
+
store,
|
|
91
|
+
placement: defaultValue(
|
|
92
|
+
props.placement,
|
|
93
|
+
syncState.placement,
|
|
94
|
+
"bottom-start" as const,
|
|
95
|
+
),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const initialValue = new String("") as "";
|
|
99
|
+
|
|
100
|
+
const initialState: SelectStoreState = {
|
|
101
|
+
...composite.getState(),
|
|
102
|
+
...popover.getState(),
|
|
103
|
+
value: defaultValue(
|
|
104
|
+
props.value,
|
|
105
|
+
syncState.value,
|
|
106
|
+
props.defaultValue,
|
|
107
|
+
initialValue,
|
|
108
|
+
),
|
|
109
|
+
setValueOnMove: defaultValue(
|
|
110
|
+
props.setValueOnMove,
|
|
111
|
+
syncState.setValueOnMove,
|
|
112
|
+
false,
|
|
113
|
+
),
|
|
114
|
+
labelElement: defaultValue(syncState.labelElement, null),
|
|
115
|
+
selectElement: defaultValue(syncState.selectElement, null),
|
|
116
|
+
listElement: defaultValue(syncState.listElement, null),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const select = createStore(initialState, composite, popover, store);
|
|
120
|
+
|
|
121
|
+
// Automatically sets the default value if it's not set.
|
|
122
|
+
setup(select, () =>
|
|
123
|
+
sync(select, ["value", "items"], (state) => {
|
|
124
|
+
if (state.value !== initialValue) return;
|
|
125
|
+
if (!state.items.length) return;
|
|
126
|
+
const item = state.items.find(
|
|
127
|
+
(item) => !item.disabled && item.value != null,
|
|
128
|
+
);
|
|
129
|
+
if (item?.value == null) return;
|
|
130
|
+
select.setState("value", item.value);
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Resets the active id to its initial state when the popover is hidden. This
|
|
135
|
+
// guarantees that the active id won't be pointing to another item when the
|
|
136
|
+
// popover is shown again, which would cause the selected item to not be
|
|
137
|
+
// auto-focused. See test "clicking on different tab and clicking outside
|
|
138
|
+
// resets the selected tab".
|
|
139
|
+
setup(select, () =>
|
|
140
|
+
sync(select, ["mounted"], (state) => {
|
|
141
|
+
if (state.mounted) return;
|
|
142
|
+
select.setState("activeId", initialState.activeId);
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Sets the active id when the value changes and the popover is hidden.
|
|
147
|
+
setup(select, () =>
|
|
148
|
+
sync(select, ["mounted", "items", "value"], (state) => {
|
|
149
|
+
// TODO: Revisit this. See test "open with keyboard, then try to open
|
|
150
|
+
// again". Probably deprecate together with using ComboboxProvider as a
|
|
151
|
+
// parent of SelectProvider.
|
|
152
|
+
if (combobox) return;
|
|
153
|
+
if (state.mounted) return;
|
|
154
|
+
const values = toArray(state.value);
|
|
155
|
+
const lastValue = values[values.length - 1];
|
|
156
|
+
if (lastValue == null) return;
|
|
157
|
+
const item = state.items.find(
|
|
158
|
+
(item) => !item.disabled && item.value === lastValue,
|
|
159
|
+
);
|
|
160
|
+
if (!item) return;
|
|
161
|
+
select.setState("activeId", item.id);
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Sets the select value when the active item changes by moving (which usually
|
|
166
|
+
// happens when moving to an item using the keyboard).
|
|
167
|
+
setup(select, () =>
|
|
168
|
+
batch(select, ["setValueOnMove", "moves"], (state) => {
|
|
169
|
+
const { mounted, value, activeId } = select.getState();
|
|
170
|
+
if (!state.setValueOnMove && mounted) return;
|
|
171
|
+
if (Array.isArray(value)) return;
|
|
172
|
+
if (!state.moves) return;
|
|
173
|
+
if (!activeId) return;
|
|
174
|
+
const item = composite.item(activeId);
|
|
175
|
+
if (!item || item.disabled || item.value == null) return;
|
|
176
|
+
select.setState("value", item.value);
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
...composite,
|
|
182
|
+
...popover,
|
|
183
|
+
...select,
|
|
184
|
+
combobox,
|
|
185
|
+
setValue: (value) => select.setState("value", value),
|
|
186
|
+
setLabelElement: (element) => select.setState("labelElement", element),
|
|
187
|
+
setSelectElement: (element) => select.setState("selectElement", element),
|
|
188
|
+
setListElement: (element) => select.setState("listElement", element),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type SelectStoreValue = string | readonly string[];
|
|
193
|
+
|
|
194
|
+
export interface SelectStoreItem extends CompositeStoreItem {
|
|
195
|
+
value?: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface SelectStoreState<T extends SelectStoreValue = SelectStoreValue>
|
|
199
|
+
extends CompositeStoreState<SelectStoreItem>, PopoverStoreState {
|
|
200
|
+
/** @default true */
|
|
201
|
+
virtualFocus: CompositeStoreState<SelectStoreItem>["virtualFocus"];
|
|
202
|
+
/** @default null */
|
|
203
|
+
activeId: CompositeStoreState<SelectStoreItem>["activeId"];
|
|
204
|
+
/** @default "vertical" */
|
|
205
|
+
orientation: CompositeStoreState<SelectStoreItem>["orientation"];
|
|
206
|
+
/** @default "bottom-start" */
|
|
207
|
+
placement: PopoverStoreState["placement"];
|
|
208
|
+
/**
|
|
209
|
+
* The select value.
|
|
210
|
+
*
|
|
211
|
+
* Live examples:
|
|
212
|
+
* - [Form with Select](https://ariakit.com/examples/form-select)
|
|
213
|
+
* - [Select Grid](https://ariakit.com/examples/select-grid)
|
|
214
|
+
* - [Select with custom
|
|
215
|
+
* items](https://ariakit.com/examples/select-item-custom)
|
|
216
|
+
* - [Multi-Select](https://ariakit.com/examples/select-multiple)
|
|
217
|
+
* - [Toolbar with Select](https://ariakit.com/examples/toolbar-select)
|
|
218
|
+
* - [Select with Next.js App
|
|
219
|
+
* Router](https://ariakit.com/examples/select-next-router)
|
|
220
|
+
*/
|
|
221
|
+
value: MutableValue<T>;
|
|
222
|
+
/**
|
|
223
|
+
* Whether the select
|
|
224
|
+
* [`value`](https://ariakit.com/reference/select-provider#value) should be
|
|
225
|
+
* set when the active item changes by moving (which usually happens when
|
|
226
|
+
* moving to an item using the keyboard).
|
|
227
|
+
*
|
|
228
|
+
* Live examples:
|
|
229
|
+
* - [Select Grid](https://ariakit.com/examples/select-grid)
|
|
230
|
+
* - [Select with custom
|
|
231
|
+
* items](https://ariakit.com/examples/select-item-custom)
|
|
232
|
+
* @default false
|
|
233
|
+
*/
|
|
234
|
+
setValueOnMove: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* The select label element.
|
|
237
|
+
*/
|
|
238
|
+
labelElement: HTMLElement | null;
|
|
239
|
+
/**
|
|
240
|
+
* The select button element.
|
|
241
|
+
*
|
|
242
|
+
* Live examples:
|
|
243
|
+
* - [Form with Select](https://ariakit.com/examples/form-select)
|
|
244
|
+
*/
|
|
245
|
+
selectElement: HTMLElement | null;
|
|
246
|
+
/**
|
|
247
|
+
* The select list element.
|
|
248
|
+
*/
|
|
249
|
+
listElement: HTMLElement | null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface SelectStoreFunctions<
|
|
253
|
+
T extends SelectStoreValue = SelectStoreValue,
|
|
254
|
+
>
|
|
255
|
+
extends
|
|
256
|
+
Pick<SelectStoreOptions<T>, "combobox">,
|
|
257
|
+
CompositeStoreFunctions<SelectStoreItem>,
|
|
258
|
+
PopoverStoreFunctions {
|
|
259
|
+
/**
|
|
260
|
+
* Sets the [`value`](https://ariakit.com/reference/select-provider#value)
|
|
261
|
+
* state.
|
|
262
|
+
* @example
|
|
263
|
+
* store.setValue("Apple");
|
|
264
|
+
* store.setValue(["Apple", "Banana"]);
|
|
265
|
+
* store.setValue((value) => value === "Apple" ? "Banana" : "Apple"));
|
|
266
|
+
*/
|
|
267
|
+
setValue: SetState<SelectStoreState<T>["value"]>;
|
|
268
|
+
/**
|
|
269
|
+
* Sets the `labelElement` state.
|
|
270
|
+
*/
|
|
271
|
+
setLabelElement: SetState<SelectStoreState<T>["labelElement"]>;
|
|
272
|
+
/**
|
|
273
|
+
* Sets the `selectElement` state.
|
|
274
|
+
*/
|
|
275
|
+
setSelectElement: SetState<SelectStoreState<T>["selectElement"]>;
|
|
276
|
+
/**
|
|
277
|
+
* Sets the `listElement` state.
|
|
278
|
+
*/
|
|
279
|
+
setListElement: SetState<SelectStoreState<T>["listElement"]>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface SelectStoreOptions<
|
|
283
|
+
T extends SelectStoreValue = SelectStoreValue,
|
|
284
|
+
>
|
|
285
|
+
extends
|
|
286
|
+
StoreOptions<
|
|
287
|
+
SelectStoreState<T>,
|
|
288
|
+
| "virtualFocus"
|
|
289
|
+
| "activeId"
|
|
290
|
+
| "orientation"
|
|
291
|
+
| "placement"
|
|
292
|
+
| "value"
|
|
293
|
+
| "setValueOnMove"
|
|
294
|
+
>,
|
|
295
|
+
CompositeStoreOptions<SelectStoreItem>,
|
|
296
|
+
PopoverStoreOptions {
|
|
297
|
+
/**
|
|
298
|
+
* A reference to a combobox store. This is used when combining the combobox
|
|
299
|
+
* with a select (e.g., select with a search input). The stores will share the
|
|
300
|
+
* same state.
|
|
301
|
+
*/
|
|
302
|
+
combobox?: ComboboxStore | null;
|
|
303
|
+
/**
|
|
304
|
+
* The default value. If not set, the first non-disabled item will be used.
|
|
305
|
+
*
|
|
306
|
+
* Live examples:
|
|
307
|
+
* - [Form with Select](https://ariakit.com/examples/form-select)
|
|
308
|
+
* - [Animated Select](https://ariakit.com/examples/select-animated)
|
|
309
|
+
* - [Select with Combobox](https://ariakit.com/examples/select-combobox)
|
|
310
|
+
* - [SelectGroup](https://ariakit.com/examples/select-group)
|
|
311
|
+
* - [Select with Next.js App
|
|
312
|
+
* Router](https://ariakit.com/examples/select-next-router)
|
|
313
|
+
* - [Select with Combobox and
|
|
314
|
+
* Tabs](https://ariakit.com/examples/select-combobox-tab)
|
|
315
|
+
*/
|
|
316
|
+
defaultValue?: SelectStoreState<T>["value"];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface SelectStoreProps<T extends SelectStoreValue = SelectStoreValue>
|
|
320
|
+
extends SelectStoreOptions<T>, StoreProps<SelectStoreState<T>> {}
|
|
321
|
+
|
|
322
|
+
export interface SelectStore<T extends SelectStoreValue = SelectStoreValue>
|
|
323
|
+
extends SelectStoreFunctions<T>, Store<SelectStoreState<T>> {}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import {
|
|
2
|
+
batch,
|
|
3
|
+
createStore,
|
|
4
|
+
mergeStore,
|
|
5
|
+
omit,
|
|
6
|
+
setup,
|
|
7
|
+
sync,
|
|
8
|
+
} from "@ariakit/store";
|
|
9
|
+
import type { Store, StoreOptions, StoreProps } from "@ariakit/store";
|
|
10
|
+
import { chain, defaultValue } from "@ariakit/utils";
|
|
11
|
+
import type { SetState } from "@ariakit/utils";
|
|
12
|
+
import type {
|
|
13
|
+
CollectionStore,
|
|
14
|
+
CollectionStoreItem,
|
|
15
|
+
} from "../collection/collection-store.ts";
|
|
16
|
+
import { createCollectionStore } from "../collection/collection-store.ts";
|
|
17
|
+
import type { ComboboxStore } from "../combobox/combobox-store.ts";
|
|
18
|
+
import type {
|
|
19
|
+
CompositeStore,
|
|
20
|
+
CompositeStoreFunctions,
|
|
21
|
+
CompositeStoreItem,
|
|
22
|
+
CompositeStoreOptions,
|
|
23
|
+
CompositeStoreState,
|
|
24
|
+
} from "../composite/composite-store.ts";
|
|
25
|
+
import { createCompositeStore } from "../composite/composite-store.ts";
|
|
26
|
+
import type { SelectStore } from "../select/select-store.ts";
|
|
27
|
+
|
|
28
|
+
export function createTabStore({
|
|
29
|
+
composite: parentComposite,
|
|
30
|
+
combobox,
|
|
31
|
+
...props
|
|
32
|
+
}: TabStoreProps = {}): TabStore {
|
|
33
|
+
const independentKeys = [
|
|
34
|
+
"items",
|
|
35
|
+
"renderedItems",
|
|
36
|
+
"moves",
|
|
37
|
+
"orientation",
|
|
38
|
+
"virtualFocus",
|
|
39
|
+
"includesBaseElement",
|
|
40
|
+
"baseElement",
|
|
41
|
+
"focusLoop",
|
|
42
|
+
"focusShift",
|
|
43
|
+
"focusWrap",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
const store = mergeStore(
|
|
47
|
+
props.store,
|
|
48
|
+
omit(parentComposite, independentKeys),
|
|
49
|
+
omit(combobox, independentKeys),
|
|
50
|
+
);
|
|
51
|
+
const syncState = store?.getState();
|
|
52
|
+
|
|
53
|
+
const composite = createCompositeStore({
|
|
54
|
+
...props,
|
|
55
|
+
store,
|
|
56
|
+
// We need to explicitly set the default value of `includesBaseElement` to
|
|
57
|
+
// `false` since we don't want the composite store to default it to `true`
|
|
58
|
+
// when the activeId state is null, which could be the case when rendering
|
|
59
|
+
// combobox with tab.
|
|
60
|
+
includesBaseElement: defaultValue(
|
|
61
|
+
props.includesBaseElement,
|
|
62
|
+
syncState?.includesBaseElement,
|
|
63
|
+
false,
|
|
64
|
+
),
|
|
65
|
+
orientation: defaultValue(
|
|
66
|
+
props.orientation,
|
|
67
|
+
syncState?.orientation,
|
|
68
|
+
"horizontal" as const,
|
|
69
|
+
),
|
|
70
|
+
focusLoop: defaultValue(props.focusLoop, syncState?.focusLoop, true),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const panels = createCollectionStore<TabStorePanel>();
|
|
74
|
+
|
|
75
|
+
const initialState: TabStoreState = {
|
|
76
|
+
...composite.getState(),
|
|
77
|
+
selectedId: defaultValue(
|
|
78
|
+
props.selectedId,
|
|
79
|
+
syncState?.selectedId,
|
|
80
|
+
props.defaultSelectedId,
|
|
81
|
+
),
|
|
82
|
+
selectOnMove: defaultValue(
|
|
83
|
+
props.selectOnMove,
|
|
84
|
+
syncState?.selectOnMove,
|
|
85
|
+
true,
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
const tab = createStore(initialState, composite, store);
|
|
89
|
+
|
|
90
|
+
// Selects the active tab when selectOnMove is true. Since we're listening to
|
|
91
|
+
// the moves state, but not the activeId state, this callback will run only
|
|
92
|
+
// when there's a move, which is usually triggered by moving through the tabs
|
|
93
|
+
// using the keyboard.
|
|
94
|
+
setup(tab, () =>
|
|
95
|
+
sync(tab, ["moves"], () => {
|
|
96
|
+
const { activeId, selectOnMove } = tab.getState();
|
|
97
|
+
if (!selectOnMove) return;
|
|
98
|
+
if (!activeId) return;
|
|
99
|
+
const tabItem = composite.item(activeId);
|
|
100
|
+
if (!tabItem) return;
|
|
101
|
+
if (tabItem.dimmed) return;
|
|
102
|
+
if (tabItem.disabled) return;
|
|
103
|
+
tab.setState("selectedId", tabItem.id);
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
let syncActiveId = true;
|
|
108
|
+
|
|
109
|
+
// Keep activeId in sync with selectedId.
|
|
110
|
+
setup(tab, () =>
|
|
111
|
+
batch(tab, ["selectedId"], (state, prev) => {
|
|
112
|
+
// There are cases where we don't want to sync activeId with selectedId.
|
|
113
|
+
// For example, restoring the selectedId from a select or combobox
|
|
114
|
+
// selected value. In those cases, we set syncActiveId to false.
|
|
115
|
+
if (!syncActiveId) {
|
|
116
|
+
syncActiveId = true;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// If there's a parent composite widget, we don't need to sync the
|
|
120
|
+
// activeId state with the initial selectedId state. The parent composite
|
|
121
|
+
// widget should handle the initial activeId state.
|
|
122
|
+
if (parentComposite && state.selectedId === prev.selectedId) return;
|
|
123
|
+
tab.setState("activeId", state.selectedId);
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Automatically set selectedId if it's undefined.
|
|
128
|
+
setup(tab, () =>
|
|
129
|
+
sync(tab, ["selectedId", "renderedItems"], (state) => {
|
|
130
|
+
if (state.selectedId !== undefined) return;
|
|
131
|
+
// First, we try to set selectedId based on the current active tab.
|
|
132
|
+
const { activeId, renderedItems } = tab.getState();
|
|
133
|
+
const tabItem = composite.item(activeId);
|
|
134
|
+
if (tabItem && !tabItem.disabled && !tabItem.dimmed) {
|
|
135
|
+
tab.setState("selectedId", tabItem.id);
|
|
136
|
+
}
|
|
137
|
+
// If there's no active tab or the active tab is dimmed, we get the
|
|
138
|
+
// first enabled tab instead.
|
|
139
|
+
else {
|
|
140
|
+
const tabItem = renderedItems.find(
|
|
141
|
+
(item) => !item.disabled && !item.dimmed,
|
|
142
|
+
);
|
|
143
|
+
tab.setState("selectedId", tabItem?.id);
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Keep panels tabIds in sync with the current tabs.
|
|
149
|
+
setup(tab, () =>
|
|
150
|
+
sync(tab, ["renderedItems"], (state) => {
|
|
151
|
+
const tabs = state.renderedItems;
|
|
152
|
+
if (!tabs.length) return;
|
|
153
|
+
return sync(panels, ["renderedItems"], (state) => {
|
|
154
|
+
const items = state.renderedItems;
|
|
155
|
+
const hasOrphanPanels = items.some((panel) => !panel.tabId);
|
|
156
|
+
if (!hasOrphanPanels) return;
|
|
157
|
+
items.forEach((panel, i) => {
|
|
158
|
+
if (panel.tabId) return;
|
|
159
|
+
const tabItem = tabs[i];
|
|
160
|
+
if (!tabItem) return;
|
|
161
|
+
panels.renderItem({ ...panel, tabId: tabItem.id });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Preserve the selected tab when a select or combobox value is selected
|
|
168
|
+
// within the tab panel.
|
|
169
|
+
let selectedIdFromSelectedValue: string | null | undefined = null;
|
|
170
|
+
|
|
171
|
+
setup(tab, () => {
|
|
172
|
+
const backupSelectedId = () => {
|
|
173
|
+
selectedIdFromSelectedValue = tab.getState().selectedId;
|
|
174
|
+
};
|
|
175
|
+
const restoreSelectedId = () => {
|
|
176
|
+
// We set syncActiveId to false to prevent the activeId state from being
|
|
177
|
+
// set to the selectedId state since this is just a restoration of the
|
|
178
|
+
// selectedId state from a select or combobox selected value.
|
|
179
|
+
syncActiveId = false;
|
|
180
|
+
tab.setState("selectedId", selectedIdFromSelectedValue);
|
|
181
|
+
};
|
|
182
|
+
if (parentComposite && "setSelectElement" in parentComposite) {
|
|
183
|
+
return chain(
|
|
184
|
+
sync(parentComposite, ["value"], backupSelectedId),
|
|
185
|
+
sync(parentComposite, ["mounted"], restoreSelectedId),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (!combobox) return;
|
|
189
|
+
return chain(
|
|
190
|
+
sync(combobox, ["selectedValue"], backupSelectedId),
|
|
191
|
+
sync(combobox, ["mounted"], restoreSelectedId),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...composite,
|
|
197
|
+
...tab,
|
|
198
|
+
panels,
|
|
199
|
+
setSelectedId: (id) => tab.setState("selectedId", id),
|
|
200
|
+
select: (id) => {
|
|
201
|
+
tab.setState("selectedId", id);
|
|
202
|
+
composite.move(id);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface TabStoreItem extends CompositeStoreItem {
|
|
208
|
+
dimmed?: boolean;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface TabStorePanel extends CollectionStoreItem {
|
|
212
|
+
tabId?: string | null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface TabStoreState extends CompositeStoreState<TabStoreItem> {
|
|
216
|
+
/** @default "horizontal" */
|
|
217
|
+
orientation: CompositeStoreState<TabStoreItem>["orientation"];
|
|
218
|
+
/** @default true */
|
|
219
|
+
focusLoop: CompositeStoreState<TabStoreItem>["focusLoop"];
|
|
220
|
+
/**
|
|
221
|
+
* The id of the tab whose panel is currently visible. If it's `undefined`, it
|
|
222
|
+
* will be automatically set to the first enabled tab.
|
|
223
|
+
*
|
|
224
|
+
* Live examples:
|
|
225
|
+
* - [Tab with React Router](https://ariakit.com/examples/tab-react-router)
|
|
226
|
+
* - [Combobox with Tabs](https://ariakit.com/examples/combobox-tabs)
|
|
227
|
+
* - [Select with Combobox and
|
|
228
|
+
* Tabs](https://ariakit.com/examples/select-combobox-tab)
|
|
229
|
+
* - [Command Menu with
|
|
230
|
+
* Tabs](https://ariakit.com/examples/dialog-combobox-tab-command-menu)
|
|
231
|
+
*/
|
|
232
|
+
selectedId: TabStoreState["activeId"];
|
|
233
|
+
/**
|
|
234
|
+
* Determines if the tab should be selected when it receives focus. If set to
|
|
235
|
+
* `false`, the tab will only be selected upon clicking, not when using arrow
|
|
236
|
+
* keys to shift focus.
|
|
237
|
+
*
|
|
238
|
+
* Live examples:
|
|
239
|
+
* - [Tab with React Router](https://ariakit.com/examples/tab-react-router)
|
|
240
|
+
* - [Select with Combobox and
|
|
241
|
+
* Tabs](https://ariakit.com/examples/select-combobox-tab)
|
|
242
|
+
* @default true
|
|
243
|
+
*/
|
|
244
|
+
selectOnMove?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface TabStoreFunctions extends CompositeStoreFunctions<TabStoreItem> {
|
|
248
|
+
/**
|
|
249
|
+
* Sets the
|
|
250
|
+
* [`selectedId`](https://ariakit.com/reference/tab-provider#selectedid) state
|
|
251
|
+
* without moving focus. If you want to move focus, use the
|
|
252
|
+
* [`select`](https://ariakit.com/reference/use-tab-store#select) function
|
|
253
|
+
* instead.
|
|
254
|
+
* @example
|
|
255
|
+
* // Selects the tab with id "tab-1"
|
|
256
|
+
* store.setSelectedId("tab-1");
|
|
257
|
+
* // Toggles between "tab-1" and "tab-2"
|
|
258
|
+
* store.setSelectedId((id) => id === "tab-1" ? "tab-2" : "tab-1"));
|
|
259
|
+
* // Selects the first tab
|
|
260
|
+
* store.setSelectedId(store.first());
|
|
261
|
+
* // Selects the next tab
|
|
262
|
+
* store.setSelectedId(store.next());
|
|
263
|
+
*/
|
|
264
|
+
setSelectedId: SetState<TabStoreState["selectedId"]>;
|
|
265
|
+
/**
|
|
266
|
+
* A collection store containing the tab panels.
|
|
267
|
+
*
|
|
268
|
+
* Live examples:
|
|
269
|
+
* - [Animated TabPanel](https://ariakit.com/examples/tab-panel-animated)
|
|
270
|
+
*/
|
|
271
|
+
panels: CollectionStore<TabStorePanel>;
|
|
272
|
+
/**
|
|
273
|
+
* Selects the tab for the given id and moves focus to it. If you want to set
|
|
274
|
+
* the [`selectedId`](https://ariakit.com/reference/tab-provider#selectedid)
|
|
275
|
+
* state without moving focus, use the
|
|
276
|
+
* [`setSelectedId`](https://ariakit.com/reference/use-tab-store#setselectedid-1)
|
|
277
|
+
* function instead.
|
|
278
|
+
* @example
|
|
279
|
+
* // Selects the tab with id "tab-1"
|
|
280
|
+
* store.select("tab-1");
|
|
281
|
+
* // Selects the first tab
|
|
282
|
+
* store.select(store.first());
|
|
283
|
+
* // Selects the next tab
|
|
284
|
+
* store.select(store.next());
|
|
285
|
+
*/
|
|
286
|
+
select: TabStore["move"];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface TabStoreOptions
|
|
290
|
+
extends
|
|
291
|
+
StoreOptions<
|
|
292
|
+
TabStoreState,
|
|
293
|
+
"orientation" | "focusLoop" | "selectedId" | "selectOnMove"
|
|
294
|
+
>,
|
|
295
|
+
CompositeStoreOptions<TabStoreItem> {
|
|
296
|
+
/**
|
|
297
|
+
* A reference to another [composite
|
|
298
|
+
* store](https://ariakit.com/reference/use-composite-store). This is used when
|
|
299
|
+
* rendering tabs as part of another composite widget such as
|
|
300
|
+
* [Combobox](https://ariakit.com/components/combobox) or
|
|
301
|
+
* [Select](https://ariakit.com/components/select). The stores will share the
|
|
302
|
+
* same state.
|
|
303
|
+
*/
|
|
304
|
+
composite?: CompositeStore | SelectStore | null;
|
|
305
|
+
/**
|
|
306
|
+
* A reference to a [combobox
|
|
307
|
+
* store](https://ariakit.com/reference/use-combobox-store). This is used when
|
|
308
|
+
* rendering tabs inside a
|
|
309
|
+
* [Combobox](https://ariakit.com/components/combobox).
|
|
310
|
+
*/
|
|
311
|
+
combobox?: ComboboxStore | null;
|
|
312
|
+
/**
|
|
313
|
+
* The id of the tab whose panel is currently visible. If it's `undefined`, it
|
|
314
|
+
* will be automatically set to the first enabled tab.
|
|
315
|
+
*
|
|
316
|
+
* Live examples:
|
|
317
|
+
* - [Combobox with Tabs](https://ariakit.com/examples/combobox-tabs)
|
|
318
|
+
* - [Animated TabPanel](https://ariakit.com/examples/tab-panel-animated)
|
|
319
|
+
* - [Select with Combobox and
|
|
320
|
+
* Tabs](https://ariakit.com/examples/select-combobox-tab)
|
|
321
|
+
* - [Command Menu with
|
|
322
|
+
* Tabs](https://ariakit.com/examples/dialog-combobox-tab-command-menu)
|
|
323
|
+
*/
|
|
324
|
+
defaultSelectedId?: TabStoreState["selectedId"];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface TabStoreProps
|
|
328
|
+
extends TabStoreOptions, StoreProps<TabStoreState> {}
|
|
329
|
+
|
|
330
|
+
export interface TabStore extends TabStoreFunctions, Store<TabStoreState> {}
|