@zag-js/combobox 0.49.0 → 0.50.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/dist/index.d.mts +24 -15
- package/dist/index.d.ts +24 -15
- package/dist/index.js +229 -214
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +240 -225
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -11
- package/src/combobox.connect.ts +81 -78
- package/src/combobox.dom.ts +13 -6
- package/src/combobox.machine.ts +125 -137
- package/src/combobox.props.ts +2 -2
- package/src/combobox.types.ts +23 -13
- package/src/index.ts +1 -0
package/src/combobox.connect.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
isLeftClick,
|
|
7
7
|
type EventKeyMap,
|
|
8
8
|
} from "@zag-js/dom-event"
|
|
9
|
-
import { ariaAttr, dataAttr, isDownloadingEvent, isOpeningInNewTab
|
|
9
|
+
import { ariaAttr, dataAttr, isDownloadingEvent, isOpeningInNewTab } from "@zag-js/dom-query"
|
|
10
10
|
import { getPlacementStyles } from "@zag-js/popper"
|
|
11
11
|
import type { NormalizeProps, PropTypes } from "@zag-js/types"
|
|
12
12
|
import { parts } from "./combobox.anatomy"
|
|
@@ -28,7 +28,8 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
28
28
|
|
|
29
29
|
const open = state.hasTag("open")
|
|
30
30
|
const focused = state.hasTag("focused")
|
|
31
|
-
const
|
|
31
|
+
const composite = state.context.composite
|
|
32
|
+
const highlightedValue = state.context.highlightedValue
|
|
32
33
|
|
|
33
34
|
const popperStyles = getPlacementStyles({
|
|
34
35
|
...state.context.positioning,
|
|
@@ -42,7 +43,7 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
42
43
|
return {
|
|
43
44
|
value,
|
|
44
45
|
disabled: Boolean(disabled || disabled),
|
|
45
|
-
highlighted:
|
|
46
|
+
highlighted: highlightedValue === value,
|
|
46
47
|
selected: state.context.value.includes(value),
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -51,8 +52,7 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
51
52
|
focused,
|
|
52
53
|
open,
|
|
53
54
|
inputValue: state.context.inputValue,
|
|
54
|
-
|
|
55
|
-
highlightedValue: state.context.highlightedValue,
|
|
55
|
+
highlightedValue,
|
|
56
56
|
highlightedItem: state.context.highlightedItem,
|
|
57
57
|
value: state.context.value,
|
|
58
58
|
valueAsString: state.context.valueAsString,
|
|
@@ -65,7 +65,7 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
65
65
|
setCollection(collection) {
|
|
66
66
|
send({ type: "COLLECTION.SET", value: collection })
|
|
67
67
|
},
|
|
68
|
-
|
|
68
|
+
setHighlightValue(value) {
|
|
69
69
|
send({ type: "HIGHLIGHTED_VALUE.SET", value })
|
|
70
70
|
},
|
|
71
71
|
selectValue(value) {
|
|
@@ -87,9 +87,9 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
87
87
|
focus() {
|
|
88
88
|
dom.getInputEl(state.context)?.focus()
|
|
89
89
|
},
|
|
90
|
-
setOpen(
|
|
91
|
-
if (
|
|
92
|
-
send(
|
|
90
|
+
setOpen(nextOpen) {
|
|
91
|
+
if (nextOpen === open) return
|
|
92
|
+
send(nextOpen ? "OPEN" : "CLOSE")
|
|
93
93
|
},
|
|
94
94
|
rootProps: normalize.element({
|
|
95
95
|
...parts.root.attrs,
|
|
@@ -109,7 +109,7 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
109
109
|
"data-invalid": dataAttr(invalid),
|
|
110
110
|
"data-focus": dataAttr(focused),
|
|
111
111
|
onClick(event) {
|
|
112
|
-
if (
|
|
112
|
+
if (composite) return
|
|
113
113
|
event.preventDefault()
|
|
114
114
|
dom.getTriggerEl(state.context)?.focus({ preventScroll: true })
|
|
115
115
|
},
|
|
@@ -152,13 +152,12 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
152
152
|
role: "combobox",
|
|
153
153
|
defaultValue: state.context.inputValue,
|
|
154
154
|
"aria-autocomplete": state.context.autoComplete ? "both" : "list",
|
|
155
|
-
"aria-controls":
|
|
155
|
+
"aria-controls": dom.getContentId(state.context),
|
|
156
156
|
"aria-expanded": open,
|
|
157
157
|
"data-state": open ? "open" : "closed",
|
|
158
|
-
"aria-activedescendant": state.context
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
onClick() {
|
|
158
|
+
"aria-activedescendant": highlightedValue ? dom.getItemId(state.context, highlightedValue) : undefined,
|
|
159
|
+
onClick(event) {
|
|
160
|
+
if (event.defaultPrevented) return
|
|
162
161
|
if (!state.context.openOnClick) return
|
|
163
162
|
if (!interactive) return
|
|
164
163
|
send("INPUT.CLICK")
|
|
@@ -231,86 +230,85 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
231
230
|
},
|
|
232
231
|
}),
|
|
233
232
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
233
|
+
getTriggerProps(props = {}) {
|
|
234
|
+
return normalize.button({
|
|
235
|
+
...parts.trigger.attrs,
|
|
236
|
+
dir: state.context.dir,
|
|
237
|
+
id: dom.getTriggerId(state.context),
|
|
238
|
+
"aria-haspopup": composite ? "listbox" : "dialog",
|
|
239
|
+
type: "button",
|
|
240
|
+
tabIndex: props.focusable ? undefined : -1,
|
|
241
|
+
"aria-label": translations.triggerLabel,
|
|
242
|
+
"aria-expanded": open,
|
|
243
|
+
"data-state": open ? "open" : "closed",
|
|
244
|
+
"aria-controls": open ? dom.getContentId(state.context) : undefined,
|
|
245
|
+
disabled,
|
|
246
|
+
"data-focusable": dataAttr(props.focusable),
|
|
247
|
+
"data-readonly": dataAttr(readOnly),
|
|
248
|
+
"data-disabled": dataAttr(disabled),
|
|
249
|
+
onFocus() {
|
|
250
|
+
if (!props.focusable) return
|
|
251
|
+
send({ type: "INPUT.FOCUS", src: "trigger" })
|
|
252
|
+
},
|
|
253
|
+
onClick(event) {
|
|
254
|
+
if (event.defaultPrevented) return
|
|
255
|
+
const evt = getNativeEvent(event)
|
|
256
|
+
if (!interactive) return
|
|
257
|
+
if (!isLeftClick(evt)) return
|
|
258
|
+
send("TRIGGER.CLICK")
|
|
259
|
+
},
|
|
260
|
+
onPointerDown(event) {
|
|
261
|
+
if (!interactive) return
|
|
262
|
+
if (event.pointerType === "touch") return
|
|
263
|
+
event.preventDefault()
|
|
264
|
+
queueMicrotask(() => {
|
|
265
|
+
dom.getInputEl(state.context)?.focus({ preventScroll: true })
|
|
266
|
+
})
|
|
267
|
+
},
|
|
268
|
+
onKeyDown(event) {
|
|
269
|
+
if (event.defaultPrevented) return
|
|
270
|
+
if (composite) return
|
|
265
271
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
ArrowUp() {
|
|
275
|
-
send("INPUT.FOCUS")
|
|
276
|
-
send("INPUT.ARROW_UP")
|
|
277
|
-
raf(() => {
|
|
278
|
-
dom.getInputEl(state.context)?.focus({ preventScroll: true })
|
|
279
|
-
})
|
|
280
|
-
},
|
|
281
|
-
}
|
|
272
|
+
const keyMap: EventKeyMap = {
|
|
273
|
+
ArrowDown() {
|
|
274
|
+
send({ type: "INPUT.ARROW_DOWN", src: "trigger" })
|
|
275
|
+
},
|
|
276
|
+
ArrowUp() {
|
|
277
|
+
send({ type: "INPUT.ARROW_UP", src: "trigger" })
|
|
278
|
+
},
|
|
279
|
+
}
|
|
282
280
|
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
const key = getEventKey(event, state.context)
|
|
282
|
+
const exec = keyMap[key]
|
|
285
283
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
284
|
+
if (exec) {
|
|
285
|
+
exec(event)
|
|
286
|
+
event.preventDefault()
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
},
|
|
292
291
|
|
|
293
292
|
contentProps: normalize.element({
|
|
294
293
|
...parts.content.attrs,
|
|
295
294
|
dir: state.context.dir,
|
|
296
295
|
id: dom.getContentId(state.context),
|
|
297
|
-
role:
|
|
296
|
+
role: !composite ? "dialog" : "listbox",
|
|
298
297
|
tabIndex: -1,
|
|
299
298
|
hidden: !open,
|
|
300
299
|
"data-state": open ? "open" : "closed",
|
|
301
300
|
"aria-labelledby": dom.getLabelId(state.context),
|
|
302
|
-
"aria-multiselectable": state.context.multiple &&
|
|
301
|
+
"aria-multiselectable": state.context.multiple && composite ? true : undefined,
|
|
303
302
|
onPointerDown(event) {
|
|
304
303
|
// prevent options or elements within listbox from taking focus
|
|
305
304
|
event.preventDefault()
|
|
306
305
|
},
|
|
307
306
|
}),
|
|
308
307
|
|
|
309
|
-
// only used when triggerOnly: true
|
|
310
308
|
listProps: normalize.element({
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"aria-multiselectable":
|
|
309
|
+
role: !composite ? "listbox" : undefined,
|
|
310
|
+
"aria-labelledby": dom.getLabelId(state.context),
|
|
311
|
+
"aria-multiselectable": state.context.multiple && !composite ? true : undefined,
|
|
314
312
|
}),
|
|
315
313
|
|
|
316
314
|
clearTriggerProps: normalize.button({
|
|
@@ -323,7 +321,11 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
323
321
|
"aria-label": translations.clearTriggerLabel,
|
|
324
322
|
"aria-controls": dom.getInputId(state.context),
|
|
325
323
|
hidden: !state.context.value.length,
|
|
326
|
-
|
|
324
|
+
onPointerDown(event) {
|
|
325
|
+
event.preventDefault()
|
|
326
|
+
},
|
|
327
|
+
onClick(event) {
|
|
328
|
+
if (event.defaultPrevented) return
|
|
327
329
|
if (!interactive) return
|
|
328
330
|
send({ type: "VALUE.CLEAR", src: "clear-trigger" })
|
|
329
331
|
},
|
|
@@ -349,12 +351,13 @@ export function connect<T extends PropTypes, V extends CollectionItem>(
|
|
|
349
351
|
"data-value": itemState.value,
|
|
350
352
|
onPointerMove() {
|
|
351
353
|
if (itemState.disabled) return
|
|
354
|
+
if (itemState.highlighted) return
|
|
352
355
|
send({ type: "ITEM.POINTER_MOVE", value })
|
|
353
356
|
},
|
|
354
357
|
onPointerLeave() {
|
|
355
358
|
if (props.persistFocus) return
|
|
356
359
|
if (itemState.disabled) return
|
|
357
|
-
const mouseMoved = state.previousEvent.type
|
|
360
|
+
const mouseMoved = state.previousEvent.type.includes("POINTER")
|
|
358
361
|
if (!mouseMoved) return
|
|
359
362
|
send({ type: "ITEM.POINTER_LEAVE", value })
|
|
360
363
|
},
|
package/src/combobox.dom.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createScope } from "@zag-js/dom-query"
|
|
1
|
+
import { createScope, query } from "@zag-js/dom-query"
|
|
2
2
|
import type { MachineContext as Ctx } from "./combobox.types"
|
|
3
3
|
|
|
4
4
|
export const dom = createScope({
|
|
@@ -7,7 +7,6 @@ export const dom = createScope({
|
|
|
7
7
|
getControlId: (ctx: Ctx) => ctx.ids?.control ?? `combobox:${ctx.id}:control`,
|
|
8
8
|
getInputId: (ctx: Ctx) => ctx.ids?.input ?? `combobox:${ctx.id}:input`,
|
|
9
9
|
getContentId: (ctx: Ctx) => ctx.ids?.content ?? `combobox:${ctx.id}:content`,
|
|
10
|
-
getListId: (ctx: Ctx) => `combobox:${ctx.id}:listbox`,
|
|
11
10
|
getPositionerId: (ctx: Ctx) => ctx.ids?.positioner ?? `combobox:${ctx.id}:popper`,
|
|
12
11
|
getTriggerId: (ctx: Ctx) => ctx.ids?.trigger ?? `combobox:${ctx.id}:toggle-btn`,
|
|
13
12
|
getClearTriggerId: (ctx: Ctx) => ctx.ids?.clearTrigger ?? `combobox:${ctx.id}:clear-btn`,
|
|
@@ -17,17 +16,25 @@ export const dom = createScope({
|
|
|
17
16
|
getItemId: (ctx: Ctx, id: string) => `combobox:${ctx.id}:option:${id}`,
|
|
18
17
|
|
|
19
18
|
getContentEl: (ctx: Ctx) => dom.getById(ctx, dom.getContentId(ctx)),
|
|
20
|
-
getListEl: (ctx: Ctx) => dom.getById(ctx, dom.getListId(ctx)),
|
|
21
19
|
getInputEl: (ctx: Ctx) => dom.getById<HTMLInputElement>(ctx, dom.getInputId(ctx)),
|
|
22
20
|
getPositionerEl: (ctx: Ctx) => dom.getById(ctx, dom.getPositionerId(ctx)),
|
|
23
21
|
getControlEl: (ctx: Ctx) => dom.getById(ctx, dom.getControlId(ctx)),
|
|
24
22
|
getTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getTriggerId(ctx)),
|
|
25
23
|
getClearTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getClearTriggerId(ctx)),
|
|
26
|
-
|
|
27
|
-
isInputFocused: (ctx: Ctx) => dom.getDoc(ctx).activeElement === dom.getInputEl(ctx),
|
|
28
24
|
getHighlightedItemEl: (ctx: Ctx) => {
|
|
29
25
|
const value = ctx.highlightedValue
|
|
30
26
|
if (value == null) return
|
|
31
|
-
return dom.getContentEl(ctx)
|
|
27
|
+
return query(dom.getContentEl(ctx), `[role=option][data-value="${CSS.escape(value)}"`)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
focusInputEl: (ctx: Ctx) => {
|
|
31
|
+
const inputEl = dom.getInputEl(ctx)
|
|
32
|
+
if (dom.getActiveElement(ctx) === inputEl) return
|
|
33
|
+
inputEl?.focus({ preventScroll: true })
|
|
34
|
+
},
|
|
35
|
+
focusTriggerEl: (ctx: Ctx) => {
|
|
36
|
+
const triggerEl = dom.getTriggerEl(ctx)
|
|
37
|
+
if (dom.getActiveElement(ctx) === triggerEl) return
|
|
38
|
+
triggerEl?.focus({ preventScroll: true })
|
|
32
39
|
},
|
|
33
40
|
})
|