reka-ui
Version:
Vue port for Radix UI Primitives.
221 lines (217 loc) • 8.38 kB
JavaScript
;
const vue = require('vue');
const vueVirtual = require('@tanstack/vue-virtual');
const Listbox_utils = require('./utils.cjs');
const RovingFocus_utils = require('../RovingFocus/utils.cjs');
const shared = require('@vueuse/shared');
const shared_useTypeahead = require('../shared/useTypeahead.cjs');
const core = require('@vueuse/core');
const Collection_Collection = require('../Collection/Collection.cjs');
const shared_getActiveElement = require('../shared/getActiveElement.cjs');
const Listbox_ListboxRoot = require('./ListboxRoot.cjs');
const shared_arrays = require('../shared/arrays.cjs');
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
__name: "ListboxVirtualizer",
props: {
options: {},
overscan: {},
estimateSize: {},
textContent: { type: Function }
},
setup(__props) {
const props = __props;
const slots = vue.useSlots();
const rootContext = Listbox_ListboxRoot.injectListboxRootContext();
const parentEl = core.useParentElement();
const { getItems } = Collection_Collection.useCollection();
rootContext.isVirtual.value = true;
const padding = vue.computed(() => {
const el = parentEl.value;
if (!el) {
return { start: 0, end: 0 };
} else {
const styles = window.getComputedStyle(el);
return {
start: Number.parseFloat(styles.paddingBlockStart || styles.paddingTop),
end: Number.parseFloat(styles.paddingBlockEnd || styles.paddingBottom)
};
}
});
const virtualizer = vueVirtual.useVirtualizer(
{
get scrollPaddingStart() {
return padding.value.start;
},
get scrollPaddingEnd() {
return padding.value.end;
},
get count() {
return props.options.length;
},
get horizontal() {
return rootContext.orientation.value === "horizontal";
},
estimateSize() {
return props.estimateSize ?? 28;
},
getScrollElement() {
return parentEl.value;
},
overscan: props.overscan ?? 12
}
);
const virtualizedItems = vue.computed(() => virtualizer.value.getVirtualItems().map((item) => {
const defaultNode = slots.default({
option: props.options[item.index],
virtualizer: virtualizer.value,
virtualItem: item
})[0];
const targetNode = defaultNode.type === vue.Fragment && Array.isArray(defaultNode.children) ? defaultNode.children[0] : defaultNode;
return {
item,
is: vue.cloneVNode(targetNode, {
"key": `${item.key}`,
"data-index": item.index,
"aria-setsize": props.options.length,
"aria-posinset": item.index + 1,
"style": {
position: "absolute",
top: 0,
left: 0,
transform: `translateY(${item.start}px)`,
overflowAnchor: "none"
}
})
};
}));
rootContext.virtualFocusHook.on((event) => {
const index = props.options.findIndex((option) => {
if (Array.isArray(rootContext.modelValue.value))
return Listbox_utils.compare(option, rootContext.modelValue.value[0], rootContext.by);
else
return Listbox_utils.compare(option, rootContext.modelValue.value, rootContext.by);
});
if (index !== -1) {
event?.preventDefault();
virtualizer.value.scrollToIndex(index, { align: "start" });
requestAnimationFrame(() => {
const item = Listbox_utils.queryCheckedElement(parentEl.value);
if (item) {
rootContext.changeHighlight(item);
if (event)
item?.focus();
}
});
} else {
rootContext.highlightFirstItem(event);
}
});
rootContext.virtualHighlightHook.on((value) => {
const index = props.options.findIndex((option) => {
return Listbox_utils.compare(option, value, rootContext.by);
});
virtualizer.value.scrollToIndex(index, { align: "start" });
requestAnimationFrame(() => {
const item = Listbox_utils.queryCheckedElement(parentEl.value);
if (item)
rootContext.changeHighlight(item);
});
});
const search = shared.refAutoReset("", 1e3);
const optionsWithMetadata = vue.computed(() => {
const parseTextContent = (option) => {
if (props.textContent)
return props.textContent(option);
else
return option?.toString().toLowerCase();
};
return props.options.map((option, index) => ({
index,
textContent: parseTextContent(option)
}));
});
function handleMultipleReplace(event, intent) {
if (!rootContext.firstValue?.value || !rootContext.multiple.value || !Array.isArray(rootContext.modelValue.value))
return;
const collection = getItems().filter((i) => i.ref.dataset.disabled !== "");
const lastValue = collection.find((i) => i.ref === rootContext.highlightedElement.value)?.value;
if (!lastValue)
return;
let value = null;
switch (intent) {
case "prev":
case "next": {
value = shared_arrays.findValuesBetween(props.options, rootContext.firstValue.value, lastValue);
break;
}
case "first": {
value = shared_arrays.findValuesBetween(props.options, rootContext.firstValue.value, props.options?.[0]);
break;
}
case "last": {
value = shared_arrays.findValuesBetween(props.options, rootContext.firstValue.value, props.options?.[props.options.length - 1]);
break;
}
}
rootContext.modelValue.value = value;
}
rootContext.virtualKeydownHook.on((event) => {
const isMetaKey = event.altKey || event.ctrlKey || event.metaKey;
const isTabKey = event.key === "Tab" && !isMetaKey;
if (isTabKey)
return;
let intent = RovingFocus_utils.MAP_KEY_TO_FOCUS_INTENT[event.key];
if (isMetaKey && event.key === "a" && rootContext.multiple.value) {
event.preventDefault();
rootContext.modelValue.value = [...props.options];
intent = "last";
} else if (event.shiftKey && intent) {
handleMultipleReplace(event, intent);
}
if (["first", "last"].includes(intent)) {
event.preventDefault();
const index = intent === "first" ? 0 : props.options.length - 1;
virtualizer.value.scrollToIndex(index);
requestAnimationFrame(() => {
const items = getItems();
const item = intent === "first" ? items[0] : items[items.length - 1];
if (item)
rootContext.changeHighlight(item.ref);
});
} else if (!intent && !isMetaKey) {
search.value += event.key;
const currentIndex = Number(shared_getActiveElement.getActiveElement()?.getAttribute("data-index"));
const currentMatch = optionsWithMetadata.value[currentIndex].textContent;
const filteredOptions = optionsWithMetadata.value.map((i) => i.textContent ?? "");
const next = shared_useTypeahead.getNextMatch(filteredOptions, search.value, currentMatch);
const nextMatch = optionsWithMetadata.value.find((option) => option.textContent === next);
if (nextMatch) {
virtualizer.value.scrollToIndex(nextMatch.index, { align: "start" });
requestAnimationFrame(() => {
const item = parentEl.value.querySelector(`[data-index="${nextMatch.index}"]`);
if (item instanceof HTMLElement)
rootContext.changeHighlight(item);
});
}
}
});
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock("div", {
"data-reka-virtualizer": "",
style: vue.normalizeStyle({
position: "relative",
width: "100%",
height: `${vue.unref(virtualizer).getTotalSize()}px`
})
}, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(virtualizedItems.value, ({ is, item }) => {
return vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent(is), {
key: item.index
});
}), 128))
], 4);
};
}
});
exports._sfc_main = _sfc_main;
//# sourceMappingURL=ListboxVirtualizer.cjs.map