reka-ui
Version:
Vue port for Radix UI Primitives.
312 lines (309 loc) • 11.1 kB
JavaScript
import { defineComponent, toRefs, ref, watch, nextTick, createBlock, openBlock, unref, withCtx, renderSlot, createCommentVNode } from 'vue';
import { useVModel, createEventHook } from '@vueuse/core';
import { c as compare } from './utils.js';
import { u as useCollection } from '../Collection/Collection.js';
import '@floating-ui/vue';
import { g as getFocusIntent } from '../RovingFocus/utils.js';
import { u as useTypeahead } from '../shared/useTypeahead.js';
import { u as usePrimitiveElement } from '../Primitive/usePrimitiveElement.js';
import { u as useDirection } from '../shared/useDirection.js';
import { u as useFormControl } from '../shared/useFormControl.js';
import { c as createContext } from '../shared/createContext.js';
import { P as Primitive } from '../Primitive/Primitive.js';
import { _ as _sfc_main$1 } from '../VisuallyHidden/VisuallyHiddenInput.js';
import { u as useKbd } from '../shared/useKbd.js';
import { f as findValuesBetween } from '../shared/arrays.js';
const [injectListboxRootContext, provideListboxRootContext] = createContext("ListboxRoot");
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "ListboxRoot",
props: {
modelValue: {},
defaultValue: {},
multiple: { type: Boolean },
orientation: { default: "vertical" },
dir: {},
disabled: { type: Boolean },
selectionBehavior: { default: "toggle" },
highlightOnHover: { type: Boolean },
by: {},
asChild: { type: Boolean },
as: {},
name: {},
required: { type: Boolean }
},
emits: ["update:modelValue", "highlight", "entryFocus", "leave"],
setup(__props, { expose: __expose, emit: __emit }) {
const props = __props;
const emits = __emit;
const { multiple, highlightOnHover, orientation, disabled, selectionBehavior, dir: propDir } = toRefs(props);
const { getItems } = useCollection({ isProvider: true });
const { handleTypeaheadSearch } = useTypeahead();
const { primitiveElement, currentElement } = usePrimitiveElement();
const kbd = useKbd();
const dir = useDirection(propDir);
const isFormControl = useFormControl(currentElement);
const firstValue = ref();
const isUserAction = ref(false);
const focusable = ref(true);
const modelValue = useVModel(props, "modelValue", emits, {
defaultValue: props.defaultValue ?? (multiple.value ? [] : void 0),
passive: props.modelValue === void 0,
deep: true
});
function onValueChange(val) {
isUserAction.value = true;
if (props.multiple) {
const modelArray = Array.isArray(modelValue.value) ? [...modelValue.value] : [];
const index = modelArray.findIndex((i) => compare(i, val, props.by));
if (props.selectionBehavior === "toggle") {
index === -1 ? modelArray.push(val) : modelArray.splice(index, 1);
modelValue.value = modelArray;
} else {
modelValue.value = [val];
firstValue.value = val;
}
} else {
if (props.selectionBehavior === "toggle") {
if (compare(modelValue.value, val, props.by))
modelValue.value = void 0;
else
modelValue.value = val;
} else {
modelValue.value = val;
}
}
setTimeout(() => {
isUserAction.value = false;
}, 1);
}
const highlightedElement = ref(null);
const previousElement = ref(null);
const isVirtual = ref(false);
const isComposing = ref(false);
const virtualFocusHook = createEventHook();
const virtualKeydownHook = createEventHook();
const virtualHighlightHook = createEventHook();
function getCollectionItem() {
return getItems().map((i) => i.ref).filter((i) => i.dataset.disabled !== "");
}
function changeHighlight(el, scrollIntoView = true) {
if (!el)
return;
highlightedElement.value = el;
if (focusable.value)
highlightedElement.value.focus();
if (scrollIntoView)
highlightedElement.value.scrollIntoView({ block: "nearest" });
const highlightedItem = getItems().find((i) => i.ref === el);
emits("highlight", highlightedItem);
}
function highlightItem(value) {
if (isVirtual.value) {
virtualHighlightHook.trigger(value);
} else {
const item = getItems().find((i) => compare(i.value, value, props.by));
if (item) {
highlightedElement.value = item.ref;
changeHighlight(item.ref);
}
}
}
function onKeydownEnter(event) {
if (highlightedElement.value && highlightedElement.value.isConnected) {
event.preventDefault();
event.stopPropagation();
if (!isComposing.value) {
highlightedElement.value.click();
}
}
}
function onKeydownTypeAhead(event) {
if (!focusable.value)
return;
isUserAction.value = true;
if (isVirtual.value) {
virtualKeydownHook.trigger(event);
} else {
const isMetaKey = event.altKey || event.ctrlKey || event.metaKey;
if (isMetaKey && event.key === "a" && multiple.value) {
const collection = getItems();
const values = collection.map((i) => i.value);
modelValue.value = [...values];
event.preventDefault();
changeHighlight(collection[collection.length - 1].ref);
} else if (!isMetaKey) {
const el = handleTypeaheadSearch(event.key, getItems());
if (el)
changeHighlight(el);
}
}
setTimeout(() => {
isUserAction.value = false;
}, 1);
}
function onCompositionStart() {
isComposing.value = true;
}
function onCompositionEnd() {
requestAnimationFrame(() => {
isComposing.value = false;
});
}
function highlightFirstItem() {
nextTick(() => {
const event = new KeyboardEvent("keydown", { key: "PageUp" });
onKeydownNavigation(event);
});
}
function onLeave(event) {
const el = highlightedElement.value;
if (el?.isConnected) {
previousElement.value = el;
}
highlightedElement.value = null;
emits("leave", event);
}
function onEnter(event) {
const entryFocusEvent = new CustomEvent("listbox.entryFocus", { bubbles: false, cancelable: true });
event.currentTarget?.dispatchEvent(entryFocusEvent);
emits("entryFocus", entryFocusEvent);
if (entryFocusEvent.defaultPrevented)
return;
if (previousElement.value) {
changeHighlight(previousElement.value);
} else {
const el = getCollectionItem()?.[0];
changeHighlight(el);
}
}
function onKeydownNavigation(event) {
const intent = getFocusIntent(event, orientation.value, dir.value);
if (!intent)
return;
let collection = getCollectionItem();
if (highlightedElement.value) {
if (intent === "last") {
collection.reverse();
} else if (intent === "prev" || intent === "next") {
if (intent === "prev")
collection.reverse();
const currentIndex = collection.indexOf(highlightedElement.value);
collection = collection.slice(currentIndex + 1);
}
handleMultipleReplace(event, collection[0]);
}
if (collection.length) {
const index = !highlightedElement.value && intent === "prev" ? collection.length - 1 : 0;
changeHighlight(collection[index]);
}
if (isVirtual.value)
return virtualKeydownHook.trigger(event);
}
function handleMultipleReplace(event, targetEl) {
if (isVirtual.value || props.selectionBehavior !== "replace" || !multiple.value || !Array.isArray(modelValue.value))
return;
const isMetaKey = event.altKey || event.ctrlKey || event.metaKey;
if (isMetaKey && !event.shiftKey)
return;
if (event.shiftKey) {
const collection = getItems().filter((i) => i.ref.dataset.disabled !== "");
let lastValue = collection.find((i) => i.ref === targetEl)?.value;
if (event.key === kbd.END)
lastValue = collection[collection.length - 1].value;
else if (event.key === kbd.HOME)
lastValue = collection[0].value;
if (!lastValue || !firstValue.value)
return;
const values = findValuesBetween(collection.map((i) => i.value), firstValue.value, lastValue);
modelValue.value = values;
}
}
async function highlightSelected(event) {
await nextTick();
if (isVirtual.value) {
virtualFocusHook.trigger(event);
} else {
const collection = getCollectionItem();
const item = collection.find((i) => i.dataset.state === "checked");
if (item)
changeHighlight(item);
else if (collection.length)
changeHighlight(collection[0]);
}
}
watch(modelValue, () => {
if (!isUserAction.value) {
nextTick(() => {
highlightSelected();
});
}
}, { immediate: true, deep: true });
__expose({
highlightedElement,
highlightItem,
highlightFirstItem,
highlightSelected,
getItems
});
provideListboxRootContext({
modelValue,
// @ts-expect-error ignoring
onValueChange,
multiple,
orientation,
dir,
disabled,
highlightOnHover,
highlightedElement,
isVirtual,
virtualFocusHook,
virtualKeydownHook,
virtualHighlightHook,
by: props.by,
firstValue,
selectionBehavior,
focusable,
onLeave,
onEnter,
changeHighlight,
onKeydownEnter,
onKeydownNavigation,
onKeydownTypeAhead,
onCompositionStart,
onCompositionEnd,
highlightFirstItem
});
return (_ctx, _cache) => {
return openBlock(), createBlock(unref(Primitive), {
ref_key: "primitiveElement",
ref: primitiveElement,
as: _ctx.as,
"as-child": _ctx.asChild,
dir: unref(dir),
"data-disabled": unref(disabled) ? "" : void 0,
onPointerleave: onLeave,
onFocusout: _cache[0] || (_cache[0] = async (event) => {
const target = event.relatedTarget || event.target;
await nextTick();
if (highlightedElement.value && unref(currentElement) && !unref(currentElement).contains(target)) {
onLeave(event);
}
})
}, {
default: withCtx(() => [
renderSlot(_ctx.$slots, "default", { modelValue: unref(modelValue) }),
unref(isFormControl) && _ctx.name ? (openBlock(), createBlock(unref(_sfc_main$1), {
key: 0,
name: _ctx.name,
value: unref(modelValue),
disabled: unref(disabled),
required: _ctx.required
}, null, 8, ["name", "value", "disabled", "required"])) : createCommentVNode("", true)
]),
_: 3
}, 8, ["as", "as-child", "dir", "data-disabled"]);
};
}
});
export { _sfc_main as _, injectListboxRootContext as i };
//# sourceMappingURL=ListboxRoot.js.map