const items = ['React', 'Solid', 'Vue']
const Basic = () => {
const contentRef = useRef(null)
const api = useVirtualizer({
count: items.length,
getScrollElement: () => contentRef.current,
estimateSize: () => 35,
})
const virtualItems = api.getVirtualItems()
return (
<Select.Root
scrollFn={(index) => api.scrollToIndex(index, { align: 'center' })}
items={items}
>
<Select.Label>Framework</Select.Label>
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select a Framework" />
<Select.Indicator>
<ChevronDownIcon />
</Select.Indicator>
</Select.Trigger>
<Select.ClearTrigger>Clear</Select.ClearTrigger>
</Select.Control>
<Select.Positioner>
<Select.Content ref={contentRef}>
<Virtualizer.Root value={api}>
<Select.ItemGroup id="framework">
<Select.ItemGroupLabel htmlFor="framework">
Frameworks
</Select.ItemGroupLabel>
{virtualItems.map((virtualItem) => {
const item = items[virtualItem.index]
return (
<Virtualizer.Item key={item} item={virtualItem} asChild>
<Select.Item item={item}>
<Select.ItemText>{item}</Select.ItemText>
<Select.ItemIndicator>✓</Select.ItemIndicator>
</Select.Item>
</Virtualizer.Item>
)
})}
</Select.ItemGroup>
</Virtualizer.Root>
</Select.Content>
</Select.Positioner>
</Select.Root>
)
}
Last active
March 25, 2024 13:30
-
-
Save segunadebayo/17f4fac8c76aca59bd455d70678f03d3 to your computer and use it in GitHub Desktop.
zag: virtualized select
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { getEventKey, getNativeEvent, type EventKeyMap } from "@zag-js/dom-event" | |
import { ariaAttr, dataAttr, getByTypeahead, isEditableElement, isSelfEvent } from "@zag-js/dom-query" | |
import { getPlacementStyles } from "@zag-js/popper" | |
import type { NormalizeProps, PropTypes } from "@zag-js/types" | |
import { visuallyHiddenStyle } from "@zag-js/visually-hidden" | |
import { parts } from "./select.anatomy" | |
import { dom } from "./select.dom" | |
import type { CollectionItem, ItemProps, MachineApi, Send, State } from "./select.types" | |
export function connect<T extends PropTypes, V extends CollectionItem = CollectionItem>( | |
state: State, | |
send: Send, | |
normalize: NormalizeProps<T>, | |
): MachineApi<T, V> { | |
const isDisabled = state.context.isDisabled | |
const isInvalid = state.context.invalid | |
const isReadOnly = state.context.readOnly | |
const isInteractive = state.context.isInteractive | |
const isOpen = state.hasTag("open") | |
const isFocused = state.matches("focused") | |
const highlightedItem = state.context.highlightedItem | |
const selectedItems = state.context.selectedItems | |
const isTypingAhead = state.context.isTypingAhead | |
function getItemState(props: ItemProps) { | |
const { item } = props | |
const disabled = state.context.collection.isItemDisabled(item) | |
const value = state.context.collection.itemToValue(item) | |
return { | |
value, | |
isDisabled: Boolean(disabled || isDisabled), | |
isHighlighted: state.context.highlightedValue === value, | |
isSelected: state.context.value.includes(value), | |
} | |
} | |
const popperStyles = getPlacementStyles({ | |
...state.context.positioning, | |
placement: state.context.currentPlacement, | |
}) | |
const virtual = state.context.virtualizer | |
return { | |
virtualItems: virtual?.getVirtualItems() ?? [], | |
isOpen, | |
isFocused, | |
isValueEmpty: state.context.value.length === 0, | |
highlightedItem, | |
highlightedValue: state.context.highlightedValue, | |
selectedItems, | |
hasSelectedItems: state.context.hasSelectedItems, | |
value: state.context.value, | |
valueAsString: state.context.valueAsString, | |
collection: state.context.collection, | |
setCollection(collection) { | |
send({ type: "COLLECTION.SET", value: collection }) | |
}, | |
reposition(options = {}) { | |
send({ type: "POSITIONING.SET", options }) | |
}, | |
focus() { | |
dom.getTriggerEl(state.context)?.focus({ preventScroll: true }) | |
}, | |
open() { | |
send("OPEN") | |
}, | |
close() { | |
send("CLOSE") | |
}, | |
selectValue(value) { | |
send({ type: "ITEM.SELECT", value }) | |
}, | |
setValue(value) { | |
send({ type: "VALUE.SET", value }) | |
}, | |
highlightValue(value) { | |
send({ type: "HIGHLIGHTED_VALUE.SET", value }) | |
}, | |
clearValue(value) { | |
if (value) { | |
send({ type: "ITEM.CLEAR", value }) | |
} else { | |
send({ type: "VALUE.CLEAR" }) | |
} | |
}, | |
getItemState, | |
rootProps: normalize.element({ | |
...parts.root.attrs, | |
dir: state.context.dir, | |
id: dom.getRootId(state.context), | |
"data-invalid": dataAttr(isInvalid), | |
"data-readonly": dataAttr(isReadOnly), | |
}), | |
labelProps: normalize.label({ | |
dir: state.context.dir, | |
id: dom.getLabelId(state.context), | |
...parts.label.attrs, | |
"data-disabled": dataAttr(isDisabled), | |
"data-invalid": dataAttr(isInvalid), | |
"data-readonly": dataAttr(isReadOnly), | |
htmlFor: dom.getHiddenSelectId(state.context), | |
onClick() { | |
if (isDisabled) return | |
dom.getTriggerEl(state.context)?.focus({ preventScroll: true }) | |
}, | |
}), | |
controlProps: normalize.element({ | |
...parts.control.attrs, | |
dir: state.context.dir, | |
id: dom.getControlId(state.context), | |
"data-state": isOpen ? "open" : "closed", | |
"data-focus": dataAttr(isFocused), | |
"data-disabled": dataAttr(isDisabled), | |
"data-invalid": dataAttr(isInvalid), | |
}), | |
triggerProps: normalize.button({ | |
id: dom.getTriggerId(state.context), | |
disabled: isDisabled, | |
dir: state.context.dir, | |
type: "button", | |
"aria-controls": dom.getContentId(state.context), | |
"aria-expanded": isOpen, | |
"data-state": isOpen ? "open" : "closed", | |
"aria-haspopup": "listbox", | |
"aria-labelledby": dom.getLabelId(state.context), | |
...parts.trigger.attrs, | |
"data-disabled": dataAttr(isDisabled), | |
"data-invalid": dataAttr(isInvalid), | |
"aria-invalid": isInvalid, | |
"data-readonly": dataAttr(isReadOnly), | |
"data-placement": state.context.currentPlacement, | |
"data-placeholder-shown": dataAttr(!state.context.hasSelectedItems), | |
onPointerDown(event) { | |
if (event.button || event.ctrlKey || !isInteractive) return | |
event.currentTarget.dataset.pointerType = event.pointerType | |
if (isDisabled || event.pointerType === "touch") return | |
send({ type: "TRIGGER.CLICK" }) | |
}, | |
onClick(event) { | |
if (!isInteractive || event.button) return | |
if (event.currentTarget.dataset.pointerType === "touch") { | |
send({ type: "TRIGGER.CLICK" }) | |
} | |
}, | |
onFocus() { | |
send("TRIGGER.FOCUS") | |
}, | |
onBlur() { | |
send("TRIGGER.BLUR") | |
}, | |
onKeyDown(event) { | |
if (!isInteractive) return | |
const keyMap: EventKeyMap = { | |
ArrowUp() { | |
send({ type: "TRIGGER.ARROW_UP" }) | |
}, | |
ArrowDown(event) { | |
send({ type: event.altKey ? "OPEN" : "TRIGGER.ARROW_DOWN" }) | |
}, | |
ArrowLeft() { | |
send({ type: "TRIGGER.ARROW_LEFT" }) | |
}, | |
ArrowRight() { | |
send({ type: "TRIGGER.ARROW_RIGHT" }) | |
}, | |
Home() { | |
send({ type: "TRIGGER.HOME" }) | |
}, | |
End() { | |
send({ type: "TRIGGER.END" }) | |
}, | |
Enter() { | |
send({ type: "TRIGGER.ENTER" }) | |
}, | |
Space(event) { | |
if (isTypingAhead) { | |
send({ type: "TRIGGER.TYPEAHEAD", key: event.key }) | |
} else { | |
send({ type: "TRIGGER.ENTER" }) | |
} | |
}, | |
} | |
const exec = keyMap[getEventKey(event, state.context)] | |
if (exec) { | |
exec(event) | |
event.preventDefault() | |
return | |
} | |
if (getByTypeahead.isValidEvent(event)) { | |
send({ type: "TRIGGER.TYPEAHEAD", key: event.key }) | |
event.preventDefault() | |
} | |
}, | |
}), | |
indicatorProps: normalize.element({ | |
...parts.indicator.attrs, | |
dir: state.context.dir, | |
"aria-hidden": true, | |
"data-state": isOpen ? "open" : "closed", | |
"data-disabled": dataAttr(isDisabled), | |
"data-invalid": dataAttr(isInvalid), | |
"data-readonly": dataAttr(isReadOnly), | |
}), | |
getItemProps(props) { | |
const itemState = getItemState(props) | |
return normalize.element({ | |
id: dom.getItemId(state.context, itemState.value), | |
role: "option", | |
...parts.item.attrs, | |
dir: state.context.dir, | |
"data-value": itemState.value, | |
"aria-selected": itemState.isSelected, | |
"data-state": itemState.isSelected ? "checked" : "unchecked", | |
"data-highlighted": dataAttr(itemState.isHighlighted), | |
"data-disabled": dataAttr(itemState.isDisabled), | |
"aria-disabled": ariaAttr(itemState.isDisabled), | |
onPointerMove(event) { | |
if (itemState.isDisabled || event.pointerType !== "mouse") return | |
if (itemState.value === state.context.highlightedValue) return | |
send({ type: "ITEM.POINTER_MOVE", value: itemState.value }) | |
}, | |
onPointerUp() { | |
if (itemState.isDisabled) return | |
send({ type: "ITEM.CLICK", src: "pointerup", value: itemState.value }) | |
}, | |
onPointerLeave(event) { | |
const isKeyboardNavigationEvent = ["CONTENT.ARROW_UP", "CONTENT.ARROW_DOWN"].includes(state.event.type) | |
if (itemState.isDisabled || event.pointerType !== "mouse" || isKeyboardNavigationEvent) return | |
send({ type: "ITEM.POINTER_LEAVE" }) | |
}, | |
onTouchEnd(event) { | |
// prevent clicking elements behind content | |
event.preventDefault() | |
event.stopPropagation() | |
}, | |
"aria-setsize": virtual?.getTotalSize(), | |
"aria-posinset": props.virtual ? props.virtual.index + 1 : undefined, | |
style: { | |
...(virtual && { | |
position: "absolute", | |
top: 0, | |
left: 0, | |
width: "100%", | |
height: `${props.virtual!.size}px`, | |
transform: `translateY(${props.virtual!.start}px)`, | |
}), | |
}, | |
}) | |
}, | |
getItemTextProps(props) { | |
const itemState = getItemState(props) | |
return normalize.element({ | |
...parts.itemText.attrs, | |
"data-disabled": dataAttr(itemState.isDisabled), | |
"data-highlighted": dataAttr(itemState.isHighlighted), | |
}) | |
}, | |
getItemIndicatorProps(props) { | |
const itemState = getItemState(props) | |
return normalize.element({ | |
"aria-hidden": true, | |
...parts.itemIndicator.attrs, | |
"data-state": itemState.isSelected ? "checked" : "unchecked", | |
hidden: !itemState.isSelected, | |
}) | |
}, | |
getItemGroupLabelProps(props) { | |
const { htmlFor } = props | |
return normalize.element({ | |
...parts.itemGroupLabel.attrs, | |
id: dom.getItemGroupLabelId(state.context, htmlFor), | |
role: "group", | |
dir: state.context.dir, | |
}) | |
}, | |
getItemGroupProps(props) { | |
const { id } = props | |
return normalize.element({ | |
...parts.itemGroup.attrs, | |
"data-disabled": dataAttr(isDisabled), | |
id: dom.getItemGroupId(state.context, id), | |
"aria-labelledby": dom.getItemGroupLabelId(state.context, id), | |
dir: state.context.dir, | |
}) | |
}, | |
clearTriggerProps: normalize.button({ | |
...parts.clearTrigger.attrs, | |
id: dom.getClearTriggerId(state.context), | |
type: "button", | |
"aria-label": "Clear value", | |
disabled: isDisabled, | |
hidden: !state.context.hasSelectedItems, | |
dir: state.context.dir, | |
onClick() { | |
send("VALUE.CLEAR") | |
}, | |
}), | |
hiddenSelectProps: normalize.select({ | |
name: state.context.name, | |
form: state.context.form, | |
disabled: !isInteractive, | |
multiple: state.context.multiple, | |
"aria-hidden": true, | |
id: dom.getHiddenSelectId(state.context), | |
// defaultValue: state.context.selectedOption?.value, | |
style: visuallyHiddenStyle, | |
tabIndex: -1, | |
// Some browser extensions will focus the hidden select. | |
// Let's forward the focus to the trigger. | |
onFocus() { | |
dom.getTriggerEl(state.context)?.focus({ preventScroll: true }) | |
}, | |
"aria-labelledby": dom.getLabelId(state.context), | |
}), | |
positionerProps: normalize.element({ | |
...parts.positioner.attrs, | |
dir: state.context.dir, | |
id: dom.getPositionerId(state.context), | |
style: popperStyles.floating, | |
}), | |
virtualizerProps: normalize.element({ | |
id: dom.getVirtualizerId(state.context), | |
style: { | |
height: virtual ? `${virtual.getTotalSize()}px` : undefined, | |
width: "100%", | |
position: "relative", | |
}, | |
}), | |
contentProps: normalize.element({ | |
hidden: !isOpen, | |
dir: state.context.dir, | |
id: dom.getContentId(state.context), | |
role: "listbox", | |
...parts.content.attrs, | |
"data-state": isOpen ? "open" : "closed", | |
"aria-activedescendant": state.context.highlightedValue | |
? dom.getItemId(state.context, state.context.highlightedValue) | |
: undefined, | |
"aria-multiselectable": state.context.multiple ? "true" : undefined, | |
"aria-labelledby": dom.getLabelId(state.context), | |
tabIndex: 0, | |
onKeyDown(event) { | |
const evt = getNativeEvent(event) | |
if (!isInteractive || !isSelfEvent(evt)) return | |
const keyMap: EventKeyMap = { | |
ArrowUp() { | |
send({ type: "CONTENT.ARROW_UP" }) | |
}, | |
ArrowDown() { | |
send({ type: "CONTENT.ARROW_DOWN" }) | |
}, | |
Home() { | |
send({ type: "CONTENT.HOME" }) | |
}, | |
End() { | |
send({ type: "CONTENT.END" }) | |
}, | |
Enter() { | |
send({ type: "ITEM.CLICK", src: "keydown.enter" }) | |
}, | |
Space(event) { | |
if (isTypingAhead) { | |
send({ type: "CONTENT.TYPEAHEAD", key: event.key }) | |
} else { | |
keyMap.Enter?.(event) | |
} | |
}, | |
} | |
const exec = keyMap[getEventKey(event)] | |
if (exec) { | |
exec(event) | |
event.preventDefault() | |
return | |
} | |
if (isEditableElement(event.target)) { | |
return | |
} | |
if (getByTypeahead.isValidEvent(event)) { | |
send({ type: "CONTENT.TYPEAHEAD", key: event.key }) | |
event.preventDefault() | |
} | |
}, | |
}), | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Virtualizer, | |
elementScroll, | |
observeElementOffset, | |
observeElementRect, | |
type VirtualizerOptions, | |
} from '@tanstack/virtual-core' | |
import { createMachine, guards, ref } from '@zag-js/core' | |
import { trackDismissableElement } from '@zag-js/dismissable' | |
import { getByTypeahead, raf, scrollIntoView } from '@zag-js/dom-query' | |
import { trackFormControl } from '@zag-js/form-utils' | |
import { observeAttributes } from '@zag-js/mutation-observer' | |
import { getPlacement } from '@zag-js/popper' | |
import { proxyTabFocus } from '@zag-js/tabbable' | |
import { addOrRemove, compact, isEqual } from '@zag-js/utils' | |
import { collection } from './select.collection' | |
import { dom } from './select.dom' | |
import type { | |
CollectionItem, | |
MachineContext, | |
MachineState, | |
UserDefinedContext, | |
} from './select.types' | |
const { and, not, or } = guards | |
function getVirtualOptions( | |
ctx: MachineContext | |
): VirtualizerOptions<HTMLElement, HTMLElement> { | |
return { | |
count: ctx.collection.count(), | |
observeElementRect: observeElementRect, | |
observeElementOffset: observeElementOffset, | |
scrollToFn: elementScroll, | |
getScrollElement: () => dom.getContentEl(ctx), | |
estimateSize: () => 35, | |
overscan: 10, | |
} | |
} | |
export function machine<T extends CollectionItem>( | |
userContext: UserDefinedContext<T> | |
) { | |
const ctx = compact(userContext) | |
return createMachine<MachineContext, MachineState>( | |
{ | |
id: 'select', | |
context: { | |
value: [], | |
highlightedValue: null, | |
selectOnBlur: false, | |
loop: false, | |
closeOnSelect: true, | |
disabled: false, | |
...ctx, | |
collection: ctx.collection ?? collection.empty(), | |
typeahead: getByTypeahead.defaultOptions, | |
fieldsetDisabled: false, | |
restoreFocus: true, | |
positioning: { | |
placement: 'bottom-start', | |
gutter: 8, | |
...ctx.positioning, | |
}, | |
force: -1, | |
// @ts-expect-error | |
virtualizer: ref<any>(new Virtualizer(getVirtualOptions(ctx))), | |
}, | |
computed: { | |
hasSelectedItems: (ctx) => ctx.value.length > 0, | |
isTypingAhead: (ctx) => ctx.typeahead.keysSoFar !== '', | |
isDisabled: (ctx) => !!ctx.disabled || ctx.fieldsetDisabled, | |
isInteractive: (ctx) => !(ctx.isDisabled || ctx.readOnly), | |
selectedItems: (ctx) => ctx.collection.items(ctx.value), | |
highlightedItem: (ctx) => ctx.collection.item(ctx.highlightedValue), | |
valueAsString: (ctx) => ctx.collection.itemsToString(ctx.selectedItems), | |
}, | |
initial: ctx.open ? 'open' : 'idle', | |
watch: { | |
open: ['toggleVisibility'], | |
value: ['syncSelectElement'], | |
}, | |
on: { | |
'HIGHLIGHTED_VALUE.SET': { | |
actions: ['setHighlightedItem'], | |
}, | |
'ITEM.SELECT': { | |
actions: ['selectItem'], | |
}, | |
'ITEM.CLEAR': { | |
actions: ['clearItem'], | |
}, | |
'VALUE.SET': { | |
actions: ['setSelectedItems'], | |
}, | |
'VALUE.CLEAR': { | |
actions: ['clearSelectedItems'], | |
}, | |
'COLLECTION.SET': { | |
actions: ['setCollection'], | |
}, | |
}, | |
activities: ['trackFormControlState', 'trackVirtualizer'], | |
states: { | |
idle: { | |
tags: ['closed'], | |
on: { | |
'CONTROLLED.OPEN': [ | |
{ | |
guard: 'isTriggerClickEvent', | |
target: 'open', | |
actions: ['highlightFirstSelectedItem'], | |
}, | |
{ | |
target: 'open', | |
}, | |
], | |
'TRIGGER.CLICK': [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnOpen'], | |
}, | |
{ | |
target: 'open', | |
actions: ['invokeOnOpen', 'highlightFirstSelectedItem'], | |
}, | |
], | |
'TRIGGER.FOCUS': { | |
target: 'focused', | |
}, | |
OPEN: [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnOpen'], | |
}, | |
{ | |
target: 'open', | |
actions: ['invokeOnOpen'], | |
}, | |
], | |
}, | |
}, | |
focused: { | |
tags: ['closed'], | |
entry: ['focusTriggerEl'], | |
on: { | |
'CONTROLLED.OPEN': [ | |
{ | |
guard: 'isTriggerClickEvent', | |
target: 'open', | |
actions: ['highlightFirstSelectedItem'], | |
}, | |
{ | |
guard: 'isTriggerArrowUpEvent', | |
target: 'open', | |
actions: ['highlightComputedLastItem'], | |
}, | |
{ | |
guard: or('isTriggerArrowDownEvent', 'isTriggerEnterEvent'), | |
target: 'open', | |
actions: ['highlightComputedFirstItem'], | |
}, | |
{ | |
target: 'open', | |
}, | |
], | |
OPEN: [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnOpen'], | |
}, | |
{ | |
target: 'open', | |
actions: ['invokeOnOpen'], | |
}, | |
], | |
'TRIGGER.BLUR': { | |
target: 'idle', | |
}, | |
'TRIGGER.CLICK': [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnOpen'], | |
}, | |
{ | |
target: 'open', | |
actions: ['invokeOnOpen', 'highlightFirstSelectedItem'], | |
}, | |
], | |
'TRIGGER.ENTER': [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnOpen'], | |
}, | |
{ | |
target: 'open', | |
actions: ['invokeOnOpen', 'highlightComputedFirstItem'], | |
}, | |
], | |
'TRIGGER.ARROW_UP': [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnOpen'], | |
}, | |
{ | |
target: 'open', | |
actions: ['invokeOnOpen', 'highlightComputedLastItem'], | |
}, | |
], | |
'TRIGGER.ARROW_DOWN': [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnOpen'], | |
}, | |
{ | |
target: 'open', | |
actions: ['invokeOnOpen', 'highlightComputedFirstItem'], | |
}, | |
], | |
'TRIGGER.ARROW_LEFT': [ | |
{ | |
guard: and(not('multiple'), 'hasSelectedItems'), | |
actions: ['selectPreviousItem'], | |
}, | |
{ | |
guard: not('multiple'), | |
actions: ['selectLastItem'], | |
}, | |
], | |
'TRIGGER.ARROW_RIGHT': [ | |
{ | |
guard: and(not('multiple'), 'hasSelectedItems'), | |
actions: ['selectNextItem'], | |
}, | |
{ | |
guard: not('multiple'), | |
actions: ['selectFirstItem'], | |
}, | |
], | |
'TRIGGER.HOME': { | |
guard: not('multiple'), | |
actions: ['selectFirstItem'], | |
}, | |
'TRIGGER.END': { | |
guard: not('multiple'), | |
actions: ['selectLastItem'], | |
}, | |
'TRIGGER.TYPEAHEAD': { | |
guard: not('multiple'), | |
actions: ['selectMatchingItem'], | |
}, | |
}, | |
}, | |
open: { | |
tags: ['open'], | |
entry: ['focusContentEl'], | |
exit: ['scrollContentToTop'], | |
activities: [ | |
'trackDismissableElement', | |
'computePlacement', | |
'scrollToHighlightedItem', | |
'proxyTabFocus', | |
], | |
on: { | |
'CONTROLLED.CLOSE': [ | |
{ | |
guard: 'shouldRestoreFocus', | |
target: 'focused', | |
actions: ['clearHighlightedItem'], | |
}, | |
{ | |
target: 'idle', | |
actions: ['clearHighlightedItem'], | |
}, | |
], | |
CLOSE: [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnClose'], | |
}, | |
{ | |
target: 'focused', | |
actions: ['invokeOnClose', 'clearHighlightedItem'], | |
}, | |
], | |
'TRIGGER.CLICK': [ | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnClose'], | |
}, | |
{ | |
target: 'focused', | |
actions: ['invokeOnClose', 'clearHighlightedItem'], | |
}, | |
], | |
'ITEM.CLICK': [ | |
{ | |
guard: and('closeOnSelect', 'isOpenControlled'), | |
actions: ['selectHighlightedItem', 'invokeOnClose'], | |
}, | |
{ | |
guard: 'closeOnSelect', | |
target: 'focused', | |
actions: [ | |
'selectHighlightedItem', | |
'invokeOnClose', | |
'clearHighlightedItem', | |
], | |
}, | |
{ | |
actions: ['selectHighlightedItem'], | |
}, | |
], | |
'CONTENT.INTERACT_OUTSIDE': [ | |
// == group 1 == | |
{ | |
guard: and( | |
'selectOnBlur', | |
'hasHighlightedItem', | |
'isOpenControlled' | |
), | |
actions: ['selectHighlightedItem', 'invokeOnClose'], | |
}, | |
{ | |
guard: and('selectOnBlur', 'hasHighlightedItem'), | |
target: 'idle', | |
actions: [ | |
'selectHighlightedItem', | |
'invokeOnClose', | |
'clearHighlightedItem', | |
], | |
}, | |
// == group 2 == | |
{ | |
guard: and('shouldRestoreFocus', 'isOpenControlled'), | |
actions: ['invokeOnClose'], | |
}, | |
{ | |
guard: 'shouldRestoreFocus', | |
target: 'focused', | |
actions: ['invokeOnClose', 'clearHighlightedItem'], | |
}, | |
// == group 3 == | |
{ | |
guard: 'isOpenControlled', | |
actions: ['invokeOnClose'], | |
}, | |
{ | |
target: 'idle', | |
actions: ['invokeOnClose', 'clearHighlightedItem'], | |
}, | |
], | |
'CONTENT.HOME': { | |
actions: ['highlightFirstItem'], | |
}, | |
'CONTENT.END': { | |
actions: ['highlightLastItem'], | |
}, | |
'CONTENT.ARROW_DOWN': [ | |
{ | |
guard: and( | |
'hasHighlightedItem', | |
'loop', | |
'isLastItemHighlighted' | |
), | |
actions: ['highlightFirstItem'], | |
}, | |
{ | |
guard: 'hasHighlightedItem', | |
actions: ['highlightNextItem'], | |
}, | |
{ | |
actions: ['highlightFirstItem'], | |
}, | |
], | |
'CONTENT.ARROW_UP': [ | |
{ | |
guard: and( | |
'hasHighlightedItem', | |
'loop', | |
'isFirstItemHighlighted' | |
), | |
actions: ['highlightLastItem'], | |
}, | |
{ | |
guard: 'hasHighlightedItem', | |
actions: ['highlightPreviousItem'], | |
}, | |
{ | |
actions: ['highlightLastItem'], | |
}, | |
], | |
'CONTENT.TYPEAHEAD': { | |
actions: ['highlightMatchingItem'], | |
}, | |
'ITEM.POINTER_MOVE': { | |
actions: ['highlightItem'], | |
}, | |
'ITEM.POINTER_LEAVE': { | |
actions: ['clearHighlightedItem'], | |
}, | |
'POSITIONING.SET': { | |
actions: ['reposition'], | |
}, | |
}, | |
}, | |
}, | |
}, | |
{ | |
guards: { | |
loop: (ctx) => !!ctx.loop, | |
multiple: (ctx) => !!ctx.multiple, | |
hasSelectedItems: (ctx) => !!ctx.hasSelectedItems, | |
hasHighlightedItem: (ctx) => ctx.highlightedValue != null, | |
isFirstItemHighlighted: (ctx) => | |
ctx.highlightedValue === ctx.collection.first(), | |
isLastItemHighlighted: (ctx) => | |
ctx.highlightedValue === ctx.collection.last(), | |
selectOnBlur: (ctx) => !!ctx.selectOnBlur, | |
closeOnSelect: (ctx, evt) => { | |
if (ctx.multiple) return false | |
return !!(evt.closeOnSelect ?? ctx.closeOnSelect) | |
}, | |
shouldRestoreFocus: (ctx) => !!ctx.restoreFocus, | |
// guard assertions (for controlled mode) | |
isOpenControlled: (ctx) => !!ctx['open.controlled'], | |
isTriggerClickEvent: (_ctx, evt) => | |
evt.previousEvent?.type === 'TRIGGER.CLICK', | |
isTriggerEnterEvent: (_ctx, evt) => | |
evt.previousEvent?.type === 'TRIGGER.ENTER', | |
isTriggerArrowUpEvent: (_ctx, evt) => | |
evt.previousEvent?.type === 'TRIGGER.ARROW_UP', | |
isTriggerArrowDownEvent: (_ctx, evt) => | |
evt.previousEvent?.type === 'TRIGGER.ARROW_DOWN', | |
}, | |
activities: { | |
trackVirtualizer(ctx) { | |
if (!ctx.virtualizer) return | |
ctx.virtualizer.setOptions({ | |
...getVirtualOptions(ctx), | |
onChange: () => ctx.force++, | |
}) | |
ctx.virtualizer._willUpdate() | |
return ctx.virtualizer._didMount() | |
}, | |
proxyTabFocus(ctx) { | |
const contentEl = () => dom.getContentEl(ctx) | |
return proxyTabFocus(contentEl, { | |
defer: true, | |
triggerElement: dom.getTriggerEl(ctx), | |
onFocus(el) { | |
raf(() => el.focus({ preventScroll: true })) | |
}, | |
}) | |
}, | |
trackFormControlState(ctx, _evt, { initialContext }) { | |
return trackFormControl(dom.getHiddenSelectEl(ctx), { | |
onFieldsetDisabledChange(disabled) { | |
ctx.fieldsetDisabled = disabled | |
}, | |
onFormReset() { | |
set.selectedItems(ctx, initialContext.value) | |
}, | |
}) | |
}, | |
trackDismissableElement(ctx, _evt, { send }) { | |
const contentEl = () => dom.getContentEl(ctx) | |
return trackDismissableElement(contentEl, { | |
defer: true, | |
exclude: [dom.getTriggerEl(ctx), dom.getClearTriggerEl(ctx)], | |
onFocusOutside: ctx.onFocusOutside, | |
onPointerDownOutside: ctx.onPointerDownOutside, | |
onInteractOutside(event) { | |
ctx.onInteractOutside?.(event) | |
ctx.restoreFocus = !event.detail.focusable | |
}, | |
onDismiss() { | |
send({ type: 'CONTENT.INTERACT_OUTSIDE' }) | |
}, | |
}) | |
}, | |
computePlacement(ctx) { | |
ctx.currentPlacement = ctx.positioning.placement | |
const triggerEl = () => dom.getTriggerEl(ctx) | |
const positionerEl = () => dom.getPositionerEl(ctx) | |
return getPlacement(triggerEl, positionerEl, { | |
defer: true, | |
...ctx.positioning, | |
onComplete(data) { | |
ctx.currentPlacement = data.placement | |
}, | |
}) | |
}, | |
scrollToHighlightedItem(ctx, _evt, { getState }) { | |
const exec = () => { | |
const state = getState() | |
// don't scroll into view if we're using the pointer | |
if (state.event.type.startsWith('ITEM.POINTER')) return | |
if (ctx.virtualizer) { | |
const highlightedIndex = ctx.collection.indexOf( | |
ctx.highlightedValue | |
) | |
ctx.virtualizer!.scrollToIndex(highlightedIndex, { | |
behavior: 'auto', | |
align: 'center', | |
}) | |
return | |
} | |
scrollIntoView(dom.getHighlightedOptionEl(ctx), { | |
rootEl: dom.getContentEl(ctx), | |
block: 'nearest', | |
inline: 'center', | |
}) | |
} | |
let called = 1 | |
if (ctx.virtualizer) { | |
const id = setInterval(() => { | |
if (called === 2) { | |
clearInterval(id) | |
return | |
} | |
exec() | |
called++ | |
}, 1000 / 60) | |
} else { | |
raf(() => exec()) | |
} | |
return observeAttributes( | |
dom.getContentEl(ctx), | |
['aria-activedescendant'], | |
exec | |
) | |
}, | |
}, | |
actions: { | |
reposition(ctx, evt) { | |
const positionerEl = () => dom.getPositionerEl(ctx) | |
getPlacement(dom.getTriggerEl(ctx), positionerEl, { | |
...ctx.positioning, | |
...evt.options, | |
defer: true, | |
listeners: false, | |
onComplete(data) { | |
ctx.currentPlacement = data.placement | |
}, | |
}) | |
}, | |
toggleVisibility(ctx, evt, { send }) { | |
send({ | |
type: ctx.open ? 'CONTROLLED.OPEN' : 'CONTROLLED.CLOSE', | |
previousEvent: evt, | |
}) | |
}, | |
highlightPreviousItem(ctx) { | |
if (ctx.highlightedValue == null) return | |
const value = ctx.collection.prev(ctx.highlightedValue) | |
set.highlightedItem(ctx, value) | |
}, | |
highlightNextItem(ctx) { | |
if (ctx.highlightedValue == null) return | |
const value = ctx.collection.next(ctx.highlightedValue) | |
set.highlightedItem(ctx, value) | |
}, | |
highlightFirstItem(ctx) { | |
const value = ctx.collection.first() | |
set.highlightedItem(ctx, value) | |
}, | |
highlightLastItem(ctx) { | |
const value = ctx.collection.last() | |
set.highlightedItem(ctx, value) | |
}, | |
focusContentEl(ctx) { | |
raf(() => { | |
dom.getContentEl(ctx)?.focus({ preventScroll: true }) | |
}) | |
}, | |
focusTriggerEl(ctx) { | |
raf(() => { | |
dom.getTriggerEl(ctx)?.focus({ preventScroll: true }) | |
}) | |
}, | |
selectHighlightedItem(ctx, evt) { | |
const value = evt.value ?? ctx.highlightedValue | |
if (value == null) return | |
set.selectedItem(ctx, value) | |
}, | |
highlightComputedFirstItem(ctx) { | |
const value = ctx.hasSelectedItems | |
? ctx.collection.sort(ctx.value)[0] | |
: ctx.collection.first() | |
set.highlightedItem(ctx, value) | |
}, | |
highlightComputedLastItem(ctx) { | |
const value = ctx.hasSelectedItems | |
? ctx.collection.sort(ctx.value)[0] | |
: ctx.collection.last() | |
set.highlightedItem(ctx, value) | |
}, | |
highlightFirstSelectedItem(ctx) { | |
if (!ctx.hasSelectedItems) return | |
const [value] = ctx.collection.sort(ctx.value) | |
set.highlightedItem(ctx, value) | |
}, | |
highlightItem(ctx, evt) { | |
set.highlightedItem(ctx, evt.value) | |
}, | |
highlightMatchingItem(ctx, evt) { | |
const value = ctx.collection.search(evt.key, { | |
state: ctx.typeahead, | |
currentValue: ctx.highlightedValue, | |
}) | |
if (value == null) return | |
set.highlightedItem(ctx, value) | |
}, | |
setHighlightedItem(ctx, evt) { | |
set.highlightedItem(ctx, evt.value) | |
}, | |
clearHighlightedItem(ctx) { | |
set.highlightedItem(ctx, null, true) | |
}, | |
selectItem(ctx, evt) { | |
set.selectedItem(ctx, evt.value) | |
}, | |
clearItem(ctx, evt) { | |
const value = ctx.value.filter((v) => v !== evt.value) | |
set.selectedItems(ctx, value) | |
}, | |
setSelectedItems(ctx, evt) { | |
set.selectedItems(ctx, evt.value) | |
}, | |
clearSelectedItems(ctx) { | |
set.selectedItems(ctx, []) | |
}, | |
selectPreviousItem(ctx) { | |
const value = ctx.collection.prev(ctx.value[0]) | |
set.selectedItem(ctx, value) | |
}, | |
selectNextItem(ctx) { | |
const value = ctx.collection.next(ctx.value[0]) | |
set.selectedItem(ctx, value) | |
}, | |
selectFirstItem(ctx) { | |
const value = ctx.collection.first() | |
set.selectedItem(ctx, value) | |
}, | |
selectLastItem(ctx) { | |
const value = ctx.collection.last() | |
set.selectedItem(ctx, value) | |
}, | |
selectMatchingItem(ctx, evt) { | |
const value = ctx.collection.search(evt.key, { | |
state: ctx.typeahead, | |
currentValue: ctx.value[0], | |
}) | |
if (value == null) return | |
set.selectedItem(ctx, value) | |
}, | |
scrollContentToTop(ctx) { | |
if (ctx.virtualizer) { | |
ctx.virtualizer.scrollToIndex(0) | |
} else { | |
dom.getContentEl(ctx)?.scrollTo(0, 0) | |
} | |
}, | |
invokeOnOpen(ctx) { | |
ctx.onOpenChange?.({ open: true }) | |
}, | |
invokeOnClose(ctx) { | |
ctx.onOpenChange?.({ open: false }) | |
}, | |
syncSelectElement(ctx) { | |
const selectEl = dom.getHiddenSelectEl(ctx) | |
if (!selectEl) return | |
for (const option of selectEl.options) { | |
option.selected = ctx.value.includes(option.value) | |
} | |
}, | |
setCollection(ctx, evt) { | |
ctx.collection = evt.value | |
}, | |
}, | |
} | |
) | |
} | |
function dispatchChangeEvent(ctx: MachineContext) { | |
raf(() => { | |
const node = dom.getHiddenSelectEl(ctx) | |
if (!node) return | |
const win = dom.getWin(ctx) | |
const changeEvent = new win.Event('change', { | |
bubbles: true, | |
composed: true, | |
}) | |
node.dispatchEvent(changeEvent) | |
}) | |
} | |
const invoke = { | |
change: (ctx: MachineContext) => { | |
ctx.onValueChange?.({ | |
value: Array.from(ctx.value), | |
items: ctx.selectedItems, | |
}) | |
dispatchChangeEvent(ctx) | |
}, | |
highlightChange: (ctx: MachineContext) => { | |
ctx.onHighlightChange?.({ | |
highlightedValue: ctx.highlightedValue, | |
highlightedItem: ctx.highlightedItem, | |
highlightedIndex: ctx.collection.indexOf(ctx.highlightedValue), | |
}) | |
}, | |
} | |
const set = { | |
selectedItem: ( | |
ctx: MachineContext, | |
value: string | null | undefined, | |
force = false | |
) => { | |
if (isEqual(ctx.value, value)) return | |
if (value == null && !force) return | |
if (value == null && force) { | |
ctx.value = [] | |
invoke.change(ctx) | |
return | |
} | |
const nextValue = ctx.multiple ? addOrRemove(ctx.value, value!) : [value!] | |
ctx.value = nextValue | |
invoke.change(ctx) | |
}, | |
selectedItems: (ctx: MachineContext, value: string[]) => { | |
if (isEqual(ctx.value, value)) return | |
ctx.value = value | |
invoke.change(ctx) | |
}, | |
highlightedItem: ( | |
ctx: MachineContext, | |
value: string | null | undefined, | |
force = false | |
) => { | |
if (isEqual(ctx.highlightedValue, value)) return | |
if (value == null && !force) return | |
ctx.highlightedValue = value ?? null | |
invoke.highlightChange(ctx) | |
}, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { normalizeProps, Portal, useMachine } from '@zag-js/react' | |
import * as select from '@zag-js/select' | |
import { selectControls, selectData } from '@zag-js/shared' | |
import serialize from 'form-serialize' | |
import { useId } from 'react' | |
import { StateVisualizer } from '../components/state-visualizer' | |
import { Toolbar } from '../components/toolbar' | |
import { useControls } from '../hooks/use-controls' | |
export default function Page() { | |
const controls = useControls(selectControls) | |
const [state, send] = useMachine( | |
select.machine({ | |
collection: select.collection({ items: selectData }), | |
id: useId(), | |
name: 'country', | |
// onHighlightChange(details) { | |
// console.log("onHighlightChange", details) | |
// }, | |
// onValueChange(details) { | |
// console.log("onChange", details) | |
// }, | |
// onOpenChange(details) { | |
// console.log("onOpenChange", details) | |
// }, | |
}), | |
{ | |
context: controls.context, | |
} | |
) | |
const api = select.connect(state, send, normalizeProps) | |
return ( | |
<> | |
<main className="select"> | |
<div {...api.rootProps}> | |
<label {...api.labelProps}>Label</label> | |
{/* control */} | |
<div {...api.controlProps}> | |
<button {...api.triggerProps}> | |
<span>{api.valueAsString || 'Select option'}</span> | |
<span {...api.indicatorProps}>▼</span> | |
</button> | |
<button {...api.clearTriggerProps}>X</button> | |
</div> | |
<form | |
onChange={(e) => { | |
const formData = serialize(e.currentTarget, { hash: true }) | |
console.log(formData) | |
}} | |
> | |
{/* Hidden select */} | |
<select {...api.hiddenSelectProps}> | |
{api.isValueEmpty && <option value="" />} | |
{selectData.map((option) => ( | |
<option key={option.value} value={option.value}> | |
{option.label} | |
</option> | |
))} | |
</select> | |
</form> | |
{/* UI select */} | |
<Portal> | |
<div {...api.positionerProps}> | |
<div {...api.contentProps}> | |
<div {...api.virtualizerProps}> | |
{api.virtualItems.map((virtualItem) => { | |
const item = selectData[virtualItem.index] | |
return ( | |
<div | |
key={virtualItem.key} | |
{...api.getItemProps({ item, virtual: virtualItem })} | |
> | |
<span {...api.getItemTextProps({ item })}> | |
{item.label} | |
</span> | |
<span {...api.getItemIndicatorProps({ item })}>✓</span> | |
</div> | |
) | |
})} | |
</div> | |
</div> | |
</div> | |
</Portal> | |
</div> | |
</main> | |
<Toolbar controls={controls.ui} viz> | |
{/* <StateVisualizer state={state} omit={["collection", "virtualizer"]} /> */} | |
</Toolbar> | |
</> | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { Collection, CollectionItem, CollectionOptions } from "@zag-js/collection" | |
import type { StateMachine as S } from "@zag-js/core" | |
import type { InteractOutsideHandlers } from "@zag-js/dismissable" | |
import type { TypeaheadState } from "@zag-js/dom-query" | |
import type { Placement, PositioningOptions } from "@zag-js/popper" | |
import type { CommonProperties, DirectionProperty, PropTypes, RequiredBy } from "@zag-js/types" | |
import type { VirtualItem, Virtualizer, VirtualizerOptions } from "@tanstack/virtual-core" | |
/* ----------------------------------------------------------------------------- | |
* Callback details | |
* -----------------------------------------------------------------------------*/ | |
export interface ValueChangeDetails<T extends CollectionItem = CollectionItem> { | |
value: string[] | |
items: T[] | |
} | |
export interface HighlightChangeDetails<T extends CollectionItem = CollectionItem> { | |
highlightedValue: string | null | |
highlightedItem: T | null | |
highlightedIndex: number | |
} | |
export interface OpenChangeDetails { | |
open: boolean | |
} | |
/* ----------------------------------------------------------------------------- | |
* Machine context | |
* -----------------------------------------------------------------------------*/ | |
export type ElementIds = Partial<{ | |
root: string | |
content: string | |
control: string | |
trigger: string | |
clearTrigger: string | |
label: string | |
hiddenSelect: string | |
positioner: string | |
virtualizer: string | |
item(id: string | number): string | |
itemGroup(id: string | number): string | |
itemGroupLabel(id: string | number): string | |
}> | |
interface PublicContext<T extends CollectionItem = CollectionItem> | |
extends DirectionProperty, | |
CommonProperties, | |
InteractOutsideHandlers { | |
/** | |
* The item collection | |
*/ | |
collection: Collection<CollectionItem> | |
/** | |
* The ids of the elements in the select. Useful for composition. | |
*/ | |
ids?: ElementIds | |
/** | |
* The `name` attribute of the underlying select. | |
*/ | |
name?: string | |
/** | |
* The associate form of the underlying select. | |
*/ | |
form?: string | |
/** | |
* Whether the select is disabled | |
*/ | |
disabled?: boolean | |
/** | |
* Whether the select is invalid | |
*/ | |
invalid?: boolean | |
/** | |
* Whether the select is read-only | |
*/ | |
readOnly?: boolean | |
/** | |
* Whether the select should close after an item is selected | |
*/ | |
closeOnSelect?: boolean | |
/** | |
* Whether to select the highlighted item when the user presses Tab, | |
* and the menu is open. | |
*/ | |
selectOnBlur?: boolean | |
/** | |
* The callback fired when the highlighted item changes. | |
*/ | |
onHighlightChange?: (details: HighlightChangeDetails<T>) => void | |
/** | |
* The callback fired when the selected item changes. | |
*/ | |
onValueChange?: (details: ValueChangeDetails<T>) => void | |
/** | |
* Function called when the popup is opened | |
*/ | |
onOpenChange?: (details: OpenChangeDetails) => void | |
/** | |
* The positioning options of the menu. | |
*/ | |
positioning: PositioningOptions | |
/** | |
* The virtualization options of the menu. | |
*/ | |
virtualization?: VirtualizerOptions<HTMLElement, HTMLElement> | |
force?: any | |
/** | |
* The keys of the selected items | |
*/ | |
value: string[] | |
/** | |
* The key of the highlighted item | |
*/ | |
highlightedValue: string | null | |
/** | |
* Whether to loop the keyboard navigation through the options | |
*/ | |
loop?: boolean | |
/** | |
* Whether to allow multiple selection | |
*/ | |
multiple?: boolean | |
/** | |
* Whether the select menu is open | |
*/ | |
open?: boolean | |
/** | |
* Whether the select's open state is controlled by the user | |
*/ | |
"open.controlled"?: boolean | |
} | |
interface PrivateContext { | |
/** | |
* @internal | |
* Internal state of the typeahead | |
*/ | |
typeahead: TypeaheadState | |
/** | |
* @internal | |
* The current placement of the menu | |
*/ | |
currentPlacement?: Placement | |
/** | |
* @internal | |
* Whether the fieldset is disabled | |
*/ | |
fieldsetDisabled: boolean | |
/** | |
* @internal | |
* Whether to restore focus to the trigger after the menu closes | |
*/ | |
restoreFocus?: boolean | |
/** | |
* @internal | |
* The virtualizer instance | |
*/ | |
virtualizer?: Virtualizer<HTMLElement, HTMLElement> | |
} | |
type ComputedContext<T extends CollectionItem = CollectionItem> = Readonly<{ | |
/** | |
* @computed | |
* Whether there's a selected option | |
*/ | |
hasSelectedItems: boolean | |
/** | |
* @computed | |
* Whether a typeahead is currently active | |
*/ | |
isTypingAhead: boolean | |
/** | |
* @computed | |
* Whether the select is interactive | |
*/ | |
isInteractive: boolean | |
/** | |
* @computed | |
* Whether the select is disabled | |
*/ | |
isDisabled: boolean | |
/** | |
* The highlighted item | |
*/ | |
highlightedItem: T | null | |
/** | |
* @computed | |
* The selected items | |
*/ | |
selectedItems: T[] | |
/** | |
* @computed | |
* The display value of the select (based on the selected items) | |
*/ | |
valueAsString: string | |
}> | |
export type UserDefinedContext<T extends CollectionItem = CollectionItem> = RequiredBy< | |
PublicContext<T>, | |
"id" | "collection" | |
> | |
export interface MachineContext extends PublicContext, PrivateContext, ComputedContext {} | |
export interface MachineState { | |
value: "idle" | "focused" | "open" | |
} | |
export type State = S.State<MachineContext, MachineState> | |
export type Send = S.Send<S.AnyEventObject> | |
/* ----------------------------------------------------------------------------- | |
* Component API | |
* -----------------------------------------------------------------------------*/ | |
export interface ItemProps<T extends CollectionItem = CollectionItem> { | |
item: T | |
virtual?: VirtualItem | |
} | |
export interface ItemState { | |
value: string | |
isDisabled: boolean | |
isSelected: boolean | |
isHighlighted: boolean | |
} | |
export interface ItemGroupProps { | |
id: string | |
} | |
export interface ItemGroupLabelProps { | |
htmlFor: string | |
} | |
export interface MachineApi<T extends PropTypes = PropTypes, V extends CollectionItem = CollectionItem> { | |
/** | |
* The virtual items | |
*/ | |
virtualItems: VirtualItem[] | |
/** | |
* Whether the select is focused | |
*/ | |
isFocused: boolean | |
/** | |
* Whether the select is open | |
*/ | |
isOpen: boolean | |
/** | |
* Whether the select value is empty | |
*/ | |
isValueEmpty: boolean | |
/** | |
* The value of the highlighted item | |
*/ | |
highlightedValue: string | null | |
/** | |
* The highlighted item | |
*/ | |
highlightedItem: V | null | |
/** | |
* The value of the select input | |
*/ | |
highlightValue(value: string): void | |
/** | |
* The selected items | |
*/ | |
selectedItems: V[] | |
/** | |
* Whether there's a selected option | |
*/ | |
hasSelectedItems: boolean | |
/** | |
* The selected item keys | |
*/ | |
value: string[] | |
/** | |
* The string representation of the selected items | |
*/ | |
valueAsString: string | |
/** | |
* Function to select a value | |
*/ | |
selectValue(value: string): void | |
/** | |
* Function to set the value of the select | |
*/ | |
setValue(value: string[]): void | |
/** | |
* Function to clear the value of the select | |
*/ | |
clearValue(value?: string): void | |
/** | |
* Function to focus on the select input | |
*/ | |
focus(): void | |
/** | |
* Returns the state of a select item | |
*/ | |
getItemState(props: ItemProps): ItemState | |
/** | |
* Function to open the select | |
*/ | |
open(): void | |
/** | |
* Function to close the select | |
*/ | |
close(): void | |
/** | |
* Function to toggle the select | |
*/ | |
collection: Collection<V> | |
/** | |
* Function to set the collection of items | |
*/ | |
setCollection(collection: Collection<V>): void | |
/** | |
* Function to set the positioning options of the select | |
*/ | |
reposition(options: Partial<PositioningOptions>): void | |
rootProps: T["element"] | |
labelProps: T["label"] | |
controlProps: T["element"] | |
triggerProps: T["button"] | |
indicatorProps: T["element"] | |
clearTriggerProps: T["button"] | |
positionerProps: T["element"] | |
virtualizerProps: T["element"] | |
contentProps: T["element"] | |
getItemProps(props: ItemProps): T["element"] | |
getItemTextProps(props: ItemProps): T["element"] | |
getItemIndicatorProps(props: ItemProps): T["element"] | |
getItemGroupProps(props: ItemGroupProps): T["element"] | |
getItemGroupLabelProps(props: ItemGroupLabelProps): T["element"] | |
hiddenSelectProps: T["select"] | |
} | |
/* ----------------------------------------------------------------------------- | |
* Re-exported types | |
* -----------------------------------------------------------------------------*/ | |
export type { CollectionOptions, CollectionItem } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment