338 lines
10 KiB
JavaScript
338 lines
10 KiB
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
|