@zag-js/combobox 0.47.0 → 0.49.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.
@@ -1,37 +1,39 @@
1
1
  import { ariaHidden } from "@zag-js/aria-hidden"
2
2
  import { createMachine, guards } from "@zag-js/core"
3
3
  import { trackDismissableElement } from "@zag-js/dismissable"
4
- import { raf, scrollIntoView } from "@zag-js/dom-query"
5
- import { observeAttributes, observeChildren } from "@zag-js/mutation-observer"
4
+ import { observeAttributes, observeChildren, raf, scrollIntoView } from "@zag-js/dom-query"
6
5
  import { getPlacement } from "@zag-js/popper"
7
- import { addOrRemove, compact, isEqual, match } from "@zag-js/utils"
6
+ import { addOrRemove, compact, isBoolean, isEqual, match } from "@zag-js/utils"
8
7
  import { collection } from "./combobox.collection"
9
8
  import { dom } from "./combobox.dom"
10
9
  import type { CollectionItem, MachineContext, MachineState, UserDefinedContext } from "./combobox.types"
11
10
 
12
11
  const { and, not } = guards
13
12
 
14
- const KEYDOWN_EVENT_REGEX = /(ARROW_UP|ARROW_DOWN|HOME|END|ENTER|ESCAPE)/
15
-
16
13
  export function machine<T extends CollectionItem>(userContext: UserDefinedContext<T>) {
17
14
  const ctx = compact(userContext)
18
15
  return createMachine<MachineContext, MachineState>(
19
16
  {
20
17
  id: "combobox",
21
- initial: ctx.autoFocus ? "focused" : "idle",
18
+ initial: ctx.open ? "suggesting" : "idle",
22
19
  context: {
23
- loop: true,
20
+ loopFocus: true,
24
21
  openOnClick: false,
25
- composing: false,
26
22
  value: [],
27
23
  highlightedValue: null,
28
24
  inputValue: "",
29
- selectOnBlur: true,
30
25
  allowCustomValue: false,
31
- closeOnSelect: true,
26
+ closeOnSelect: !ctx.multiple,
32
27
  inputBehavior: "none",
33
28
  selectionBehavior: "replace",
29
+ openOnKeyPress: true,
30
+ openOnChange: true,
31
+ dismissable: true,
32
+ popup: "listbox",
34
33
  ...ctx,
34
+ highlightedItem: null,
35
+ selectedItems: [],
36
+ valueAsString: "",
35
37
  collection: ctx.collection ?? collection.empty(),
36
38
  positioning: {
37
39
  placement: "bottom",
@@ -46,22 +48,22 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
46
48
  },
47
49
  },
48
50
 
49
- created: ["initialize"],
51
+ created: ["syncInitialValues", "syncSelectionBehavior"],
50
52
 
51
53
  computed: {
52
54
  isInputValueEmpty: (ctx) => ctx.inputValue.length === 0,
53
55
  isInteractive: (ctx) => !(ctx.readOnly || ctx.disabled),
54
56
  autoComplete: (ctx) => ctx.inputBehavior === "autocomplete",
55
57
  autoHighlight: (ctx) => ctx.inputBehavior === "autohighlight",
56
- selectedItems: (ctx) => ctx.collection.items(ctx.value),
57
- highlightedItem: (ctx) => ctx.collection.item(ctx.highlightedValue),
58
- valueAsString: (ctx) => ctx.collection.itemsToString(ctx.selectedItems),
59
58
  hasSelectedItems: (ctx) => ctx.value.length > 0,
60
59
  },
61
60
 
62
61
  watch: {
62
+ value: ["syncSelectedItems"],
63
63
  inputValue: ["syncInputValue"],
64
64
  highlightedValue: ["autofillInputValue"],
65
+ multiple: ["syncSelectionBehavior"],
66
+ open: ["toggleVisibility"],
65
67
  },
66
68
 
67
69
  on: {
@@ -80,16 +82,6 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
80
82
  "INPUT_VALUE.SET": {
81
83
  actions: "setInputValue",
82
84
  },
83
- "VALUE.CLEAR": {
84
- target: "focused",
85
- actions: ["clearInputValue", "clearSelectedItems"],
86
- },
87
- "INPUT.COMPOSITION_START": {
88
- actions: ["setIsComposing"],
89
- },
90
- "INPUT.COMPOSITION_END": {
91
- actions: ["clearIsComposing"],
92
- },
93
85
  "COLLECTION.SET": {
94
86
  actions: ["setCollection"],
95
87
  },
@@ -103,33 +95,76 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
103
95
  tags: ["idle", "closed"],
104
96
  entry: ["scrollContentToTop", "clearHighlightedItem"],
105
97
  on: {
106
- "TRIGGER.CLICK": {
98
+ "CONTROLLED.OPEN": {
107
99
  target: "interacting",
108
- actions: ["focusInput", "highlightFirstSelectedItem", "invokeOnOpen"],
109
- },
110
- "INPUT.CLICK": {
111
- guard: "openOnClick",
112
- target: "interacting",
113
- actions: ["highlightFirstSelectedItem", "invokeOnOpen"],
114
100
  },
101
+ "TRIGGER.CLICK": [
102
+ {
103
+ guard: "isOpenControlled",
104
+ actions: ["focusInput", "highlightFirstSelectedItem", "invokeOnOpen"],
105
+ },
106
+ {
107
+ target: "interacting",
108
+ actions: ["focusInput", "highlightFirstSelectedItem", "invokeOnOpen"],
109
+ },
110
+ ],
111
+ "INPUT.CLICK": [
112
+ {
113
+ guard: "isOpenControlled",
114
+ actions: ["invokeOnOpen"],
115
+ },
116
+ {
117
+ target: "interacting",
118
+ actions: ["highlightFirstSelectedItem", "invokeOnOpen"],
119
+ },
120
+ ],
115
121
  "INPUT.FOCUS": {
116
122
  target: "focused",
117
123
  },
118
- OPEN: {
119
- target: "interacting",
120
- actions: ["invokeOnOpen"],
124
+ OPEN: [
125
+ {
126
+ guard: "isOpenControlled",
127
+ actions: ["invokeOnOpen"],
128
+ },
129
+ {
130
+ target: "interacting",
131
+ actions: ["invokeOnOpen"],
132
+ },
133
+ ],
134
+ "VALUE.CLEAR": {
135
+ target: "focused",
136
+ actions: ["clearInputValue", "clearSelectedItems"],
121
137
  },
122
138
  },
123
139
  },
124
140
 
125
141
  focused: {
126
142
  tags: ["focused", "closed"],
127
- entry: ["focusInput", "scrollContentToTop", "clearHighlightedItem"],
143
+ entry: ["focusInputOrTrigger", "scrollContentToTop", "clearHighlightedItem"],
128
144
  on: {
129
- "INPUT.CHANGE": {
130
- target: "suggesting",
131
- actions: "setInputValue",
132
- },
145
+ "CONTROLLED.OPEN": [
146
+ {
147
+ guard: "isChangeEvent",
148
+ target: "suggesting",
149
+ },
150
+ {
151
+ target: "interacting",
152
+ },
153
+ ],
154
+ "INPUT.CHANGE": [
155
+ {
156
+ guard: and("isOpenControlled", "openOnChange"),
157
+ actions: ["setInputValue", "invokeOnOpen"],
158
+ },
159
+ {
160
+ guard: "openOnChange",
161
+ target: "suggesting",
162
+ actions: ["setInputValue", "invokeOnOpen"],
163
+ },
164
+ {
165
+ actions: "setInputValue",
166
+ },
167
+ ],
133
168
  "LAYER.INTERACT_OUTSIDE": {
134
169
  target: "idle",
135
170
  },
@@ -140,62 +175,104 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
140
175
  "INPUT.BLUR": {
141
176
  target: "idle",
142
177
  },
143
- "INPUT.CLICK": {
144
- guard: "openOnClick",
145
- target: "interacting",
146
- actions: ["highlightFirstSelectedItem", "invokeOnOpen"],
147
- },
148
- "TRIGGER.CLICK": {
149
- target: "interacting",
150
- actions: ["focusInput", "highlightFirstSelectedItem", "invokeOnOpen"],
151
- },
178
+ "INPUT.CLICK": [
179
+ {
180
+ guard: "isOpenControlled",
181
+ actions: ["highlightFirstSelectedItem", "invokeOnOpen"],
182
+ },
183
+ {
184
+ target: "interacting",
185
+ actions: ["highlightFirstSelectedItem", "invokeOnOpen"],
186
+ },
187
+ ],
188
+ "TRIGGER.CLICK": [
189
+ {
190
+ guard: "isOpenControlled",
191
+ actions: ["focusInput", "highlightFirstSelectedItem", "invokeOnOpen"],
192
+ },
193
+ {
194
+ target: "interacting",
195
+ actions: ["focusInput", "highlightFirstSelectedItem", "invokeOnOpen"],
196
+ },
197
+ ],
152
198
  "INPUT.ARROW_DOWN": [
199
+ // == group 1 ==
200
+ {
201
+ guard: and("isOpenControlled", "autoComplete"),
202
+ actions: ["invokeOnOpen"],
203
+ },
153
204
  {
154
205
  guard: "autoComplete",
155
206
  target: "interacting",
156
207
  actions: ["invokeOnOpen"],
157
208
  },
209
+ // == group 2 ==
158
210
  {
159
- guard: "hasSelectedItems",
160
- target: "interacting",
161
- actions: ["highlightFirstSelectedItem", "invokeOnOpen"],
211
+ guard: "isOpenControlled",
212
+ actions: ["highlightFirstOrSelectedItem", "invokeOnOpen"],
162
213
  },
163
214
  {
164
215
  target: "interacting",
165
- actions: ["highlightFirstItem", "invokeOnOpen"],
216
+ actions: ["highlightFirstOrSelectedItem", "invokeOnOpen"],
166
217
  },
167
218
  ],
168
- "INPUT.ARROW_DOWN+ALT": {
169
- target: "interacting",
170
- actions: "invokeOnOpen",
171
- },
172
219
  "INPUT.ARROW_UP": [
220
+ // == group 1 ==
173
221
  {
174
222
  guard: "autoComplete",
175
223
  target: "interacting",
176
224
  actions: "invokeOnOpen",
177
225
  },
178
226
  {
179
- guard: "hasSelectedItems",
227
+ guard: "autoComplete",
180
228
  target: "interacting",
181
- actions: ["highlightFirstSelectedItem", "invokeOnOpen"],
229
+ actions: "invokeOnOpen",
182
230
  },
231
+ // == group 2 ==
183
232
  {
184
233
  target: "interacting",
185
- actions: ["highlightLastItem", "invokeOnOpen"],
234
+ actions: ["highlightLastOrSelectedItem", "invokeOnOpen"],
235
+ },
236
+ {
237
+ target: "interacting",
238
+ actions: ["highlightLastOrSelectedItem", "invokeOnOpen"],
186
239
  },
187
240
  ],
188
- OPEN: {
189
- target: "interacting",
190
- actions: ["invokeOnOpen"],
241
+ OPEN: [
242
+ {
243
+ guard: "isOpenControlled",
244
+ actions: ["invokeOnOpen"],
245
+ },
246
+ {
247
+ target: "interacting",
248
+ actions: ["invokeOnOpen"],
249
+ },
250
+ ],
251
+ "VALUE.CLEAR": {
252
+ actions: ["clearInputValue", "clearSelectedItems"],
191
253
  },
192
254
  },
193
255
  },
194
256
 
195
257
  interacting: {
196
258
  tags: ["open", "focused"],
197
- activities: ["scrollIntoView", "trackDismissableLayer", "computePlacement", "hideOtherElements"],
259
+ activities: [
260
+ "scrollIntoView",
261
+ "trackDismissableLayer",
262
+ "computePlacement",
263
+ "hideOtherElements",
264
+ "trackContentHeight",
265
+ ],
198
266
  on: {
267
+ "CONTROLLED.CLOSE": [
268
+ {
269
+ guard: "restoreFocus",
270
+ target: "focused",
271
+ },
272
+ {
273
+ target: "idle",
274
+ },
275
+ ],
199
276
  "INPUT.HOME": {
200
277
  actions: ["highlightFirstItem"],
201
278
  },
@@ -220,31 +297,32 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
220
297
  actions: "highlightPrevItem",
221
298
  },
222
299
  ],
223
- "INPUT.ARROW_UP+ALT": {
224
- target: "focused",
225
- },
226
300
  "INPUT.ENTER": [
227
301
  {
228
- guard: not("closeOnSelect"),
229
- actions: ["selectHighlightedItem"],
302
+ guard: and("isOpenControlled", "closeOnSelect"),
303
+ actions: ["selectHighlightedItem", "invokeOnClose"],
230
304
  },
231
305
  {
306
+ guard: "closeOnSelect",
232
307
  target: "focused",
233
308
  actions: ["selectHighlightedItem", "invokeOnClose"],
234
309
  },
310
+ {
311
+ actions: ["selectHighlightedItem"],
312
+ },
235
313
  ],
236
314
  "INPUT.CHANGE": [
237
315
  {
238
316
  guard: "autoComplete",
239
317
  target: "suggesting",
240
- actions: ["setInputValue"],
318
+ actions: ["setInputValue", "invokeOnOpen"],
241
319
  },
242
320
  {
243
321
  target: "suggesting",
244
- actions: ["clearHighlightedItem", "setInputValue"],
322
+ actions: ["clearHighlightedItem", "setInputValue", "invokeOnOpen"],
245
323
  },
246
324
  ],
247
- "ITEM.POINTER_OVER": {
325
+ "ITEM.POINTER_MOVE": {
248
326
  actions: ["setHighlightedItem"],
249
327
  },
250
328
  "ITEM.POINTER_LEAVE": {
@@ -252,49 +330,88 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
252
330
  },
253
331
  "ITEM.CLICK": [
254
332
  {
255
- guard: not("closeOnSelect"),
256
- actions: ["selectItem"],
333
+ guard: and("isOpenControlled", "closeOnSelect"),
334
+ actions: ["selectItem", "invokeOnClose"],
257
335
  },
258
336
  {
337
+ guard: "closeOnSelect",
259
338
  target: "focused",
260
339
  actions: ["selectItem", "invokeOnClose"],
261
340
  },
341
+ {
342
+ actions: ["selectItem"],
343
+ },
262
344
  ],
263
345
  "LAYER.ESCAPE": [
346
+ {
347
+ guard: and("isOpenControlled", "autoComplete"),
348
+ actions: ["syncInputValue", "invokeOnClose"],
349
+ },
264
350
  {
265
351
  guard: "autoComplete",
266
352
  target: "focused",
267
353
  actions: ["syncInputValue", "invokeOnClose"],
268
354
  },
355
+ {
356
+ guard: "isOpenControlled",
357
+ actions: "invokeOnClose",
358
+ },
269
359
  {
270
360
  target: "focused",
271
361
  actions: ["invokeOnClose"],
272
362
  },
273
363
  ],
274
- "TRIGGER.CLICK": {
275
- target: "focused",
276
- actions: "invokeOnClose",
277
- },
364
+ "TRIGGER.CLICK": [
365
+ {
366
+ guard: "isOpenControlled",
367
+ actions: "invokeOnClose",
368
+ },
369
+ {
370
+ target: "focused",
371
+ actions: "invokeOnClose",
372
+ },
373
+ ],
278
374
  "LAYER.INTERACT_OUTSIDE": [
375
+ // == group 1 ==
279
376
  {
280
- guard: and("selectOnBlur", "hasHighlightedItem"),
281
- target: "idle",
282
- actions: ["selectHighlightedItem", "invokeOnClose"],
377
+ guard: and("isOpenControlled", "isCustomValue", not("allowCustomValue")),
378
+ actions: ["revertInputValue", "invokeOnClose"],
283
379
  },
284
380
  {
285
381
  guard: and("isCustomValue", not("allowCustomValue")),
286
382
  target: "idle",
287
383
  actions: ["revertInputValue", "invokeOnClose"],
288
384
  },
385
+ // == group 2 ==
386
+ {
387
+ guard: "isOpenControlled",
388
+ actions: "invokeOnClose",
389
+ },
289
390
  {
290
391
  target: "idle",
291
392
  actions: "invokeOnClose",
292
393
  },
293
394
  ],
294
- CLOSE: {
295
- target: "focused",
296
- actions: "invokeOnClose",
297
- },
395
+ CLOSE: [
396
+ {
397
+ guard: "isOpenControlled",
398
+ actions: "invokeOnClose",
399
+ },
400
+ {
401
+ target: "focused",
402
+ actions: "invokeOnClose",
403
+ },
404
+ ],
405
+ "VALUE.CLEAR": [
406
+ {
407
+ guard: "isOpenControlled",
408
+ actions: ["clearInputValue", "clearSelectedItems", "invokeOnClose"],
409
+ },
410
+ {
411
+ target: "focused",
412
+ actions: ["clearInputValue", "clearSelectedItems", "invokeOnClose"],
413
+ },
414
+ ],
298
415
  },
299
416
  },
300
417
 
@@ -306,11 +423,20 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
306
423
  "computePlacement",
307
424
  "trackChildNodes",
308
425
  "hideOtherElements",
426
+ "trackContentHeight",
309
427
  ],
310
- entry: ["focusInput", "invokeOnOpen"],
428
+ entry: ["focusInput"],
311
429
  on: {
430
+ "CONTROLLED.CLOSE": [
431
+ {
432
+ guard: "restoreFocus",
433
+ target: "focused",
434
+ },
435
+ {
436
+ target: "idle",
437
+ },
438
+ ],
312
439
  CHILDREN_CHANGE: {
313
- guard: not("isHighlightedItemVisible"),
314
440
  actions: ["highlightFirstItem"],
315
441
  },
316
442
  "INPUT.ARROW_DOWN": {
@@ -321,9 +447,6 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
321
447
  target: "interacting",
322
448
  actions: "highlightPrevItem",
323
449
  },
324
- "INPUT.ARROW_UP+ALT": {
325
- target: "focused",
326
- },
327
450
  "INPUT.HOME": {
328
451
  target: "interacting",
329
452
  actions: ["highlightFirstItem"],
@@ -334,28 +457,38 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
334
457
  },
335
458
  "INPUT.ENTER": [
336
459
  {
337
- guard: not("closeOnSelect"),
338
- actions: ["selectHighlightedItem"],
460
+ guard: and("isOpenControlled", "closeOnSelect"),
461
+ actions: ["selectHighlightedItem", "invokeOnClose"],
339
462
  },
340
463
  {
464
+ guard: "closeOnSelect",
341
465
  target: "focused",
342
466
  actions: ["selectHighlightedItem", "invokeOnClose"],
343
467
  },
468
+ {
469
+ actions: ["selectHighlightedItem"],
470
+ },
344
471
  ],
345
472
  "INPUT.CHANGE": [
346
473
  {
347
474
  guard: "autoHighlight",
348
- actions: ["setInputValue", "highlightFirstItem"],
475
+ actions: ["setInputValue"],
349
476
  },
350
477
  {
351
- actions: ["clearHighlightedItem", "setInputValue"],
478
+ actions: ["setInputValue"],
352
479
  },
353
480
  ],
354
- "LAYER.ESCAPE": {
355
- target: "focused",
356
- actions: "invokeOnClose",
357
- },
358
- "ITEM.POINTER_OVER": {
481
+ "LAYER.ESCAPE": [
482
+ {
483
+ guard: "isOpenControlled",
484
+ actions: "invokeOnClose",
485
+ },
486
+ {
487
+ target: "focused",
488
+ actions: "invokeOnClose",
489
+ },
490
+ ],
491
+ "ITEM.POINTER_MOVE": {
359
492
  target: "interacting",
360
493
  actions: "setHighlightedItem",
361
494
  },
@@ -363,34 +496,70 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
363
496
  actions: "clearHighlightedItem",
364
497
  },
365
498
  "LAYER.INTERACT_OUTSIDE": [
499
+ // == group 1 ==
500
+ {
501
+ guard: and("isOpenControlled", "isCustomValue", not("allowCustomValue")),
502
+ actions: ["revertInputValue", "invokeOnClose"],
503
+ },
366
504
  {
367
505
  guard: and("isCustomValue", not("allowCustomValue")),
368
506
  target: "idle",
369
507
  actions: ["revertInputValue", "invokeOnClose"],
370
508
  },
509
+ // == group 2 ==
510
+ {
511
+ guard: "isOpenControlled",
512
+ actions: "invokeOnClose",
513
+ },
371
514
  {
372
515
  target: "idle",
373
516
  actions: "invokeOnClose",
374
517
  },
375
518
  ],
376
- "TRIGGER.CLICK": {
377
- target: "focused",
378
- actions: "invokeOnClose",
379
- },
519
+ "TRIGGER.CLICK": [
520
+ {
521
+ guard: "isOpenControlled",
522
+ actions: "invokeOnClose",
523
+ },
524
+ {
525
+ target: "focused",
526
+ actions: "invokeOnClose",
527
+ },
528
+ ],
380
529
  "ITEM.CLICK": [
381
530
  {
382
- guard: not("closeOnSelect"),
383
- actions: ["selectItem"],
531
+ guard: and("isOpenControlled", "closeOnSelect"),
532
+ actions: ["selectItem", "invokeOnClose"],
384
533
  },
385
534
  {
535
+ guard: "closeOnSelect",
386
536
  target: "focused",
387
537
  actions: ["selectItem", "invokeOnClose"],
388
538
  },
539
+ {
540
+ actions: ["selectItem"],
541
+ },
542
+ ],
543
+ CLOSE: [
544
+ {
545
+ guard: "isOpenControlled",
546
+ actions: "invokeOnClose",
547
+ },
548
+ {
549
+ target: "focused",
550
+ actions: "invokeOnClose",
551
+ },
552
+ ],
553
+ "VALUE.CLEAR": [
554
+ {
555
+ guard: "isOpenControlled",
556
+ actions: ["clearInputValue", "clearSelectedItems", "invokeOnClose"],
557
+ },
558
+ {
559
+ target: "focused",
560
+ actions: ["clearInputValue", "clearSelectedItems", "invokeOnClose"],
561
+ },
389
562
  ],
390
- CLOSE: {
391
- target: "focused",
392
- actions: "invokeOnClose",
393
- },
394
563
  },
395
564
  },
396
565
  },
@@ -398,7 +567,6 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
398
567
 
399
568
  {
400
569
  guards: {
401
- openOnClick: (ctx) => !!ctx.openOnClick,
402
570
  isInputValueEmpty: (ctx) => ctx.isInputValueEmpty,
403
571
  autoComplete: (ctx) => ctx.autoComplete && !ctx.multiple,
404
572
  autoHighlight: (ctx) => ctx.autoHighlight,
@@ -407,23 +575,23 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
407
575
  isCustomValue: (ctx) => ctx.inputValue !== ctx.valueAsString,
408
576
  allowCustomValue: (ctx) => !!ctx.allowCustomValue,
409
577
  hasHighlightedItem: (ctx) => ctx.highlightedValue != null,
410
- hasSelectedItems: (ctx) => ctx.hasSelectedItems,
411
- selectOnBlur: (ctx) => !!ctx.selectOnBlur,
412
- closeOnSelect: (ctx) => (ctx.multiple ? false : !!ctx.closeOnSelect),
413
- isHighlightedItemVisible: (ctx) => ctx.collection.has(ctx.highlightedValue),
578
+ closeOnSelect: (ctx) => !!ctx.closeOnSelect,
579
+ isOpenControlled: (ctx) => !!ctx["open.controlled"],
580
+ openOnChange: (ctx, evt) => {
581
+ if (isBoolean(ctx.openOnChange)) return ctx.openOnChange
582
+ return !!ctx.openOnChange?.({ inputValue: evt.value })
583
+ },
584
+ restoreFocus: (_ctx, evt) => (evt.restoreFocus == null ? true : !!evt.restoreFocus),
585
+ isChangeEvent: (_ctx, evt) => evt.previousEvent?.type === "INPUT.CHANGE",
414
586
  },
415
587
 
416
588
  activities: {
417
589
  trackDismissableLayer(ctx, _evt, { send }) {
590
+ if (!ctx.dismissable) return
418
591
  const contentEl = () => dom.getContentEl(ctx)
419
592
  return trackDismissableElement(contentEl, {
420
593
  defer: true,
421
- exclude: () => [
422
- dom.getInputEl(ctx),
423
- dom.getContentEl(ctx),
424
- dom.getTriggerEl(ctx),
425
- dom.getClearTriggerEl(ctx),
426
- ],
594
+ exclude: () => [dom.getInputEl(ctx), dom.getTriggerEl(ctx), dom.getClearTriggerEl(ctx)],
427
595
  onFocusOutside: ctx.onFocusOutside,
428
596
  onPointerDownOutside: ctx.onPointerDownOutside,
429
597
  onInteractOutside: ctx.onInteractOutside,
@@ -433,7 +601,7 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
433
601
  send("LAYER.ESCAPE")
434
602
  },
435
603
  onDismiss() {
436
- send("LAYER.INTERACT_OUTSIDE")
604
+ send({ type: "LAYER.INTERACT_OUTSIDE", restoreFocus: false })
437
605
  },
438
606
  })
439
607
  },
@@ -456,26 +624,64 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
456
624
  trackChildNodes(ctx, _evt, { send }) {
457
625
  if (!ctx.autoHighlight) return
458
626
  const exec = () => send("CHILDREN_CHANGE")
459
- exec()
460
- return observeChildren(dom.getContentEl(ctx), exec)
627
+ raf(() => exec())
628
+ const contentEl = () => dom.getContentEl(ctx)
629
+ return observeChildren(contentEl, {
630
+ callback: exec,
631
+ defer: true,
632
+ })
461
633
  },
462
634
  scrollIntoView(ctx, _evt, { getState }) {
463
635
  const inputEl = dom.getInputEl(ctx)
464
636
 
465
- const exec = () => {
637
+ const exec = (immediate: boolean) => {
466
638
  const state = getState()
467
639
 
468
- const isPointer = state.event.type.startsWith("ITEM.POINTER")
469
- if (isPointer || !ctx.highlightedValue) return
640
+ const pointer = state.event.type.startsWith("ITEM.POINTER")
641
+ if (pointer || !ctx.highlightedValue) return
470
642
 
471
643
  const optionEl = dom.getHighlightedItemEl(ctx)
472
644
  const contentEl = dom.getContentEl(ctx)
473
645
 
646
+ if (ctx.scrollToIndexFn) {
647
+ const highlightedIndex = ctx.collection.indexOf(ctx.highlightedValue)
648
+ ctx.scrollToIndexFn({ index: highlightedIndex, immediate })
649
+ return
650
+ }
651
+
474
652
  scrollIntoView(optionEl, { rootEl: contentEl, block: "nearest" })
475
653
  }
476
654
 
477
- raf(() => exec())
478
- return observeAttributes(inputEl, ["aria-activedescendant"], exec)
655
+ raf(() => exec(true))
656
+ return observeAttributes(inputEl, {
657
+ attributes: ["aria-activedescendant"],
658
+ callback: () => exec(false),
659
+ })
660
+ },
661
+ trackContentHeight(ctx) {
662
+ let cleanup: VoidFunction
663
+
664
+ raf(() => {
665
+ const contentEl = dom.getContentEl(ctx)
666
+ const listboxEl = dom.getListEl(ctx)
667
+
668
+ if (!contentEl || !listboxEl) return
669
+ const win = dom.getWin(ctx)
670
+
671
+ let rafId: number
672
+ const observer = new win.ResizeObserver(() => {
673
+ rafId = requestAnimationFrame(() => {
674
+ contentEl.style.setProperty(`--height`, `${listboxEl.offsetHeight}px`)
675
+ })
676
+ })
677
+ observer.observe(contentEl)
678
+ cleanup = () => {
679
+ cancelAnimationFrame(rafId)
680
+ observer.unobserve(contentEl)
681
+ }
682
+ })
683
+
684
+ return () => cleanup?.()
479
685
  },
480
686
  },
481
687
 
@@ -493,12 +699,6 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
493
699
  },
494
700
  })
