reka-ui
Version:
Vue port for Radix UI Primitives.
338 lines (335 loc) • 10.7 kB
JavaScript
import { findValuesBetween } from "../shared/arrays.js";
import { createContext } from "../shared/createContext.js";
import { useDirection } from "../shared/useDirection.js";
import { useFormControl } from "../shared/useFormControl.js";
import { useKbd } from "../shared/useKbd.js";
import { useTypeahead } from "../shared/useTypeahead.js";
import { Primitive } from "../Primitive/Primitive.js";
import { usePrimitiveElement } from "../Primitive/usePrimitiveElement.js";
import { useCollection } from "../Collection/Collection.js";
import { getFocusIntent } from "../RovingFocus/utils.js";
import { VisuallyHiddenInput_default } from "../VisuallyHidden/VisuallyHiddenInput.js";
import { compare } from "./utils.js";
import { createBlock, createCommentVNode, defineComponent, nextTick, openBlock, ref, renderSlot, toRefs, unref, watch, withCtx } from "vue";
import { createEventHook, useVModel } from "@vueuse/core";
//#region src/Listbox/ListboxRoot.vue?vue&type=script&setup=true&lang.ts
const [injectListboxRootContext, provideListboxRootContext] = createContext("ListboxRoot");
var ListboxRoot_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
__name: "ListboxRoot",
props: {
modelValue: {
type: null,
required: false
},
defaultValue: {
type: null,
required: false
},
multiple: {
type: Boolean,
required: false
},
orientation: {
type: String,
required: false,
default: "vertical"
},
dir: {
type: String,
required: false
},
disabled: {
type: Boolean,
required: false
},
selectionBehavior: {
type: String,
required: false,
default: "toggle"
},
highlightOnHover: {
type: Boolean,
required: false
},
by: {
type: [String, Function],
required: false
},
asChild: {
type: Boolean,
required: false
},
as: {
type: null,
required: false
},
name: {
type: String,
required: false
},
required: {
type: Boolean,
required: false
}
},
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() {
nextTick(() => {
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,
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(VisuallyHiddenInput_default), {
key: 0,
name: _ctx.name,
value: unref(modelValue),
disabled: unref(disabled),
required: _ctx.required
}, null, 8, [
"name",
"value",
"disabled",
"required"
])) : createCommentVNode("v-if", true)]),
_: 3
}, 8, [
"as",
"as-child",
"dir",
"data-disabled"
]);
};
}
});
//#endregion
//#region src/Listbox/ListboxRoot.vue
var ListboxRoot_default = ListboxRoot_vue_vue_type_script_setup_true_lang_default;
//#endregion
export { ListboxRoot_default, injectListboxRootContext };
//# sourceMappingURL=ListboxRoot.js.map