495
701
  },
496
- setIsComposing(ctx) {
497
- ctx.composing = true
498
- },
499
- clearIsComposing(ctx) {
500
- ctx.composing = false
501
- },
502
702
  setHighlightedItem(ctx, evt) {
503
703
  set.highlightedItem(ctx, evt.value)
504
704
  },
@@ -516,19 +716,29 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
516
716
  set.selectedItems(ctx, value)
517
717
  },
518
718
  focusInput(ctx) {
519
- if (dom.isInputFocused(ctx)) return
520
- dom.getInputEl(ctx)?.focus({ preventScroll: true })
719
+ // use raf since the input might be rendered in the content
720
+ raf(() => {
721
+ if (dom.isInputFocused(ctx)) return
722
+ dom.getInputEl(ctx)?.focus({ preventScroll: true })
723
+ })
724
+ },
725
+ focusInputOrTrigger(ctx) {
726
+ queueMicrotask(() => {
727
+ if (ctx.popup === "dialog") {
728
+ dom.getTriggerEl(ctx)?.focus({ preventScroll: true })
729
+ } else {
730
+ dom.getInputEl(ctx)?.focus({ preventScroll: true })
731
+ }
732
+ })
521
733
  },
522
734
  syncInputValue(ctx, evt) {
523
- const isTyping = !KEYDOWN_EVENT_REGEX.test(evt.type)
524
735
  const inputEl = dom.getInputEl(ctx)
525
-
526
736
  if (!inputEl) return
737
+
527
738
  inputEl.value = ctx.inputValue
528
739
 
529
740
  raf(() => {
530
- if (isTyping) return
531
-
741
+ if (!evt.keypress) return
532
742
  const { selectionStart, selectionEnd } = inputEl
533
743
 
534
744
  if (Math.abs((selectionEnd ?? 0) - (selectionStart ?? 0)) !== 0) return
@@ -544,24 +754,33 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
544
754
  set.inputValue(ctx, "")
545
755
  },
546
756
  revertInputValue(ctx) {
547
- set.inputValue(
548
- ctx,
549
- match(ctx.selectionBehavior, {
550
- replace: ctx.hasSelectedItems ? ctx.valueAsString : "",
551
- clear: "",
552
- preserve: ctx.inputValue,
553
- }),
554
- )
555
- },
556
- initialize(ctx) {
557
- const items = ctx.collection.items(ctx.value)
558
- const valueAsString = ctx.collection.itemsToString(items)
757
+ const inputValue = match(ctx.selectionBehavior, {
758
+ replace: ctx.hasSelectedItems ? ctx.valueAsString : "",
759
+ preserve: ctx.inputValue,
760
+ clear: "",
761
+ })
762
+
763
+ set.inputValue(ctx, inputValue)
764
+ },
765
+ syncInitialValues(ctx) {
766
+ const selectedItems = ctx.collection.items(ctx.value)
767
+ const valueAsString = ctx.collection.itemsToString(selectedItems)
768
+
769
+ ctx.highlightedItem = ctx.collection.item(ctx.highlightedValue)
770
+ ctx.selectedItems = selectedItems
771
+ ctx.valueAsString = valueAsString
772
+
559
773
  ctx.inputValue = match(ctx.selectionBehavior, {
560
- preserve: valueAsString,
774
+ preserve: ctx.inputValue || valueAsString,
561
775
  replace: valueAsString,
562
776
  clear: "",
563
777
  })
564
778
  },
779
+ syncSelectionBehavior(ctx) {
780
+ if (ctx.multiple) {
781
+ ctx.selectionBehavior = "clear"
782
+ }
783
+ },
565
784
  setSelectedItems(ctx, evt) {
566
785
  set.selectedItems(ctx, evt.value)
567
786
  },
@@ -569,9 +788,13 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
569
788
  set.selectedItems(ctx, [])
570
789
  },
571
790
  scrollContentToTop(ctx) {
572
- const contentEl = dom.getContentEl(ctx)
573
- if (!contentEl) return
574
- contentEl.scrollTop = 0
791
+ if (ctx.scrollToIndexFn) {
792
+ ctx.scrollToIndexFn({ index: 0, immediate: true })
793
+ } else {
794
+ const contentEl = dom.getContentEl(ctx)
795
+ if (!contentEl) return
796
+ contentEl.scrollTop = 0
797
+ }
575
798
  },
576
799
  invokeOnOpen(ctx) {
577
800
  ctx.onOpenChange?.({ open: true })
@@ -580,28 +803,68 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
580
803
  ctx.onOpenChange?.({ open: false })
581
804
  },
582
805
  highlightFirstItem(ctx) {
583
- const value = ctx.collection.first()
584
- set.highlightedItem(ctx, value)
806
+ raf(() => {
807
+ const value = ctx.collection.first()
808
+ set.highlightedItem(ctx, value)
809
+ })
585
810
  },
586
811
  highlightLastItem(ctx) {
587
- const value = ctx.collection.last()
588
- set.highlightedItem(ctx, value)
812
+ raf(() => {
813
+ const value = ctx.collection.last()
814
+ set.highlightedItem(ctx, value)
815
+ })
589
816
  },
590
817
  highlightNextItem(ctx) {
591
- const value = ctx.collection.next(ctx.highlightedValue) ?? (ctx.loop ? ctx.collection.first() : null)
818
+ let value: string | null = null
819
+ if (ctx.highlightedValue) {
820
+ value = ctx.collection.next(ctx.highlightedValue)
821
+ if (!value && ctx.loopFocus) value = ctx.collection.first()
822
+ } else {
823
+ value = ctx.collection.first()
824
+ }
592
825
  set.highlightedItem(ctx, value)
593
826
  },
594
827
  highlightPrevItem(ctx) {
595
- const value = ctx.collection.prev(ctx.highlightedValue) ?? (ctx.loop ? ctx.collection.last() : null)
828
+ let value: string | null = null
829
+ if (ctx.highlightedValue) {
830
+ value = ctx.collection.prev(ctx.highlightedValue)
831
+ if (!value && ctx.loopFocus) value = ctx.collection.last()
832
+ } else {
833
+ value = ctx.collection.last()
834
+ }
596
835
  set.highlightedItem(ctx, value)
597
836
  },
598
837
  highlightFirstSelectedItem(ctx) {
599
- const [value] = ctx.collection.sort(ctx.value)
600
- set.highlightedItem(ctx, value)
838
+ raf(() => {
839
+ const [value] = ctx.collection.sort(ctx.value)
840
+ set.highlightedItem(ctx, value)
841
+ })
842
+ },
843
+ highlightFirstOrSelectedItem(ctx) {
844
+ raf(() => {
845
+ let value: string | null = null
846
+ if (ctx.hasSelectedItems) {
847
+ value = ctx.collection.sort(ctx.value)[0]
848
+ } else {
849
+ value = ctx.collection.first()
850
+ }
851
+ set.highlightedItem(ctx, value)
852
+ })
853
+ },
854
+ highlightLastOrSelectedItem(ctx) {
855
+ raf(() => {
856
+ let value: string | null = null
857
+ if (ctx.hasSelectedItems) {
858
+ value = ctx.collection.sort(ctx.value)[0]
859
+ } else {
860
+ value = ctx.collection.last()
861
+ }
862
+ set.highlightedItem(ctx, value)
863
+ })
601
864
  },
602
865
  autofillInputValue(ctx, evt) {
603
866
  const inputEl = dom.getInputEl(ctx)
604
- if (!ctx.autoComplete || !inputEl || !KEYDOWN_EVENT_REGEX.test(evt.type)) return
867
+ if (!ctx.autoComplete || !inputEl || !evt.keypress) return
605
868
  const valueText = ctx.collection.valueToString(ctx.highlightedValue)
606
869
  raf(() => {
607
870
  inputEl.value = valueText || ctx.inputValue
@@ -610,33 +873,76 @@ export function machine<T extends CollectionItem>(userContext: UserDefinedContex
610
873
  setCollection(ctx, evt) {
611
874
  ctx.collection = evt.value
612
875
  },
876
+ syncSelectedItems(ctx) {
877
+ const prevSelectedItems = ctx.selectedItems
878
+ ctx.selectedItems = ctx.value.map((v) => {
879
+ const foundItem = prevSelectedItems.find((item) => ctx.collection.itemToValue(item) === v)
880
+ if (foundItem) return foundItem
881
+ return ctx.collection.item(v)
882
+ })
883
+ },
884
+ toggleVisibility(ctx, evt, { send }) {
885
+ send({ type: ctx.open ? "CONTROLLED.OPEN" : "CONTROLLED.CLOSE", previousEvent: evt })
886
+ },
613
887
  },
614
888
  },
615
889
  )
616
890
  }
617
891
 
618
892
  const invoke = {
619
- selectionChange: (ctx: MachineContext) => {
893
+ valueChange: (ctx: MachineContext) => {
620
894
  ctx.onValueChange?.({
621
895
  value: Array.from(ctx.value),
622
896
  items: ctx.selectedItems,
623
897
  })
624
898
 
625
- // side effect: sync inputValue
626
- ctx.inputValue = match(ctx.selectionBehavior, {
627
- replace: ctx.valueAsString,
628
- clear: "",
629
- preserve: ctx.inputValue,
899
+ const prevSelectedItems = ctx.selectedItems
900
+
901
+ // side effect
902
+ ctx.selectedItems = ctx.value.map((v) => {
903
+ const foundItem = prevSelectedItems.find((item) => ctx.collection.itemToValue(item) === v)
904
+ if (foundItem) return foundItem
905
+ return ctx.collection.item(v)
630
906
  })
907
+
908
+ const valueAsString = ctx.collection.itemsToString(ctx.selectedItems)
909
+
910
+ ctx.valueAsString = valueAsString
911
+
912
+ let nextInputValue: string | undefined
913
+
914
+ if (ctx.getSelectionValue) {
915
+ //
916
+ nextInputValue = ctx.getSelectionValue({
917
+ inputValue: ctx.inputValue,
918
+ selectedItems: Array.from(ctx.selectedItems),
919
+ valueAsString,
920
+ })
921
+ //
922
+ } else {
923
+ //
924
+ nextInputValue = match(ctx.selectionBehavior, {
925
+ replace: ctx.valueAsString,
926
+ preserve: ctx.inputValue,
927
+ clear: "",
928
+ })
929
+ }
930
+
931
+ ctx.inputValue = nextInputValue
932
+
933
+ invoke.inputChange(ctx)
631
934
  },
632
935
  highlightChange: (ctx: MachineContext) => {
633
936
  ctx.onHighlightChange?.({
634
937
  highlightedValue: ctx.highlightedValue,
635
938
  highligtedItem: ctx.highlightedItem,
636
939
  })
940
+
941
+ // side effect
942
+ ctx.highlightedItem = ctx.collection.item(ctx.highlightedValue)
637
943
  },
638
944
  inputChange: (ctx: MachineContext) => {
639
- ctx.onInputValueChange?.({ value: ctx.inputValue })
945
+ ctx.onInputValueChange?.({ inputValue: ctx.inputValue })
640
946
  },
641
947
  }
642
948
 
@@ -648,16 +954,16 @@ const set = {
648
954
 
649
955
  if (value == null && force) {
650
956
  ctx.value = []
651
- invoke.selectionChange(ctx)
957
+ invoke.valueChange(ctx)
652
958
  return
653
959
  }
654
960
  ctx.value = ctx.multiple ? addOrRemove(ctx.value, value!) : [value!]
655
- invoke.selectionChange(ctx)
961
+ invoke.valueChange(ctx)
656
962
  },
657
963
  selectedItems: (ctx: MachineContext, value: string[]) => {
658
964
  if (isEqual(ctx.value, value)) return
659
965
  ctx.value = value
660
- invoke.selectionChange(ctx)
966
+ invoke.valueChange(ctx)
661
967
  },
662
968
  highlightedItem: (ctx: MachineContext, value: string | null | undefined, force = false) => {
663
969
  if (isEqual(ctx.highlightedValue, value)) return