vectis-ui 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/VColorPicker/VColorPicker.css +1 -1
- package/dist/components/VColorPicker/VColorPicker.js.map +1 -1
- package/dist/components/VColorPicker/VColorPicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCommandPalette/VCommandPalette.css +1 -1
- package/dist/components/VCommandPalette/VCommandPalette.js.map +1 -1
- package/dist/components/VCommandPalette/VCommandPalette.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +6 -5
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VNumberInput/number.d.ts +2 -2
- package/dist/components/VNumberInput/number.js +1 -1
- package/dist/components/VNumberInput/number.js.map +1 -1
- package/dist/components/VPagination/VPagination.js.map +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSelect/VSelect.js.map +1 -1
- package/dist/components/VSelect/VSelect.vue_vue_type_script_setup_true_lang.js +168 -174
- package/dist/components/VSelect/VSelect.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggle.js.map +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTreeView/VTreeView.js.map +1 -1
- package/dist/components/VTreeView/VTreeView.vue_vue_type_script_setup_true_lang.js +127 -131
- package/dist/components/VTreeView/VTreeView.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/useHotkeyListener.js +5 -2
- package/dist/composables/useHotkeyListener.js.map +1 -1
- package/dist/composables/useInfiniteScroll.js +2 -2
- package/dist/composables/useInfiniteScroll.js.map +1 -1
- package/dist/composables/useTypeahead.d.ts +13 -0
- package/dist/composables/useTypeahead.js +23 -0
- package/dist/composables/useTypeahead.js.map +1 -0
- package/dist/i18n/state.d.ts +1 -1
- package/dist/i18n/state.js +17 -6
- package/dist/i18n/state.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/date.js +4 -3
- package/dist/utils/date.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VCommandPalette.js","names":[],"sources":["../../../src/components/VCommandPalette/VCommandPalette.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * A native modal `<dialog>` holding a search field and the listbox it filters. The focus stays\n * in the field, which points at the active command through aria-activedescendant: no native\n * element pairs free text with a list the arrows walk through.\n */\n\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport VEmptyState from '../VEmptyState/VEmptyState.vue'\nimport VHotkeys from '../VHotkeys/VHotkeys.vue'\nimport {\n DEFAULT_PLATFORM,\n ariaKeyshortcuts,\n detectPlatform,\n parseHotkeys,\n} from '../VHotkeys/platform'\nimport type { HotkeysPlatform } from '../VHotkeys/platform'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { search as searchIcon } from '../VIcon/icons/search'\nimport { search_off as searchOffIcon } from '../VIcon/icons/search_off'\nimport type { IconSource } from '../VIcon/types'\nimport VSeparator from '../VSeparator/VSeparator.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VCommandPaletteItem from './VCommandPaletteItem.vue'\nimport { useHotkeyListener } from '../../composables/useHotkeyListener'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useDebouncedSearch } from '../../composables/useDebouncedSearch'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\nimport { createNormalizedCache, normalizeText } from '../../utils/text'\n\n/** One thing the palette can do. */\nexport interface CommandPaletteCommand {\n /** What it is called on screen, and what the search matches. */\n label: string\n /** A second line under the label. The search does not read it. */\n description?: string\n /** Other words the search matches without showing them: synonyms, a former name. */\n keywords?: string[]\n /** An icon before the label: an icon name, or an explicit render. */\n icon?: IconSource\n /**\n * A shortcut shown at the end of the row, written as for VHotkeys (`mod+shift+p`). The palette\n * displays it and does not listen for it.\n */\n shortcut?: string\n /**\n * Makes the command a link: choosing it follows the address, and a modified or middle click\n * opens it elsewhere, natively.\n */\n href?: string\n /** Shows the command without allowing it to be chosen. */\n disabled?: boolean\n /** Leaves the palette open once the command is chosen. */\n keepOpen?: boolean\n /** An identifier of your own. The palette does not read it; it comes back with `select`. */\n id?: string | number\n}\n\n/** A named block of commands. A block none of whose commands match is hidden, name included. */\nexport interface CommandPaletteGroup {\n label: string\n commands: CommandPaletteCommand[]\n}\n\n/** A rule between two blocks. One the search leaves at an end or against another is not drawn. */\nexport interface CommandPaletteSeparator {\n separator: true\n}\n\n/** Anything the list may hold: a command, a named block, or a separator. */\nexport type CommandPaletteItem =\n CommandPaletteCommand | CommandPaletteGroup | CommandPaletteSeparator\n\n/**\n * How the list is narrowed as one types: by the label and the keywords, not at all (when the\n * commands arrive already filtered by a server), or by a rule of your own.\n */\nexport type CommandPaletteFilter =\n boolean | ((command: CommandPaletteCommand, query: string) => boolean)\n\n/** What the `#item` slot receives. */\nexport interface CommandPaletteItemSlotProps {\n command: CommandPaletteCommand\n active: boolean\n}\n\n/** What the `#empty` slot receives. */\nexport interface CommandPaletteEmptySlotProps {\n query: string\n}\n\n/** What the trigger has to carry: the click that opens the palette, and the fact that it does. */\nexport type CommandPaletteTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n /** The `shortcut`, spelled for assistive technology, when there is one. */\n 'aria-keyshortcuts'?: string\n}\n\nconst isGroup = (item: CommandPaletteItem): item is CommandPaletteGroup => 'commands' in item\nconst isSeparator = (item: CommandPaletteItem): item is CommandPaletteSeparator =>\n 'separator' in item\n\ninterface CommandPaletteProps {\n /** What the palette offers: commands, named blocks of commands, and separators. */\n items: CommandPaletteItem[]\n /**\n * A shortcut that opens the palette from anywhere in the page and closes it again, written as\n * for VHotkeys: `mod+k` is ⌘K on a Mac and Ctrl+K elsewhere. Left out, nothing is listened for.\n */\n shortcut?: string\n /** How the list is narrowed as one types. */\n filter?: CommandPaletteFilter\n /**\n * How long to wait before reporting the search with `search`, in milliseconds. Zero reports\n * it at once.\n */\n searchDebounce?: number\n /** Says that commands are being loaded: a spinner shows in the search field. */\n loading?: boolean\n /** What is announced while loading with nothing to show yet. */\n loadingText?: string\n /** What the palette says when nothing matches. */\n emptyText?: string\n /** What the search field says while empty. */\n placeholder?: string\n /** The accessible name of the palette. */\n label?: string\n /** The accessible name of its search field. */\n searchLabel?: string\n /** Leaves out the footer listing the keys. */\n hideFooter?: boolean\n /**\n * How wide the palette is: a number is read as pixels, a string as any CSS length. Left out,\n * it takes the `--vectis-control-size-command-palette-width` token, 640px by default.\n */\n width?: number | string\n}\n\nconst props = withDefaults(defineProps<CommandPaletteProps>(), {\n shortcut: undefined,\n filter: true,\n searchDebounce: 250,\n loading: false,\n loadingText: undefined,\n emptyText: undefined,\n placeholder: undefined,\n label: undefined,\n searchLabel: undefined,\n hideFooter: false,\n width: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A command was chosen, by a click or by Enter. Call `event.preventDefault()` to stop a link\n * being followed, to hand its address to a router instead.\n */\n select: [command: CommandPaletteCommand, event: MouseEvent]\n /** What is being searched for, to be sent to the source. It is also reported on opening. */\n search: [query: string]\n}>()\n\ndefineSlots<{\n /**\n * The button that opens the palette. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the palette itself.\n */\n trigger?(props: { triggerProps: CommandPaletteTriggerProps }): unknown\n /** The label and description of a row. The icon and the shortcut stay in place. */\n item?(props: CommandPaletteItemSlotProps): unknown\n /** What the palette shows when nothing matches. It receives the term that was searched. */\n empty?(props: CommandPaletteEmptySlotProps): unknown\n /** What the palette shows while loading with nothing to show yet. */\n loading?(): unknown\n /** Replaces the footer listing the keys. */\n footer?(): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst m = useMessages()\nconst resolvedLabel = computed(() => props.label ?? m.value.commandPalette.label)\nconst resolvedSearchLabel = computed(() => props.searchLabel ?? m.value.commandPalette.searchLabel)\nconst resolvedPlaceholder = computed(() => props.placeholder ?? m.value.commandPalette.placeholder)\nconst resolvedEmptyText = computed(() => props.emptyText ?? m.value.commandPalette.empty)\nconst resolvedLoadingText = computed(() => props.loadingText ?? m.value.common.loading)\n\n/** Whether the palette is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n/** What is typed in the search field. It is emptied each time the palette closes. */\nconst query = defineModel<string>('query', { default: '' })\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, { persistentBackdrop: () => false, persistentEscape: () => false })\n\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst listId = useId()\nconst activeIndex = ref(-1)\n\n// @ssr\n// The server renders `mod` as Ctrl; the platform is read once mounted.\nconst platform = ref<HotkeysPlatform>(DEFAULT_PLATFORM)\nonMounted(() => {\n platform.value = detectPlatform()\n})\n\nconst shortcutTokens = computed(() => (props.shortcut ? parseHotkeys(props.shortcut) : []))\n\nuseHotkeyListener({\n tokens: () => shortcutTokens.value,\n platform: () => platform.value,\n enabled: () => !!props.shortcut,\n // While open, the focus is held inside the palette: the field being typed in is its own\n // search field, and the shortcut closes it again.\n allowInInput: () => open.value,\n onTrigger: () => (open.value ? close() : void show()),\n})\n\nconst triggerProps = computed<CommandPaletteTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n 'aria-keyshortcuts': props.shortcut\n ? ariaKeyshortcuts(shortcutTokens.value, platform.value)\n : undefined,\n}))\n\n/** A command, with its place among all of them: the same command may be listed twice. */\ntype Entry = { command: CommandPaletteCommand; source: number }\n\n// The blocks unwrapped and the separators dropped: this flat list is what the filter, the\n// keyboard and the option ids count through.\nconst allEntries = computed<Entry[]>(() =>\n props.items\n .flatMap((item) => {\n if (isSeparator(item)) return []\n return isGroup(item) ? item.commands : [item]\n })\n .map((command, source) => ({ command, source })),\n)\n\nconst normalizedOf = createNormalizedCache<CommandPaletteCommand>()\n\nfunction matches(command: CommandPaletteCommand, needle: string) {\n if (normalizedOf(command, command.label).includes(needle)) return true\n return (\n command.keywords?.some((word, i) =>\n normalizedOf(command, word, `keyword${i}`).includes(needle),\n ) ?? false\n )\n}\n\nconst term = computed(() => query.value.trim())\n\nconst filtered = computed(() => {\n const list = allEntries.value\n const matcher = props.filter\n if (matcher === false || !term.value) return list\n if (typeof matcher === 'function') return list.filter((e) => matcher(e.command, term.value))\n const needle = normalizeText(term.value)\n return list.filter((e) => matches(e.command, needle))\n})\n\nconst firstEnabled = () => filtered.value.findIndex((e) => !e.command.disabled)\n\n// @a11y\n// An option's id follows its place among ALL the commands, not its place in the filtered list:\n// a screen reader announces the active option when aria-activedescendant changes, and a\n// positional id would stay the same while the filter slides another command under it. Nor is\n// it keyed by the command object, which a block of recent commands lists a second time.\nconst optionId = (source: number) => `${listId}-${source}`\nconst activeId = computed(() => {\n const entry = filtered.value[activeIndex.value]\n return entry ? optionId(entry.source) : undefined\n})\n\ntype Row = {\n kind: 'command'\n key: string\n command: CommandPaletteCommand\n index: number\n source: number\n}\ntype Node =\n | Row\n | { kind: 'group'; key: string; label: string; labelId: string; rows: Row[] }\n | { kind: 'separator'; key: string }\n\nconst nodes = computed<Node[]>(() => {\n const kept = new Set(filtered.value.map((e) => e.source))\n let source = 0\n let next = 0\n // Walked in the order `allEntries` was built, so `source` names the same entry.\n const rowOf = (command: CommandPaletteCommand, key: string): Row | null => {\n const at = source++\n return kept.has(at) ? { kind: 'command', key, command, index: next++, source: at } : null\n }\n\n const result: Node[] = []\n // A separator is held back until something comes after it, and dropped when nothing came\n // before: one rule for the three ways a search can strand it.\n let pendingSeparator: string | null = null\n for (const [i, item] of props.items.entries()) {\n if (isSeparator(item)) {\n if (result.length > 0) pendingSeparator = `separator-${i}`\n continue\n }\n let node: Node | null\n if (isGroup(item)) {\n const rows = item.commands\n .map((command, j) => rowOf(command, `${i}.${j}`))\n .filter((row): row is Row => row !== null)\n node = rows.length\n ? {\n kind: 'group',\n key: `group-${i}`,\n label: item.label,\n labelId: `${listId}-group-${i}`,\n rows,\n }\n : null\n } else {\n node = rowOf(item, String(i))\n }\n if (!node) continue\n if (pendingSeparator !== null) {\n result.push({ kind: 'separator', key: pendingSeparator })\n pendingSeparator = null\n }\n result.push(node)\n }\n return result\n})\n\n// @a11y\n/** What is announced about the list: that nothing matches, or that something is loading. */\nconst stateAnnouncement = computed(() => {\n if (!open.value || filtered.value.length > 0) return ''\n return props.loading ? resolvedLoadingText.value : resolvedEmptyText.value\n})\n\nconst { request: emitSearch, cancel: cancelSearch } = useDebouncedSearch({\n delay: () => props.searchDebounce,\n active: () => open.value,\n report: (value) => emit('search', value),\n})\n\nwatch(open, (value) => {\n if (value) {\n activeIndex.value = firstEnabled()\n emitSearch(term.value, true)\n } else {\n cancelSearch()\n query.value = ''\n activeIndex.value = -1\n }\n})\n\nwatch(term, (value) => {\n if (!open.value) return\n activeIndex.value = firstEnabled()\n emitSearch(value)\n})\n\n// Commands arriving from a source replace the list without the term changing: a highlight that\n// fell out of it, or that never existed, moves to the first command, or Enter would do nothing.\nwatch(filtered, (list) => {\n if (!open.value) return\n if (activeIndex.value < 0 || activeIndex.value >= list.length) activeIndex.value = firstEnabled()\n})\n\n// @a11y\n// The focus never moves, so nothing scrolls the active row into view by itself.\nwatch(\n activeId,\n () => {\n if (!activeId.value) return\n // Optional call: jsdom implements no scrolling.\n document.getElementById(activeId.value)?.scrollIntoView?.({ block: 'nearest' })\n },\n { flush: 'post' },\n)\n\n/** What a row draws: the command's own fields, and none of the ones meant for the palette. */\nconst rowProps = ({\n label,\n description,\n icon,\n shortcut,\n href,\n disabled,\n}: CommandPaletteCommand) => ({\n label,\n description,\n icon,\n shortcut,\n href,\n disabled,\n})\n\nfunction hover(row: Row) {\n if (!row.command.disabled) activeIndex.value = row.index\n}\n\nfunction choose(command: CommandPaletteCommand, event: MouseEvent) {\n emit('select', command, event)\n // A link opened elsewhere leaves this page as it was, the palette included.\n const elsewhere = !!command.href && (event.ctrlKey || event.metaKey || event.shiftKey)\n if (!command.keepOpen && !elsewhere) open.value = false\n}\n\n// @keyboard\nfunction move(delta: number) {\n const list = filtered.value\n if (list.length === 0) return\n // From nothing highlighted, a step back lands on the last command rather than the one before.\n let i = activeIndex.value < 0 && delta < 0 ? 0 : activeIndex.value\n for (let step = 0; step < list.length; step++) {\n i = (i + delta + list.length) % list.length\n if (!list[i]?.command.disabled) break\n }\n activeIndex.value = i\n}\n\n// @keyboard\n// Home and End stay with the field, where they move the caret. Escape is the dialog's own\n// close request.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.isComposing) return\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n move(1)\n break\n case 'ArrowUp':\n event.preventDefault()\n move(-1)\n break\n case 'Enter': {\n if (!activeId.value) return\n event.preventDefault()\n // Clicking the row itself is what lets a link be followed natively.\n document.getElementById(activeId.value)?.click()\n break\n }\n }\n}\n\ndefineExpose({\n /**\n * Opens the palette, exactly as setting `open` does. The promise settles once the element has\n * been rendered.\n */\n show,\n /** Closes the palette. */\n close,\n /** Moves the focus to the search field, while the palette is open. */\n focus: (options?: FocusOptions) => inputEl.value?.focus(options),\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-label=\"resolvedLabel\"\n v-bind=\"rootAttrs\"\n class=\"v-command-palette\"\n :style=\"{ '--command-palette-width': cssSize(width) }\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <div class=\"v-command-palette-search\">\n <VIcon :name=\"searchIcon\" class=\"v-command-palette-search-icon\" />\n <!-- `autofocus` is what the native dialog focusing steps look for when it opens. -->\n <!-- eslint-disable vuejs-accessibility/no-autofocus -->\n <input\n ref=\"inputEl\"\n v-model=\"query\"\n class=\"v-command-palette-input\"\n type=\"text\"\n role=\"combobox\"\n autofocus\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n aria-expanded=\"true\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"resolvedSearchLabel\"\n :placeholder=\"resolvedPlaceholder\"\n @keydown=\"onKeydown\"\n />\n <!-- eslint-enable vuejs-accessibility/no-autofocus -->\n <VSpinner v-if=\"loading\" class=\"v-command-palette-spinner\" aria-hidden=\"true\" />\n </div>\n\n <!-- A press on a row would take the focus out of the field; it is cancelled so the click\n that follows still chooses, and the field keeps typing. -->\n <div\n :id=\"listId\"\n role=\"listbox\"\n :aria-label=\"resolvedLabel\"\n class=\"v-command-palette-list v-control\"\n data-size=\"md\"\n @mousedown.prevent\n >\n <template v-for=\"node in nodes\" :key=\"node.key\">\n <VSeparator\n v-if=\"node.kind === 'separator'\"\n role=\"presentation\"\n class=\"v-command-palette-separator\"\n />\n <div\n v-else-if=\"node.kind === 'group'\"\n role=\"group\"\n class=\"v-command-palette-group\"\n :aria-labelledby=\"node.labelId\"\n >\n <span :id=\"node.labelId\" class=\"v-command-palette-group-label\">{{ node.label }}</span>\n <VCommandPaletteItem\n v-for=\"row in node.rows\"\n :id=\"optionId(row.source)\"\n :key=\"row.key\"\n v-bind=\"rowProps(row.command)\"\n :active=\"row.index === activeIndex\"\n @select=\"(event) => choose(row.command, event)\"\n @pointermove=\"hover(row)\"\n >\n <template v-if=\"$slots.item\" #default>\n <slot name=\"item\" :command=\"row.command\" :active=\"row.index === activeIndex\" />\n </template>\n </VCommandPaletteItem>\n </div>\n <VCommandPaletteItem\n v-else\n :id=\"optionId(node.source)\"\n v-bind=\"rowProps(node.command)\"\n :active=\"node.index === activeIndex\"\n @select=\"(event) => choose(node.command, event)\"\n @pointermove=\"hover(node)\"\n >\n <template v-if=\"$slots.item\" #default>\n <slot name=\"item\" :command=\"node.command\" :active=\"node.index === activeIndex\" />\n </template>\n </VCommandPaletteItem>\n </template>\n\n <!-- Loading is checked before emptiness, so a list waiting for an answer never claims\n there is none. Both are spoken by the status below instead. -->\n <div\n v-if=\"loading && filtered.length === 0\"\n class=\"v-command-palette-state\"\n aria-hidden=\"true\"\n >\n <slot name=\"loading\">\n <VSpinner />\n <span>{{ resolvedLoadingText }}</span>\n </slot>\n </div>\n <div v-else-if=\"filtered.length === 0\" class=\"v-command-palette-state\" aria-hidden=\"true\">\n <slot name=\"empty\" :query=\"term\">\n <VEmptyState\n size=\"sm\"\n class=\"v-command-palette-empty\"\n :icon=\"searchOffIcon\"\n :title=\"resolvedEmptyText\"\n />\n </slot>\n </div>\n </div>\n\n <footer v-if=\"!hideFooter\" class=\"v-command-palette-footer\">\n <slot name=\"footer\">\n <!-- A reminder for the eye: the combobox already tells assistive technology how it\n works, and every cap would otherwise be read as \"Keyboard shortcut\". -->\n <span class=\"v-command-palette-hints\" aria-hidden=\"true\">\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"up\" />\n <VHotkeys keys=\"down\" />\n {{ m.commandPalette.navigate }}\n </span>\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"enter\" />\n {{ m.commandPalette.choose }}\n </span>\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"esc\" />\n {{ m.common.close }}\n </span>\n </span>\n </slot>\n </footer>\n\n <!-- Outside the listbox, which may hold only options and groups. -->\n <span class=\"v-visually-hidden\" role=\"status\">{{ stateAnnouncement }}</span>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-command-palette {\n /*\n * Declared on the element rather than as a fallback: a custom property inherits, and a\n * palette opened inside a wider dialog would otherwise take that dialog's width.\n */\n --command-palette-width: var(--vectis-control-size-command-palette-width);\n /*\n * Where a row's icon and a block's name start: the list padding plus the inline padding of\n * a medium row. The field and the footer use it to line up with them.\n */\n --command-palette-inset: calc(var(--vectis-space-2) + var(--vectis-space-4));\n inline-size: var(--command-palette-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(\n 100dvb - var(--vectis-control-size-command-palette-offset) - var(--vectis-space-4)\n );\n /*\n * Pinned near the top rather than centred: the list grows and shrinks with every keystroke,\n * and a centred palette would move the field being typed in.\n */\n margin: var(--vectis-control-size-command-palette-offset) auto auto;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /* The display above beats the browser's own rule hiding a closed dialog. */\n .v-command-palette:not([open]) {\n display: none;\n }\n\n .v-command-palette:focus-visible {\n outline: none;\n }\n\n .v-command-palette-search {\n flex: none;\n display: flex;\n align-items: center;\n /* The gap of a medium row, so the typed text starts where the labels do. */\n gap: var(--vectis-space-2);\n min-block-size: var(--vectis-control-height-xl);\n padding-inline: var(--command-palette-inset);\n border-block-end: 1px solid var(--vectis-color-border);\n /* The magnifier matches the larger text of the field, through VIcon's context size. */\n --vectis-icon-size: var(--vectis-icon-size-md);\n }\n\n /* The field draws no ring of its own inside the palette: its edge takes the focus colour. */\n .v-command-palette-search:has(.v-command-palette-input:focus-visible) {\n border-block-end-color: var(--vectis-focus-ring-color);\n }\n\n .v-command-palette-search-icon,\n .v-command-palette-spinner {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n .v-command-palette-input {\n flex: 1;\n min-inline-size: 0;\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: var(--vectis-text-body-lg-size);\n font-weight: var(--vectis-text-body-lg-weight);\n line-height: var(--vectis-text-body-lg-leading);\n outline: none;\n }\n\n .v-command-palette-input::placeholder {\n color: var(--vectis-color-text-subtle);\n opacity: 1;\n }\n\n /*\n * The zero minimum is what lets the list shrink inside a short viewport and scroll, a flex\n * item otherwise refusing to go below its content.\n */\n .v-command-palette-list {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n min-block-size: 0;\n max-block-size: var(--vectis-control-size-command-palette-list-max-block);\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: var(--vectis-space-2);\n scroll-padding-block: var(--vectis-space-2);\n }\n\n /* Blocks and rows refuse to shrink: the list is a bounded column that scrolls instead. */\n .v-command-palette-group {\n display: flex;\n flex: none;\n flex-direction: column;\n gap: var(--vectis-space-1);\n }\n\n .v-command-palette-list > .v-command-palette-item {\n flex: none;\n }\n\n .v-command-palette-group-label {\n display: flex;\n align-items: center;\n min-block-size: var(--control-height);\n padding-inline: var(--control-padding-inline);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n\n /* Compounded with `.v-separator`, whose own sheet sets the margin and may load after this one. */\n .v-separator.v-command-palette-separator {\n flex: none;\n margin-inline: calc(-1 * var(--vectis-space-2));\n }\n\n .v-command-palette-state {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: center;\n gap: var(--control-gap);\n min-block-size: var(--control-height);\n padding: var(--vectis-space-1) var(--control-padding-inline);\n font-size: var(--control-font-size);\n color: var(--vectis-color-text-muted);\n }\n\n /* `[data-size]` lifts the selector above VEmptyState's size rules, whose sheet may load after. */\n .v-command-palette-state > .v-command-palette-empty[data-size] {\n flex: 1;\n padding-block: var(--vectis-space-4);\n padding-inline: 0;\n }\n\n .v-command-palette-footer {\n flex: none;\n display: flex;\n align-items: center;\n padding: var(--vectis-space-2) var(--command-palette-inset);\n border-block-start: 1px solid var(--vectis-color-border);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n color: var(--vectis-color-text-muted);\n }\n\n .v-command-palette-hints {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vectis-space-1) var(--vectis-space-4);\n }\n\n .v-command-palette-hint {\n display: inline-flex;\n align-items: center;\n gap: var(--vectis-space-1);\n }\n\n .v-command-palette {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-command-palette[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-command-palette::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-command-palette[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-command-palette,\n .v-command-palette::backdrop {\n transition: none;\n }\n }\n\n @media (forced-colors: active) {\n .v-command-palette {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VCommandPalette.js","names":[],"sources":["../../../src/components/VCommandPalette/VCommandPalette.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * A native modal `<dialog>` holding a search field and the listbox it filters. The focus stays\n * in the field, which points at the active command through aria-activedescendant: no native\n * element pairs free text with a list the arrows walk through.\n */\n\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport VEmptyState from '../VEmptyState/VEmptyState.vue'\nimport VHotkeys from '../VHotkeys/VHotkeys.vue'\nimport {\n DEFAULT_PLATFORM,\n ariaKeyshortcuts,\n detectPlatform,\n parseHotkeys,\n} from '../VHotkeys/platform'\nimport type { HotkeysPlatform } from '../VHotkeys/platform'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { search as searchIcon } from '../VIcon/icons/search'\nimport { search_off as searchOffIcon } from '../VIcon/icons/search_off'\nimport type { IconSource } from '../VIcon/types'\nimport VSeparator from '../VSeparator/VSeparator.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VCommandPaletteItem from './VCommandPaletteItem.vue'\nimport { useHotkeyListener } from '../../composables/useHotkeyListener'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useDebouncedSearch } from '../../composables/useDebouncedSearch'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\nimport { createNormalizedCache, normalizeText } from '../../utils/text'\n\n/** One thing the palette can do. */\nexport interface CommandPaletteCommand {\n /** What it is called on screen, and what the search matches. */\n label: string\n /** A second line under the label. The search does not read it. */\n description?: string\n /** Other words the search matches without showing them: synonyms, a former name. */\n keywords?: string[]\n /** An icon before the label: an icon name, or an explicit render. */\n icon?: IconSource\n /**\n * A shortcut shown at the end of the row, written as for VHotkeys (`mod+shift+p`). The palette\n * displays it and does not listen for it.\n */\n shortcut?: string\n /**\n * Makes the command a link: choosing it follows the address, and a modified or middle click\n * opens it elsewhere, natively.\n */\n href?: string\n /** Shows the command without allowing it to be chosen. */\n disabled?: boolean\n /** Leaves the palette open once the command is chosen. */\n keepOpen?: boolean\n /** An identifier of your own. The palette does not read it; it comes back with `select`. */\n id?: string | number\n}\n\n/** A named block of commands. A block none of whose commands match is hidden, name included. */\nexport interface CommandPaletteGroup {\n label: string\n commands: CommandPaletteCommand[]\n}\n\n/** A rule between two blocks. One the search leaves at an end or against another is not drawn. */\nexport interface CommandPaletteSeparator {\n separator: true\n}\n\n/** Anything the list may hold: a command, a named block, or a separator. */\nexport type CommandPaletteItem =\n CommandPaletteCommand | CommandPaletteGroup | CommandPaletteSeparator\n\n/**\n * How the list is narrowed as one types: by the label and the keywords, not at all (when the\n * commands arrive already filtered by a server), or by a rule of your own.\n */\nexport type CommandPaletteFilter =\n boolean | ((command: CommandPaletteCommand, query: string) => boolean)\n\n/** What the `#item` slot receives. */\nexport interface CommandPaletteItemSlotProps {\n command: CommandPaletteCommand\n active: boolean\n}\n\n/** What the `#empty` slot receives. */\nexport interface CommandPaletteEmptySlotProps {\n query: string\n}\n\n/** What the trigger has to carry: the click that opens the palette, and the fact that it does. */\nexport type CommandPaletteTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n /** The `shortcut`, spelled for assistive technology, when there is one. */\n 'aria-keyshortcuts'?: string\n}\n\nconst isGroup = (item: CommandPaletteItem): item is CommandPaletteGroup => 'commands' in item\nconst isSeparator = (item: CommandPaletteItem): item is CommandPaletteSeparator =>\n 'separator' in item\n\ninterface CommandPaletteProps {\n /** What the palette offers: commands, named blocks of commands, and separators. */\n items: CommandPaletteItem[]\n /**\n * A shortcut that opens the palette from anywhere in the page and closes it again, written as\n * for VHotkeys: `mod+k` is ⌘K on a Mac and Ctrl+K elsewhere. Left out, nothing is listened for.\n */\n shortcut?: string\n /** How the list is narrowed as one types. */\n filter?: CommandPaletteFilter\n /**\n * How long to wait before reporting the search with `search`, in milliseconds. Zero reports\n * it at once.\n */\n searchDebounce?: number\n /** Says that commands are being loaded: a spinner shows in the search field. */\n loading?: boolean\n /** What is announced while loading with nothing to show yet. */\n loadingText?: string\n /** What the palette says when nothing matches. */\n emptyText?: string\n /** What the search field says while empty. */\n placeholder?: string\n /** The accessible name of the palette. */\n label?: string\n /** The accessible name of its search field. */\n searchLabel?: string\n /** Leaves out the footer listing the keys. */\n hideFooter?: boolean\n /**\n * How wide the palette is: a number is read as pixels, a string as any CSS length. Left out,\n * it takes the `--vectis-control-size-command-palette-width` token, 640px by default.\n */\n width?: number | string\n}\n\nconst props = withDefaults(defineProps<CommandPaletteProps>(), {\n shortcut: undefined,\n filter: true,\n searchDebounce: 250,\n loading: false,\n loadingText: undefined,\n emptyText: undefined,\n placeholder: undefined,\n label: undefined,\n searchLabel: undefined,\n hideFooter: false,\n width: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A command was chosen, by a click or by Enter. Call `event.preventDefault()` to stop a link\n * being followed, to hand its address to a router instead.\n */\n select: [command: CommandPaletteCommand, event: MouseEvent]\n /** What is being searched for, to be sent to the source. It is also reported on opening. */\n search: [query: string]\n}>()\n\ndefineSlots<{\n /**\n * The button that opens the palette. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the palette itself.\n */\n trigger?(props: { triggerProps: CommandPaletteTriggerProps }): unknown\n /** The label and description of a row. The icon and the shortcut stay in place. */\n item?(props: CommandPaletteItemSlotProps): unknown\n /** What the palette shows when nothing matches. It receives the term that was searched. */\n empty?(props: CommandPaletteEmptySlotProps): unknown\n /** What the palette shows while loading with nothing to show yet. */\n loading?(): unknown\n /** Replaces the footer listing the keys. */\n footer?(): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst m = useMessages()\nconst resolvedLabel = computed(() => props.label ?? m.value.commandPalette.label)\nconst resolvedSearchLabel = computed(() => props.searchLabel ?? m.value.commandPalette.searchLabel)\nconst resolvedPlaceholder = computed(() => props.placeholder ?? m.value.commandPalette.placeholder)\nconst resolvedEmptyText = computed(() => props.emptyText ?? m.value.commandPalette.empty)\nconst resolvedLoadingText = computed(() => props.loadingText ?? m.value.common.loading)\n\n/** Whether the palette is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n/** What is typed in the search field. It is emptied each time the palette closes. */\nconst query = defineModel<string>('query', { default: '' })\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, { persistentBackdrop: () => false, persistentEscape: () => false })\n\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst listId = useId()\nconst activeIndex = ref(-1)\n\n// @ssr\n// The server renders `mod` as Ctrl; the platform is read once mounted.\nconst platform = ref<HotkeysPlatform>(DEFAULT_PLATFORM)\nonMounted(() => {\n platform.value = detectPlatform()\n})\n\nconst shortcutTokens = computed(() => (props.shortcut ? parseHotkeys(props.shortcut) : []))\n\nuseHotkeyListener({\n tokens: () => shortcutTokens.value,\n platform: () => platform.value,\n enabled: () => !!props.shortcut,\n // While open, the focus is held inside the palette: the field being typed in is its own\n // search field, and the shortcut closes it again.\n allowInInput: () => open.value,\n onTrigger: () => (open.value ? close() : void show()),\n})\n\nconst triggerProps = computed<CommandPaletteTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n 'aria-keyshortcuts': props.shortcut\n ? ariaKeyshortcuts(shortcutTokens.value, platform.value)\n : undefined,\n}))\n\n/** A command, with its place among all of them: the same command may be listed twice. */\ntype Entry = { command: CommandPaletteCommand; source: number }\n\n// The blocks unwrapped and the separators dropped: this flat list is what the filter, the\n// keyboard and the option ids count through.\nconst allEntries = computed<Entry[]>(() =>\n props.items\n .flatMap((item) => {\n if (isSeparator(item)) return []\n return isGroup(item) ? item.commands : [item]\n })\n .map((command, source) => ({ command, source })),\n)\n\nconst normalizedOf = createNormalizedCache<CommandPaletteCommand>()\n\nfunction matches(command: CommandPaletteCommand, needle: string) {\n if (normalizedOf(command, command.label).includes(needle)) return true\n return (\n command.keywords?.some((word, i) =>\n normalizedOf(command, word, `keyword${i}`).includes(needle),\n ) ?? false\n )\n}\n\nconst term = computed(() => query.value.trim())\n\nconst filtered = computed(() => {\n const list = allEntries.value\n const matcher = props.filter\n if (matcher === false || !term.value) return list\n if (typeof matcher === 'function') return list.filter((e) => matcher(e.command, term.value))\n const needle = normalizeText(term.value)\n return list.filter((e) => matches(e.command, needle))\n})\n\nconst firstEnabled = () => filtered.value.findIndex((e) => !e.command.disabled)\n\n// @a11y\n// An option's id follows its place among ALL the commands, not its place in the filtered list:\n// a screen reader announces the active option when aria-activedescendant changes, and a\n// positional id would stay the same while the filter slides another command under it. Nor is\n// it keyed by the command object, which a block of recent commands lists a second time.\nconst optionId = (source: number) => `${listId}-${source}`\nconst activeId = computed(() => {\n const entry = filtered.value[activeIndex.value]\n return entry ? optionId(entry.source) : undefined\n})\n\ntype Row = {\n kind: 'command'\n key: string\n command: CommandPaletteCommand\n index: number\n source: number\n}\ntype Node =\n | Row\n | { kind: 'group'; key: string; label: string; labelId: string; rows: Row[] }\n | { kind: 'separator'; key: string }\n\nconst nodes = computed<Node[]>(() => {\n const kept = new Set(filtered.value.map((e) => e.source))\n let source = 0\n let next = 0\n // Walked in the order `allEntries` was built, so `source` names the same entry.\n const rowOf = (command: CommandPaletteCommand, key: string): Row | null => {\n const at = source++\n return kept.has(at) ? { kind: 'command', key, command, index: next++, source: at } : null\n }\n\n const result: Node[] = []\n // A separator is held back until something comes after it, and dropped when nothing came\n // before: one rule for the three ways a search can strand it.\n let pendingSeparator: string | null = null\n for (const [i, item] of props.items.entries()) {\n if (isSeparator(item)) {\n if (result.length > 0) pendingSeparator = `separator-${i}`\n continue\n }\n let node: Node | null\n if (isGroup(item)) {\n const rows = item.commands\n .map((command, j) => rowOf(command, `${i}.${j}`))\n .filter((row): row is Row => row !== null)\n node = rows.length\n ? {\n kind: 'group',\n key: `group-${i}`,\n label: item.label,\n labelId: `${listId}-group-${i}`,\n rows,\n }\n : null\n } else {\n node = rowOf(item, String(i))\n }\n if (!node) continue\n if (pendingSeparator !== null) {\n result.push({ kind: 'separator', key: pendingSeparator })\n pendingSeparator = null\n }\n result.push(node)\n }\n return result\n})\n\n// @a11y\n/** What is announced about the list: that nothing matches, or that something is loading. */\nconst stateAnnouncement = computed(() => {\n if (!open.value || filtered.value.length > 0) return ''\n return props.loading ? resolvedLoadingText.value : resolvedEmptyText.value\n})\n\nconst { request: emitSearch, cancel: cancelSearch } = useDebouncedSearch({\n delay: () => props.searchDebounce,\n active: () => open.value,\n report: (value) => emit('search', value),\n})\n\nwatch(open, (value) => {\n if (value) {\n activeIndex.value = firstEnabled()\n emitSearch(term.value, true)\n } else {\n cancelSearch()\n query.value = ''\n activeIndex.value = -1\n }\n})\n\nwatch(term, (value) => {\n if (!open.value) return\n activeIndex.value = firstEnabled()\n emitSearch(value)\n})\n\n// Commands arriving from a source replace the list without the term changing: a highlight that\n// fell out of it, or that never existed, moves to the first command, or Enter would do nothing.\nwatch(filtered, (list) => {\n if (!open.value) return\n if (activeIndex.value < 0 || activeIndex.value >= list.length) activeIndex.value = firstEnabled()\n})\n\n// @a11y\n// The focus never moves, so nothing scrolls the active row into view by itself.\nwatch(\n activeId,\n () => {\n if (!activeId.value) return\n // Optional call: jsdom implements no scrolling.\n document.getElementById(activeId.value)?.scrollIntoView?.({ block: 'nearest' })\n },\n { flush: 'post' },\n)\n\n/** What a row draws: the command's own fields, and none of the ones meant for the palette. */\nconst rowProps = ({\n label,\n description,\n icon,\n shortcut,\n href,\n disabled,\n}: CommandPaletteCommand) => ({\n label,\n description,\n icon,\n shortcut,\n href,\n disabled,\n})\n\nfunction hover(row: Row) {\n if (!row.command.disabled) activeIndex.value = row.index\n}\n\nfunction choose(command: CommandPaletteCommand, event: MouseEvent) {\n emit('select', command, event)\n // A link opened elsewhere leaves this page as it was, the palette included.\n const elsewhere = !!command.href && (event.ctrlKey || event.metaKey || event.shiftKey)\n if (!command.keepOpen && !elsewhere) open.value = false\n}\n\n// @keyboard\nfunction move(delta: number) {\n const list = filtered.value\n if (list.length === 0) return\n // From nothing highlighted, a step back lands on the last command rather than the one before.\n let i = activeIndex.value < 0 && delta < 0 ? 0 : activeIndex.value\n for (let step = 0; step < list.length; step++) {\n i = (i + delta + list.length) % list.length\n if (!list[i]?.command.disabled) break\n }\n activeIndex.value = i\n}\n\n// @keyboard\n// Home and End stay with the field, where they move the caret. Escape is the dialog's own\n// close request.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.isComposing) return\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n move(1)\n break\n case 'ArrowUp':\n event.preventDefault()\n move(-1)\n break\n case 'Enter': {\n if (!activeId.value) return\n event.preventDefault()\n // Clicking the row itself is what lets a link be followed natively.\n document.getElementById(activeId.value)?.click()\n break\n }\n }\n}\n\ndefineExpose({\n /**\n * Opens the palette, exactly as setting `open` does. The promise settles once the element has\n * been rendered.\n */\n show,\n /** Closes the palette. */\n close,\n /** Moves the focus to the search field, while the palette is open. */\n focus: (options?: FocusOptions) => inputEl.value?.focus(options),\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-label=\"resolvedLabel\"\n v-bind=\"rootAttrs\"\n class=\"v-command-palette\"\n :style=\"{ '--command-palette-width': cssSize(width) }\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <div class=\"v-command-palette-search\">\n <VIcon :name=\"searchIcon\" class=\"v-command-palette-search-icon\" />\n <!-- `autofocus` is what the native dialog focusing steps look for when it opens. -->\n <!-- eslint-disable vuejs-accessibility/no-autofocus -->\n <input\n ref=\"inputEl\"\n v-model=\"query\"\n class=\"v-command-palette-input\"\n type=\"text\"\n role=\"combobox\"\n autofocus\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n aria-expanded=\"true\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"resolvedSearchLabel\"\n :placeholder=\"resolvedPlaceholder\"\n @keydown=\"onKeydown\"\n />\n <!-- eslint-enable vuejs-accessibility/no-autofocus -->\n <VSpinner v-if=\"loading\" class=\"v-command-palette-spinner\" aria-hidden=\"true\" />\n </div>\n\n <!-- A press on a row would take the focus out of the field; it is cancelled so the click\n that follows still chooses, and the field keeps typing. -->\n <div\n :id=\"listId\"\n role=\"listbox\"\n :aria-label=\"resolvedLabel\"\n class=\"v-command-palette-list v-control\"\n data-size=\"md\"\n @mousedown.prevent\n >\n <template v-for=\"node in nodes\" :key=\"node.key\">\n <VSeparator\n v-if=\"node.kind === 'separator'\"\n role=\"presentation\"\n class=\"v-command-palette-separator\"\n />\n <div\n v-else-if=\"node.kind === 'group'\"\n role=\"group\"\n class=\"v-command-palette-group\"\n :aria-labelledby=\"node.labelId\"\n >\n <span :id=\"node.labelId\" class=\"v-command-palette-group-label\">{{ node.label }}</span>\n <VCommandPaletteItem\n v-for=\"row in node.rows\"\n :id=\"optionId(row.source)\"\n :key=\"row.key\"\n v-bind=\"rowProps(row.command)\"\n :active=\"row.index === activeIndex\"\n @select=\"(event) => choose(row.command, event)\"\n @pointermove=\"hover(row)\"\n >\n <template v-if=\"$slots.item\" #default>\n <slot name=\"item\" :command=\"row.command\" :active=\"row.index === activeIndex\" />\n </template>\n </VCommandPaletteItem>\n </div>\n <VCommandPaletteItem\n v-else\n :id=\"optionId(node.source)\"\n v-bind=\"rowProps(node.command)\"\n :active=\"node.index === activeIndex\"\n @select=\"(event) => choose(node.command, event)\"\n @pointermove=\"hover(node)\"\n >\n <template v-if=\"$slots.item\" #default>\n <slot name=\"item\" :command=\"node.command\" :active=\"node.index === activeIndex\" />\n </template>\n </VCommandPaletteItem>\n </template>\n\n <!-- Loading is checked before emptiness, so a list waiting for an answer never claims\n there is none. Both are spoken by the status below instead. -->\n <div\n v-if=\"loading && filtered.length === 0\"\n class=\"v-command-palette-state\"\n aria-hidden=\"true\"\n >\n <slot name=\"loading\">\n <VSpinner />\n <span>{{ resolvedLoadingText }}</span>\n </slot>\n </div>\n <div v-else-if=\"filtered.length === 0\" class=\"v-command-palette-state\" aria-hidden=\"true\">\n <slot name=\"empty\" :query=\"term\">\n <VEmptyState\n size=\"sm\"\n class=\"v-command-palette-empty\"\n :icon=\"searchOffIcon\"\n :title=\"resolvedEmptyText\"\n />\n </slot>\n </div>\n </div>\n\n <footer v-if=\"!hideFooter\" class=\"v-command-palette-footer\">\n <slot name=\"footer\">\n <!-- A reminder for the eye: the combobox already tells assistive technology how it\n works, and every cap would otherwise be read as \"Keyboard shortcut\". -->\n <span class=\"v-command-palette-hints\" aria-hidden=\"true\">\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"up\" />\n <VHotkeys keys=\"down\" />\n {{ m.commandPalette.navigate }}\n </span>\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"enter\" />\n {{ m.commandPalette.choose }}\n </span>\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"esc\" />\n {{ m.common.close }}\n </span>\n </span>\n </slot>\n </footer>\n\n <!-- Outside the listbox, which may hold only options and groups. -->\n <span class=\"v-visually-hidden\" role=\"status\">{{ stateAnnouncement }}</span>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-command-palette {\n /*\n * Declared on the element rather than as a fallback: a custom property inherits, and a\n * palette opened inside a wider dialog would otherwise take that dialog's width.\n */\n --command-palette-width: var(--vectis-control-size-command-palette-width);\n /*\n * Where a row's icon and a block's name start: the list padding plus the inline padding of\n * a medium row. The field and the footer use it to line up with them.\n */\n --command-palette-inset: calc(var(--vectis-space-2) + var(--vectis-space-4));\n inline-size: var(--command-palette-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(\n 100dvb - var(--vectis-control-size-command-palette-offset) - var(--vectis-space-4)\n );\n /*\n * Pinned near the top rather than centred: the list grows and shrinks with every keystroke,\n * and a centred palette would move the field being typed in.\n */\n margin: var(--vectis-control-size-command-palette-offset) auto auto;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /* The display above beats the browser's own rule hiding a closed dialog. */\n .v-command-palette:not([open]) {\n display: none;\n }\n\n .v-command-palette:focus-visible {\n outline: none;\n }\n\n .v-command-palette-search {\n flex: none;\n display: flex;\n align-items: center;\n /* The gap of a medium row, so the typed text starts where the labels do. */\n gap: var(--vectis-space-2);\n min-block-size: var(--vectis-control-height-xl);\n padding-inline: var(--command-palette-inset);\n border-block-end: 1px solid var(--vectis-color-border);\n /* The magnifier matches the larger text of the field, through VIcon's context size. */\n --vectis-icon-size: var(--vectis-icon-size-md);\n }\n\n /* The field draws no ring of its own inside the palette: its edge takes the focus colour. */\n .v-command-palette-search:has(.v-command-palette-input:focus-visible) {\n border-block-end-color: var(--vectis-focus-ring-color);\n }\n\n .v-command-palette-search-icon,\n .v-command-palette-spinner {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n .v-command-palette-input {\n flex: 1;\n min-inline-size: 0;\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: var(--vectis-text-body-lg-size);\n font-weight: var(--vectis-text-body-lg-weight);\n line-height: var(--vectis-text-body-lg-leading);\n outline: none;\n }\n\n .v-command-palette-input::placeholder {\n color: var(--vectis-color-text-subtle);\n opacity: 1;\n }\n\n /*\n * The zero minimum is what lets the list shrink inside a short viewport and scroll, a flex\n * item otherwise refusing to go below its content.\n */\n .v-command-palette-list {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n min-block-size: 0;\n max-block-size: var(--vectis-control-size-command-palette-list-max-block);\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: var(--vectis-space-2);\n scroll-padding-block: var(--vectis-space-2);\n }\n\n /* Blocks and rows refuse to shrink: the list is a bounded column that scrolls instead. */\n .v-command-palette-group {\n display: flex;\n flex: none;\n flex-direction: column;\n gap: var(--vectis-space-1);\n }\n\n .v-command-palette-list > .v-command-palette-item {\n flex: none;\n }\n\n .v-command-palette-group-label {\n display: flex;\n align-items: center;\n min-block-size: var(--control-height);\n padding-inline: var(--control-padding-inline);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n\n /* Compounded with `.v-separator`, whose own sheet sets the margin and may load after this one. */\n .v-separator.v-command-palette-separator {\n flex: none;\n margin-inline: calc(-1 * var(--vectis-space-2));\n }\n\n .v-command-palette-state {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: center;\n gap: var(--control-gap);\n min-block-size: var(--control-height);\n padding: var(--vectis-space-1) var(--control-padding-inline);\n font-size: var(--control-font-size);\n color: var(--vectis-color-text-muted);\n }\n\n /* `[data-size]` lifts the selector above VEmptyState's size rules, whose sheet may load after. */\n .v-command-palette-state > .v-command-palette-empty[data-size] {\n flex: 1;\n padding-block: var(--vectis-space-4);\n padding-inline: 0;\n }\n\n .v-command-palette-footer {\n flex: none;\n display: flex;\n align-items: center;\n padding: var(--vectis-space-2) var(--command-palette-inset);\n border-block-start: 1px solid var(--vectis-color-border);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n color: var(--vectis-color-text-muted);\n }\n\n /* Keys mean nothing without a keyboard; a custom `#footer` stays. A trackpad or mouse keeps it. */\n @media not (any-pointer: fine) {\n .v-command-palette-footer:has(> .v-command-palette-hints) {\n display: none;\n }\n }\n\n .v-command-palette-hints {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vectis-space-1) var(--vectis-space-4);\n }\n\n .v-command-palette-hint {\n display: inline-flex;\n align-items: center;\n gap: var(--vectis-space-1);\n }\n\n .v-command-palette {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-command-palette[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-command-palette::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-command-palette[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-command-palette,\n .v-command-palette::backdrop {\n transition: none;\n }\n }\n\n @media (forced-colors: active) {\n .v-command-palette {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
package/dist/components/VCommandPalette/VCommandPalette.vue_vue_type_script_setup_true_lang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VCommandPalette.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VCommandPalette/VCommandPalette.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * A native modal `<dialog>` holding a search field and the listbox it filters. The focus stays\n * in the field, which points at the active command through aria-activedescendant: no native\n * element pairs free text with a list the arrows walk through.\n */\n\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport VEmptyState from '../VEmptyState/VEmptyState.vue'\nimport VHotkeys from '../VHotkeys/VHotkeys.vue'\nimport {\n DEFAULT_PLATFORM,\n ariaKeyshortcuts,\n detectPlatform,\n parseHotkeys,\n} from '../VHotkeys/platform'\nimport type { HotkeysPlatform } from '../VHotkeys/platform'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { search as searchIcon } from '../VIcon/icons/search'\nimport { search_off as searchOffIcon } from '../VIcon/icons/search_off'\nimport type { IconSource } from '../VIcon/types'\nimport VSeparator from '../VSeparator/VSeparator.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VCommandPaletteItem from './VCommandPaletteItem.vue'\nimport { useHotkeyListener } from '../../composables/useHotkeyListener'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useDebouncedSearch } from '../../composables/useDebouncedSearch'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\nimport { createNormalizedCache, normalizeText } from '../../utils/text'\n\n/** One thing the palette can do. */\nexport interface CommandPaletteCommand {\n /** What it is called on screen, and what the search matches. */\n label: string\n /** A second line under the label. The search does not read it. */\n description?: string\n /** Other words the search matches without showing them: synonyms, a former name. */\n keywords?: string[]\n /** An icon before the label: an icon name, or an explicit render. */\n icon?: IconSource\n /**\n * A shortcut shown at the end of the row, written as for VHotkeys (`mod+shift+p`). The palette\n * displays it and does not listen for it.\n */\n shortcut?: string\n /**\n * Makes the command a link: choosing it follows the address, and a modified or middle click\n * opens it elsewhere, natively.\n */\n href?: string\n /** Shows the command without allowing it to be chosen. */\n disabled?: boolean\n /** Leaves the palette open once the command is chosen. */\n keepOpen?: boolean\n /** An identifier of your own. The palette does not read it; it comes back with `select`. */\n id?: string | number\n}\n\n/** A named block of commands. A block none of whose commands match is hidden, name included. */\nexport interface CommandPaletteGroup {\n label: string\n commands: CommandPaletteCommand[]\n}\n\n/** A rule between two blocks. One the search leaves at an end or against another is not drawn. */\nexport interface CommandPaletteSeparator {\n separator: true\n}\n\n/** Anything the list may hold: a command, a named block, or a separator. */\nexport type CommandPaletteItem =\n CommandPaletteCommand | CommandPaletteGroup | CommandPaletteSeparator\n\n/**\n * How the list is narrowed as one types: by the label and the keywords, not at all (when the\n * commands arrive already filtered by a server), or by a rule of your own.\n */\nexport type CommandPaletteFilter =\n boolean | ((command: CommandPaletteCommand, query: string) => boolean)\n\n/** What the `#item` slot receives. */\nexport interface CommandPaletteItemSlotProps {\n command: CommandPaletteCommand\n active: boolean\n}\n\n/** What the `#empty` slot receives. */\nexport interface CommandPaletteEmptySlotProps {\n query: string\n}\n\n/** What the trigger has to carry: the click that opens the palette, and the fact that it does. */\nexport type CommandPaletteTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n /** The `shortcut`, spelled for assistive technology, when there is one. */\n 'aria-keyshortcuts'?: string\n}\n\nconst isGroup = (item: CommandPaletteItem): item is CommandPaletteGroup => 'commands' in item\nconst isSeparator = (item: CommandPaletteItem): item is CommandPaletteSeparator =>\n 'separator' in item\n\ninterface CommandPaletteProps {\n /** What the palette offers: commands, named blocks of commands, and separators. */\n items: CommandPaletteItem[]\n /**\n * A shortcut that opens the palette from anywhere in the page and closes it again, written as\n * for VHotkeys: `mod+k` is ⌘K on a Mac and Ctrl+K elsewhere. Left out, nothing is listened for.\n */\n shortcut?: string\n /** How the list is narrowed as one types. */\n filter?: CommandPaletteFilter\n /**\n * How long to wait before reporting the search with `search`, in milliseconds. Zero reports\n * it at once.\n */\n searchDebounce?: number\n /** Says that commands are being loaded: a spinner shows in the search field. */\n loading?: boolean\n /** What is announced while loading with nothing to show yet. */\n loadingText?: string\n /** What the palette says when nothing matches. */\n emptyText?: string\n /** What the search field says while empty. */\n placeholder?: string\n /** The accessible name of the palette. */\n label?: string\n /** The accessible name of its search field. */\n searchLabel?: string\n /** Leaves out the footer listing the keys. */\n hideFooter?: boolean\n /**\n * How wide the palette is: a number is read as pixels, a string as any CSS length. Left out,\n * it takes the `--vectis-control-size-command-palette-width` token, 640px by default.\n */\n width?: number | string\n}\n\nconst props = withDefaults(defineProps<CommandPaletteProps>(), {\n shortcut: undefined,\n filter: true,\n searchDebounce: 250,\n loading: false,\n loadingText: undefined,\n emptyText: undefined,\n placeholder: undefined,\n label: undefined,\n searchLabel: undefined,\n hideFooter: false,\n width: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A command was chosen, by a click or by Enter. Call `event.preventDefault()` to stop a link\n * being followed, to hand its address to a router instead.\n */\n select: [command: CommandPaletteCommand, event: MouseEvent]\n /** What is being searched for, to be sent to the source. It is also reported on opening. */\n search: [query: string]\n}>()\n\ndefineSlots<{\n /**\n * The button that opens the palette. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the palette itself.\n */\n trigger?(props: { triggerProps: CommandPaletteTriggerProps }): unknown\n /** The label and description of a row. The icon and the shortcut stay in place. */\n item?(props: CommandPaletteItemSlotProps): unknown\n /** What the palette shows when nothing matches. It receives the term that was searched. */\n empty?(props: CommandPaletteEmptySlotProps): unknown\n /** What the palette shows while loading with nothing to show yet. */\n loading?(): unknown\n /** Replaces the footer listing the keys. */\n footer?(): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst m = useMessages()\nconst resolvedLabel = computed(() => props.label ?? m.value.commandPalette.label)\nconst resolvedSearchLabel = computed(() => props.searchLabel ?? m.value.commandPalette.searchLabel)\nconst resolvedPlaceholder = computed(() => props.placeholder ?? m.value.commandPalette.placeholder)\nconst resolvedEmptyText = computed(() => props.emptyText ?? m.value.commandPalette.empty)\nconst resolvedLoadingText = computed(() => props.loadingText ?? m.value.common.loading)\n\n/** Whether the palette is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n/** What is typed in the search field. It is emptied each time the palette closes. */\nconst query = defineModel<string>('query', { default: '' })\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, { persistentBackdrop: () => false, persistentEscape: () => false })\n\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst listId = useId()\nconst activeIndex = ref(-1)\n\n// @ssr\n// The server renders `mod` as Ctrl; the platform is read once mounted.\nconst platform = ref<HotkeysPlatform>(DEFAULT_PLATFORM)\nonMounted(() => {\n platform.value = detectPlatform()\n})\n\nconst shortcutTokens = computed(() => (props.shortcut ? parseHotkeys(props.shortcut) : []))\n\nuseHotkeyListener({\n tokens: () => shortcutTokens.value,\n platform: () => platform.value,\n enabled: () => !!props.shortcut,\n // While open, the focus is held inside the palette: the field being typed in is its own\n // search field, and the shortcut closes it again.\n allowInInput: () => open.value,\n onTrigger: () => (open.value ? close() : void show()),\n})\n\nconst triggerProps = computed<CommandPaletteTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n 'aria-keyshortcuts': props.shortcut\n ? ariaKeyshortcuts(shortcutTokens.value, platform.value)\n : undefined,\n}))\n\n/** A command, with its place among all of them: the same command may be listed twice. */\ntype Entry = { command: CommandPaletteCommand; source: number }\n\n// The blocks unwrapped and the separators dropped: this flat list is what the filter, the\n// keyboard and the option ids count through.\nconst allEntries = computed<Entry[]>(() =>\n props.items\n .flatMap((item) => {\n if (isSeparator(item)) return []\n return isGroup(item) ? item.commands : [item]\n })\n .map((command, source) => ({ command, source })),\n)\n\nconst normalizedOf = createNormalizedCache<CommandPaletteCommand>()\n\nfunction matches(command: CommandPaletteCommand, needle: string) {\n if (normalizedOf(command, command.label).includes(needle)) return true\n return (\n command.keywords?.some((word, i) =>\n normalizedOf(command, word, `keyword${i}`).includes(needle),\n ) ?? false\n )\n}\n\nconst term = computed(() => query.value.trim())\n\nconst filtered = computed(() => {\n const list = allEntries.value\n const matcher = props.filter\n if (matcher === false || !term.value) return list\n if (typeof matcher === 'function') return list.filter((e) => matcher(e.command, term.value))\n const needle = normalizeText(term.value)\n return list.filter((e) => matches(e.command, needle))\n})\n\nconst firstEnabled = () => filtered.value.findIndex((e) => !e.command.disabled)\n\n// @a11y\n// An option's id follows its place among ALL the commands, not its place in the filtered list:\n// a screen reader announces the active option when aria-activedescendant changes, and a\n// positional id would stay the same while the filter slides another command under it. Nor is\n// it keyed by the command object, which a block of recent commands lists a second time.\nconst optionId = (source: number) => `${listId}-${source}`\nconst activeId = computed(() => {\n const entry = filtered.value[activeIndex.value]\n return entry ? optionId(entry.source) : undefined\n})\n\ntype Row = {\n kind: 'command'\n key: string\n command: CommandPaletteCommand\n index: number\n source: number\n}\ntype Node =\n | Row\n | { kind: 'group'; key: string; label: string; labelId: string; rows: Row[] }\n | { kind: 'separator'; key: string }\n\nconst nodes = computed<Node[]>(() => {\n const kept = new Set(filtered.value.map((e) => e.source))\n let source = 0\n let next = 0\n // Walked in the order `allEntries` was built, so `source` names the same entry.\n const rowOf = (command: CommandPaletteCommand, key: string): Row | null => {\n const at = source++\n return kept.has(at) ? { kind: 'command', key, command, index: next++, source: at } : null\n }\n\n const result: Node[] = []\n // A separator is held back until something comes after it, and dropped when nothing came\n // before: one rule for the three ways a search can strand it.\n let pendingSeparator: string | null = null\n for (const [i, item] of props.items.entries()) {\n if (isSeparator(item)) {\n if (result.length > 0) pendingSeparator = `separator-${i}`\n continue\n }\n let node: Node | null\n if (isGroup(item)) {\n const rows = item.commands\n .map((command, j) => rowOf(command, `${i}.${j}`))\n .filter((row): row is Row => row !== null)\n node = rows.length\n ? {\n kind: 'group',\n key: `group-${i}`,\n label: item.label,\n labelId: `${listId}-group-${i}`,\n rows,\n }\n : null\n } else {\n node = rowOf(item, String(i))\n }\n if (!node) continue\n if (pendingSeparator !== null) {\n result.push({ kind: 'separator', key: pendingSeparator })\n pendingSeparator = null\n }\n result.push(node)\n }\n return result\n})\n\n// @a11y\n/** What is announced about the list: that nothing matches, or that something is loading. */\nconst stateAnnouncement = computed(() => {\n if (!open.value || filtered.value.length > 0) return ''\n return props.loading ? resolvedLoadingText.value : resolvedEmptyText.value\n})\n\nconst { request: emitSearch, cancel: cancelSearch } = useDebouncedSearch({\n delay: () => props.searchDebounce,\n active: () => open.value,\n report: (value) => emit('search', value),\n})\n\nwatch(open, (value) => {\n if (value) {\n activeIndex.value = firstEnabled()\n emitSearch(term.value, true)\n } else {\n cancelSearch()\n query.value = ''\n activeIndex.value = -1\n }\n})\n\nwatch(term, (value) => {\n if (!open.value) return\n activeIndex.value = firstEnabled()\n emitSearch(value)\n})\n\n// Commands arriving from a source replace the list without the term changing: a highlight that\n// fell out of it, or that never existed, moves to the first command, or Enter would do nothing.\nwatch(filtered, (list) => {\n if (!open.value) return\n if (activeIndex.value < 0 || activeIndex.value >= list.length) activeIndex.value = firstEnabled()\n})\n\n// @a11y\n// The focus never moves, so nothing scrolls the active row into view by itself.\nwatch(\n activeId,\n () => {\n if (!activeId.value) return\n // Optional call: jsdom implements no scrolling.\n document.getElementById(activeId.value)?.scrollIntoView?.({ block: 'nearest' })\n },\n { flush: 'post' },\n)\n\n/** What a row draws: the command's own fields, and none of the ones meant for the palette. */\nconst rowProps = ({\n label,\n description,\n icon,\n shortcut,\n href,\n disabled,\n}: CommandPaletteCommand) => ({\n label,\n description,\n icon,\n shortcut,\n href,\n disabled,\n})\n\nfunction hover(row: Row) {\n if (!row.command.disabled) activeIndex.value = row.index\n}\n\nfunction choose(command: CommandPaletteCommand, event: MouseEvent) {\n emit('select', command, event)\n // A link opened elsewhere leaves this page as it was, the palette included.\n const elsewhere = !!command.href && (event.ctrlKey || event.metaKey || event.shiftKey)\n if (!command.keepOpen && !elsewhere) open.value = false\n}\n\n// @keyboard\nfunction move(delta: number) {\n const list = filtered.value\n if (list.length === 0) return\n // From nothing highlighted, a step back lands on the last command rather than the one before.\n let i = activeIndex.value < 0 && delta < 0 ? 0 : activeIndex.value\n for (let step = 0; step < list.length; step++) {\n i = (i + delta + list.length) % list.length\n if (!list[i]?.command.disabled) break\n }\n activeIndex.value = i\n}\n\n// @keyboard\n// Home and End stay with the field, where they move the caret. Escape is the dialog's own\n// close request.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.isComposing) return\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n move(1)\n break\n case 'ArrowUp':\n event.preventDefault()\n move(-1)\n break\n case 'Enter': {\n if (!activeId.value) return\n event.preventDefault()\n // Clicking the row itself is what lets a link be followed natively.\n document.getElementById(activeId.value)?.click()\n break\n }\n }\n}\n\ndefineExpose({\n /**\n * Opens the palette, exactly as setting `open` does. The promise settles once the element has\n * been rendered.\n */\n show,\n /** Closes the palette. */\n close,\n /** Moves the focus to the search field, while the palette is open. */\n focus: (options?: FocusOptions) => inputEl.value?.focus(options),\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-label=\"resolvedLabel\"\n v-bind=\"rootAttrs\"\n class=\"v-command-palette\"\n :style=\"{ '--command-palette-width': cssSize(width) }\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <div class=\"v-command-palette-search\">\n <VIcon :name=\"searchIcon\" class=\"v-command-palette-search-icon\" />\n <!-- `autofocus` is what the native dialog focusing steps look for when it opens. -->\n <!-- eslint-disable vuejs-accessibility/no-autofocus -->\n <input\n ref=\"inputEl\"\n v-model=\"query\"\n class=\"v-command-palette-input\"\n type=\"text\"\n role=\"combobox\"\n autofocus\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n aria-expanded=\"true\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"resolvedSearchLabel\"\n :placeholder=\"resolvedPlaceholder\"\n @keydown=\"onKeydown\"\n />\n <!-- eslint-enable vuejs-accessibility/no-autofocus -->\n <VSpinner v-if=\"loading\" class=\"v-command-palette-spinner\" aria-hidden=\"true\" />\n </div>\n\n <!-- A press on a row would take the focus out of the field; it is cancelled so the click\n that follows still chooses, and the field keeps typing. -->\n <div\n :id=\"listId\"\n role=\"listbox\"\n :aria-label=\"resolvedLabel\"\n class=\"v-command-palette-list v-control\"\n data-size=\"md\"\n @mousedown.prevent\n >\n <template v-for=\"node in nodes\" :key=\"node.key\">\n <VSeparator\n v-if=\"node.kind === 'separator'\"\n role=\"presentation\"\n class=\"v-command-palette-separator\"\n />\n <div\n v-else-if=\"node.kind === 'group'\"\n role=\"group\"\n class=\"v-command-palette-group\"\n :aria-labelledby=\"node.labelId\"\n >\n <span :id=\"node.labelId\" class=\"v-command-palette-group-label\">{{ node.label }}</span>\n <VCommandPaletteItem\n v-for=\"row in node.rows\"\n :id=\"optionId(row.source)\"\n :key=\"row.key\"\n v-bind=\"rowProps(row.command)\"\n :active=\"row.index === activeIndex\"\n @select=\"(event) => choose(row.command, event)\"\n @pointermove=\"hover(row)\"\n >\n <template v-if=\"$slots.item\" #default>\n <slot name=\"item\" :command=\"row.command\" :active=\"row.index === activeIndex\" />\n </template>\n </VCommandPaletteItem>\n </div>\n <VCommandPaletteItem\n v-else\n :id=\"optionId(node.source)\"\n v-bind=\"rowProps(node.command)\"\n :active=\"node.index === activeIndex\"\n @select=\"(event) => choose(node.command, event)\"\n @pointermove=\"hover(node)\"\n >\n <template v-if=\"$slots.item\" #default>\n <slot name=\"item\" :command=\"node.command\" :active=\"node.index === activeIndex\" />\n </template>\n </VCommandPaletteItem>\n </template>\n\n <!-- Loading is checked before emptiness, so a list waiting for an answer never claims\n there is none. Both are spoken by the status below instead. -->\n <div\n v-if=\"loading && filtered.length === 0\"\n class=\"v-command-palette-state\"\n aria-hidden=\"true\"\n >\n <slot name=\"loading\">\n <VSpinner />\n <span>{{ resolvedLoadingText }}</span>\n </slot>\n </div>\n <div v-else-if=\"filtered.length === 0\" class=\"v-command-palette-state\" aria-hidden=\"true\">\n <slot name=\"empty\" :query=\"term\">\n <VEmptyState\n size=\"sm\"\n class=\"v-command-palette-empty\"\n :icon=\"searchOffIcon\"\n :title=\"resolvedEmptyText\"\n />\n </slot>\n </div>\n </div>\n\n <footer v-if=\"!hideFooter\" class=\"v-command-palette-footer\">\n <slot name=\"footer\">\n <!-- A reminder for the eye: the combobox already tells assistive technology how it\n works, and every cap would otherwise be read as \"Keyboard shortcut\". -->\n <span class=\"v-command-palette-hints\" aria-hidden=\"true\">\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"up\" />\n <VHotkeys keys=\"down\" />\n {{ m.commandPalette.navigate }}\n </span>\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"enter\" />\n {{ m.commandPalette.choose }}\n </span>\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"esc\" />\n {{ m.common.close }}\n </span>\n </span>\n </slot>\n </footer>\n\n <!-- Outside the listbox, which may hold only options and groups. -->\n <span class=\"v-visually-hidden\" role=\"status\">{{ stateAnnouncement }}</span>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-command-palette {\n /*\n * Declared on the element rather than as a fallback: a custom property inherits, and a\n * palette opened inside a wider dialog would otherwise take that dialog's width.\n */\n --command-palette-width: var(--vectis-control-size-command-palette-width);\n /*\n * Where a row's icon and a block's name start: the list padding plus the inline padding of\n * a medium row. The field and the footer use it to line up with them.\n */\n --command-palette-inset: calc(var(--vectis-space-2) + var(--vectis-space-4));\n inline-size: var(--command-palette-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(\n 100dvb - var(--vectis-control-size-command-palette-offset) - var(--vectis-space-4)\n );\n /*\n * Pinned near the top rather than centred: the list grows and shrinks with every keystroke,\n * and a centred palette would move the field being typed in.\n */\n margin: var(--vectis-control-size-command-palette-offset) auto auto;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /* The display above beats the browser's own rule hiding a closed dialog. */\n .v-command-palette:not([open]) {\n display: none;\n }\n\n .v-command-palette:focus-visible {\n outline: none;\n }\n\n .v-command-palette-search {\n flex: none;\n display: flex;\n align-items: center;\n /* The gap of a medium row, so the typed text starts where the labels do. */\n gap: var(--vectis-space-2);\n min-block-size: var(--vectis-control-height-xl);\n padding-inline: var(--command-palette-inset);\n border-block-end: 1px solid var(--vectis-color-border);\n /* The magnifier matches the larger text of the field, through VIcon's context size. */\n --vectis-icon-size: var(--vectis-icon-size-md);\n }\n\n /* The field draws no ring of its own inside the palette: its edge takes the focus colour. */\n .v-command-palette-search:has(.v-command-palette-input:focus-visible) {\n border-block-end-color: var(--vectis-focus-ring-color);\n }\n\n .v-command-palette-search-icon,\n .v-command-palette-spinner {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n .v-command-palette-input {\n flex: 1;\n min-inline-size: 0;\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: var(--vectis-text-body-lg-size);\n font-weight: var(--vectis-text-body-lg-weight);\n line-height: var(--vectis-text-body-lg-leading);\n outline: none;\n }\n\n .v-command-palette-input::placeholder {\n color: var(--vectis-color-text-subtle);\n opacity: 1;\n }\n\n /*\n * The zero minimum is what lets the list shrink inside a short viewport and scroll, a flex\n * item otherwise refusing to go below its content.\n */\n .v-command-palette-list {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n min-block-size: 0;\n max-block-size: var(--vectis-control-size-command-palette-list-max-block);\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: var(--vectis-space-2);\n scroll-padding-block: var(--vectis-space-2);\n }\n\n /* Blocks and rows refuse to shrink: the list is a bounded column that scrolls instead. */\n .v-command-palette-group {\n display: flex;\n flex: none;\n flex-direction: column;\n gap: var(--vectis-space-1);\n }\n\n .v-command-palette-list > .v-command-palette-item {\n flex: none;\n }\n\n .v-command-palette-group-label {\n display: flex;\n align-items: center;\n min-block-size: var(--control-height);\n padding-inline: var(--control-padding-inline);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n\n /* Compounded with `.v-separator`, whose own sheet sets the margin and may load after this one. */\n .v-separator.v-command-palette-separator {\n flex: none;\n margin-inline: calc(-1 * var(--vectis-space-2));\n }\n\n .v-command-palette-state {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: center;\n gap: var(--control-gap);\n min-block-size: var(--control-height);\n padding: var(--vectis-space-1) var(--control-padding-inline);\n font-size: var(--control-font-size);\n color: var(--vectis-color-text-muted);\n }\n\n /* `[data-size]` lifts the selector above VEmptyState's size rules, whose sheet may load after. */\n .v-command-palette-state > .v-command-palette-empty[data-size] {\n flex: 1;\n padding-block: var(--vectis-space-4);\n padding-inline: 0;\n }\n\n .v-command-palette-footer {\n flex: none;\n display: flex;\n align-items: center;\n padding: var(--vectis-space-2) var(--command-palette-inset);\n border-block-start: 1px solid var(--vectis-color-border);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n color: var(--vectis-color-text-muted);\n }\n\n .v-command-palette-hints {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vectis-space-1) var(--vectis-space-4);\n }\n\n .v-command-palette-hint {\n display: inline-flex;\n align-items: center;\n gap: var(--vectis-space-1);\n }\n\n .v-command-palette {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-command-palette[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-command-palette::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-command-palette[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-command-palette,\n .v-command-palette::backdrop {\n transition: none;\n }\n }\n\n @media (forced-colors: active) {\n .v-command-palette {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VCommandPalette.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VCommandPalette/VCommandPalette.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * A native modal `<dialog>` holding a search field and the listbox it filters. The focus stays\n * in the field, which points at the active command through aria-activedescendant: no native\n * element pairs free text with a list the arrows walk through.\n */\n\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport VEmptyState from '../VEmptyState/VEmptyState.vue'\nimport VHotkeys from '../VHotkeys/VHotkeys.vue'\nimport {\n DEFAULT_PLATFORM,\n ariaKeyshortcuts,\n detectPlatform,\n parseHotkeys,\n} from '../VHotkeys/platform'\nimport type { HotkeysPlatform } from '../VHotkeys/platform'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { search as searchIcon } from '../VIcon/icons/search'\nimport { search_off as searchOffIcon } from '../VIcon/icons/search_off'\nimport type { IconSource } from '../VIcon/types'\nimport VSeparator from '../VSeparator/VSeparator.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VCommandPaletteItem from './VCommandPaletteItem.vue'\nimport { useHotkeyListener } from '../../composables/useHotkeyListener'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useDebouncedSearch } from '../../composables/useDebouncedSearch'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\nimport { createNormalizedCache, normalizeText } from '../../utils/text'\n\n/** One thing the palette can do. */\nexport interface CommandPaletteCommand {\n /** What it is called on screen, and what the search matches. */\n label: string\n /** A second line under the label. The search does not read it. */\n description?: string\n /** Other words the search matches without showing them: synonyms, a former name. */\n keywords?: string[]\n /** An icon before the label: an icon name, or an explicit render. */\n icon?: IconSource\n /**\n * A shortcut shown at the end of the row, written as for VHotkeys (`mod+shift+p`). The palette\n * displays it and does not listen for it.\n */\n shortcut?: string\n /**\n * Makes the command a link: choosing it follows the address, and a modified or middle click\n * opens it elsewhere, natively.\n */\n href?: string\n /** Shows the command without allowing it to be chosen. */\n disabled?: boolean\n /** Leaves the palette open once the command is chosen. */\n keepOpen?: boolean\n /** An identifier of your own. The palette does not read it; it comes back with `select`. */\n id?: string | number\n}\n\n/** A named block of commands. A block none of whose commands match is hidden, name included. */\nexport interface CommandPaletteGroup {\n label: string\n commands: CommandPaletteCommand[]\n}\n\n/** A rule between two blocks. One the search leaves at an end or against another is not drawn. */\nexport interface CommandPaletteSeparator {\n separator: true\n}\n\n/** Anything the list may hold: a command, a named block, or a separator. */\nexport type CommandPaletteItem =\n CommandPaletteCommand | CommandPaletteGroup | CommandPaletteSeparator\n\n/**\n * How the list is narrowed as one types: by the label and the keywords, not at all (when the\n * commands arrive already filtered by a server), or by a rule of your own.\n */\nexport type CommandPaletteFilter =\n boolean | ((command: CommandPaletteCommand, query: string) => boolean)\n\n/** What the `#item` slot receives. */\nexport interface CommandPaletteItemSlotProps {\n command: CommandPaletteCommand\n active: boolean\n}\n\n/** What the `#empty` slot receives. */\nexport interface CommandPaletteEmptySlotProps {\n query: string\n}\n\n/** What the trigger has to carry: the click that opens the palette, and the fact that it does. */\nexport type CommandPaletteTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n /** The `shortcut`, spelled for assistive technology, when there is one. */\n 'aria-keyshortcuts'?: string\n}\n\nconst isGroup = (item: CommandPaletteItem): item is CommandPaletteGroup => 'commands' in item\nconst isSeparator = (item: CommandPaletteItem): item is CommandPaletteSeparator =>\n 'separator' in item\n\ninterface CommandPaletteProps {\n /** What the palette offers: commands, named blocks of commands, and separators. */\n items: CommandPaletteItem[]\n /**\n * A shortcut that opens the palette from anywhere in the page and closes it again, written as\n * for VHotkeys: `mod+k` is ⌘K on a Mac and Ctrl+K elsewhere. Left out, nothing is listened for.\n */\n shortcut?: string\n /** How the list is narrowed as one types. */\n filter?: CommandPaletteFilter\n /**\n * How long to wait before reporting the search with `search`, in milliseconds. Zero reports\n * it at once.\n */\n searchDebounce?: number\n /** Says that commands are being loaded: a spinner shows in the search field. */\n loading?: boolean\n /** What is announced while loading with nothing to show yet. */\n loadingText?: string\n /** What the palette says when nothing matches. */\n emptyText?: string\n /** What the search field says while empty. */\n placeholder?: string\n /** The accessible name of the palette. */\n label?: string\n /** The accessible name of its search field. */\n searchLabel?: string\n /** Leaves out the footer listing the keys. */\n hideFooter?: boolean\n /**\n * How wide the palette is: a number is read as pixels, a string as any CSS length. Left out,\n * it takes the `--vectis-control-size-command-palette-width` token, 640px by default.\n */\n width?: number | string\n}\n\nconst props = withDefaults(defineProps<CommandPaletteProps>(), {\n shortcut: undefined,\n filter: true,\n searchDebounce: 250,\n loading: false,\n loadingText: undefined,\n emptyText: undefined,\n placeholder: undefined,\n label: undefined,\n searchLabel: undefined,\n hideFooter: false,\n width: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A command was chosen, by a click or by Enter. Call `event.preventDefault()` to stop a link\n * being followed, to hand its address to a router instead.\n */\n select: [command: CommandPaletteCommand, event: MouseEvent]\n /** What is being searched for, to be sent to the source. It is also reported on opening. */\n search: [query: string]\n}>()\n\ndefineSlots<{\n /**\n * The button that opens the palette. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the palette itself.\n */\n trigger?(props: { triggerProps: CommandPaletteTriggerProps }): unknown\n /** The label and description of a row. The icon and the shortcut stay in place. */\n item?(props: CommandPaletteItemSlotProps): unknown\n /** What the palette shows when nothing matches. It receives the term that was searched. */\n empty?(props: CommandPaletteEmptySlotProps): unknown\n /** What the palette shows while loading with nothing to show yet. */\n loading?(): unknown\n /** Replaces the footer listing the keys. */\n footer?(): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst m = useMessages()\nconst resolvedLabel = computed(() => props.label ?? m.value.commandPalette.label)\nconst resolvedSearchLabel = computed(() => props.searchLabel ?? m.value.commandPalette.searchLabel)\nconst resolvedPlaceholder = computed(() => props.placeholder ?? m.value.commandPalette.placeholder)\nconst resolvedEmptyText = computed(() => props.emptyText ?? m.value.commandPalette.empty)\nconst resolvedLoadingText = computed(() => props.loadingText ?? m.value.common.loading)\n\n/** Whether the palette is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n/** What is typed in the search field. It is emptied each time the palette closes. */\nconst query = defineModel<string>('query', { default: '' })\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, { persistentBackdrop: () => false, persistentEscape: () => false })\n\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst listId = useId()\nconst activeIndex = ref(-1)\n\n// @ssr\n// The server renders `mod` as Ctrl; the platform is read once mounted.\nconst platform = ref<HotkeysPlatform>(DEFAULT_PLATFORM)\nonMounted(() => {\n platform.value = detectPlatform()\n})\n\nconst shortcutTokens = computed(() => (props.shortcut ? parseHotkeys(props.shortcut) : []))\n\nuseHotkeyListener({\n tokens: () => shortcutTokens.value,\n platform: () => platform.value,\n enabled: () => !!props.shortcut,\n // While open, the focus is held inside the palette: the field being typed in is its own\n // search field, and the shortcut closes it again.\n allowInInput: () => open.value,\n onTrigger: () => (open.value ? close() : void show()),\n})\n\nconst triggerProps = computed<CommandPaletteTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n 'aria-keyshortcuts': props.shortcut\n ? ariaKeyshortcuts(shortcutTokens.value, platform.value)\n : undefined,\n}))\n\n/** A command, with its place among all of them: the same command may be listed twice. */\ntype Entry = { command: CommandPaletteCommand; source: number }\n\n// The blocks unwrapped and the separators dropped: this flat list is what the filter, the\n// keyboard and the option ids count through.\nconst allEntries = computed<Entry[]>(() =>\n props.items\n .flatMap((item) => {\n if (isSeparator(item)) return []\n return isGroup(item) ? item.commands : [item]\n })\n .map((command, source) => ({ command, source })),\n)\n\nconst normalizedOf = createNormalizedCache<CommandPaletteCommand>()\n\nfunction matches(command: CommandPaletteCommand, needle: string) {\n if (normalizedOf(command, command.label).includes(needle)) return true\n return (\n command.keywords?.some((word, i) =>\n normalizedOf(command, word, `keyword${i}`).includes(needle),\n ) ?? false\n )\n}\n\nconst term = computed(() => query.value.trim())\n\nconst filtered = computed(() => {\n const list = allEntries.value\n const matcher = props.filter\n if (matcher === false || !term.value) return list\n if (typeof matcher === 'function') return list.filter((e) => matcher(e.command, term.value))\n const needle = normalizeText(term.value)\n return list.filter((e) => matches(e.command, needle))\n})\n\nconst firstEnabled = () => filtered.value.findIndex((e) => !e.command.disabled)\n\n// @a11y\n// An option's id follows its place among ALL the commands, not its place in the filtered list:\n// a screen reader announces the active option when aria-activedescendant changes, and a\n// positional id would stay the same while the filter slides another command under it. Nor is\n// it keyed by the command object, which a block of recent commands lists a second time.\nconst optionId = (source: number) => `${listId}-${source}`\nconst activeId = computed(() => {\n const entry = filtered.value[activeIndex.value]\n return entry ? optionId(entry.source) : undefined\n})\n\ntype Row = {\n kind: 'command'\n key: string\n command: CommandPaletteCommand\n index: number\n source: number\n}\ntype Node =\n | Row\n | { kind: 'group'; key: string; label: string; labelId: string; rows: Row[] }\n | { kind: 'separator'; key: string }\n\nconst nodes = computed<Node[]>(() => {\n const kept = new Set(filtered.value.map((e) => e.source))\n let source = 0\n let next = 0\n // Walked in the order `allEntries` was built, so `source` names the same entry.\n const rowOf = (command: CommandPaletteCommand, key: string): Row | null => {\n const at = source++\n return kept.has(at) ? { kind: 'command', key, command, index: next++, source: at } : null\n }\n\n const result: Node[] = []\n // A separator is held back until something comes after it, and dropped when nothing came\n // before: one rule for the three ways a search can strand it.\n let pendingSeparator: string | null = null\n for (const [i, item] of props.items.entries()) {\n if (isSeparator(item)) {\n if (result.length > 0) pendingSeparator = `separator-${i}`\n continue\n }\n let node: Node | null\n if (isGroup(item)) {\n const rows = item.commands\n .map((command, j) => rowOf(command, `${i}.${j}`))\n .filter((row): row is Row => row !== null)\n node = rows.length\n ? {\n kind: 'group',\n key: `group-${i}`,\n label: item.label,\n labelId: `${listId}-group-${i}`,\n rows,\n }\n : null\n } else {\n node = rowOf(item, String(i))\n }\n if (!node) continue\n if (pendingSeparator !== null) {\n result.push({ kind: 'separator', key: pendingSeparator })\n pendingSeparator = null\n }\n result.push(node)\n }\n return result\n})\n\n// @a11y\n/** What is announced about the list: that nothing matches, or that something is loading. */\nconst stateAnnouncement = computed(() => {\n if (!open.value || filtered.value.length > 0) return ''\n return props.loading ? resolvedLoadingText.value : resolvedEmptyText.value\n})\n\nconst { request: emitSearch, cancel: cancelSearch } = useDebouncedSearch({\n delay: () => props.searchDebounce,\n active: () => open.value,\n report: (value) => emit('search', value),\n})\n\nwatch(open, (value) => {\n if (value) {\n activeIndex.value = firstEnabled()\n emitSearch(term.value, true)\n } else {\n cancelSearch()\n query.value = ''\n activeIndex.value = -1\n }\n})\n\nwatch(term, (value) => {\n if (!open.value) return\n activeIndex.value = firstEnabled()\n emitSearch(value)\n})\n\n// Commands arriving from a source replace the list without the term changing: a highlight that\n// fell out of it, or that never existed, moves to the first command, or Enter would do nothing.\nwatch(filtered, (list) => {\n if (!open.value) return\n if (activeIndex.value < 0 || activeIndex.value >= list.length) activeIndex.value = firstEnabled()\n})\n\n// @a11y\n// The focus never moves, so nothing scrolls the active row into view by itself.\nwatch(\n activeId,\n () => {\n if (!activeId.value) return\n // Optional call: jsdom implements no scrolling.\n document.getElementById(activeId.value)?.scrollIntoView?.({ block: 'nearest' })\n },\n { flush: 'post' },\n)\n\n/** What a row draws: the command's own fields, and none of the ones meant for the palette. */\nconst rowProps = ({\n label,\n description,\n icon,\n shortcut,\n href,\n disabled,\n}: CommandPaletteCommand) => ({\n label,\n description,\n icon,\n shortcut,\n href,\n disabled,\n})\n\nfunction hover(row: Row) {\n if (!row.command.disabled) activeIndex.value = row.index\n}\n\nfunction choose(command: CommandPaletteCommand, event: MouseEvent) {\n emit('select', command, event)\n // A link opened elsewhere leaves this page as it was, the palette included.\n const elsewhere = !!command.href && (event.ctrlKey || event.metaKey || event.shiftKey)\n if (!command.keepOpen && !elsewhere) open.value = false\n}\n\n// @keyboard\nfunction move(delta: number) {\n const list = filtered.value\n if (list.length === 0) return\n // From nothing highlighted, a step back lands on the last command rather than the one before.\n let i = activeIndex.value < 0 && delta < 0 ? 0 : activeIndex.value\n for (let step = 0; step < list.length; step++) {\n i = (i + delta + list.length) % list.length\n if (!list[i]?.command.disabled) break\n }\n activeIndex.value = i\n}\n\n// @keyboard\n// Home and End stay with the field, where they move the caret. Escape is the dialog's own\n// close request.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.isComposing) return\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n move(1)\n break\n case 'ArrowUp':\n event.preventDefault()\n move(-1)\n break\n case 'Enter': {\n if (!activeId.value) return\n event.preventDefault()\n // Clicking the row itself is what lets a link be followed natively.\n document.getElementById(activeId.value)?.click()\n break\n }\n }\n}\n\ndefineExpose({\n /**\n * Opens the palette, exactly as setting `open` does. The promise settles once the element has\n * been rendered.\n */\n show,\n /** Closes the palette. */\n close,\n /** Moves the focus to the search field, while the palette is open. */\n focus: (options?: FocusOptions) => inputEl.value?.focus(options),\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-label=\"resolvedLabel\"\n v-bind=\"rootAttrs\"\n class=\"v-command-palette\"\n :style=\"{ '--command-palette-width': cssSize(width) }\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <div class=\"v-command-palette-search\">\n <VIcon :name=\"searchIcon\" class=\"v-command-palette-search-icon\" />\n <!-- `autofocus` is what the native dialog focusing steps look for when it opens. -->\n <!-- eslint-disable vuejs-accessibility/no-autofocus -->\n <input\n ref=\"inputEl\"\n v-model=\"query\"\n class=\"v-command-palette-input\"\n type=\"text\"\n role=\"combobox\"\n autofocus\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n aria-expanded=\"true\"\n :aria-controls=\"listId\"\n :aria-activedescendant=\"activeId\"\n :aria-label=\"resolvedSearchLabel\"\n :placeholder=\"resolvedPlaceholder\"\n @keydown=\"onKeydown\"\n />\n <!-- eslint-enable vuejs-accessibility/no-autofocus -->\n <VSpinner v-if=\"loading\" class=\"v-command-palette-spinner\" aria-hidden=\"true\" />\n </div>\n\n <!-- A press on a row would take the focus out of the field; it is cancelled so the click\n that follows still chooses, and the field keeps typing. -->\n <div\n :id=\"listId\"\n role=\"listbox\"\n :aria-label=\"resolvedLabel\"\n class=\"v-command-palette-list v-control\"\n data-size=\"md\"\n @mousedown.prevent\n >\n <template v-for=\"node in nodes\" :key=\"node.key\">\n <VSeparator\n v-if=\"node.kind === 'separator'\"\n role=\"presentation\"\n class=\"v-command-palette-separator\"\n />\n <div\n v-else-if=\"node.kind === 'group'\"\n role=\"group\"\n class=\"v-command-palette-group\"\n :aria-labelledby=\"node.labelId\"\n >\n <span :id=\"node.labelId\" class=\"v-command-palette-group-label\">{{ node.label }}</span>\n <VCommandPaletteItem\n v-for=\"row in node.rows\"\n :id=\"optionId(row.source)\"\n :key=\"row.key\"\n v-bind=\"rowProps(row.command)\"\n :active=\"row.index === activeIndex\"\n @select=\"(event) => choose(row.command, event)\"\n @pointermove=\"hover(row)\"\n >\n <template v-if=\"$slots.item\" #default>\n <slot name=\"item\" :command=\"row.command\" :active=\"row.index === activeIndex\" />\n </template>\n </VCommandPaletteItem>\n </div>\n <VCommandPaletteItem\n v-else\n :id=\"optionId(node.source)\"\n v-bind=\"rowProps(node.command)\"\n :active=\"node.index === activeIndex\"\n @select=\"(event) => choose(node.command, event)\"\n @pointermove=\"hover(node)\"\n >\n <template v-if=\"$slots.item\" #default>\n <slot name=\"item\" :command=\"node.command\" :active=\"node.index === activeIndex\" />\n </template>\n </VCommandPaletteItem>\n </template>\n\n <!-- Loading is checked before emptiness, so a list waiting for an answer never claims\n there is none. Both are spoken by the status below instead. -->\n <div\n v-if=\"loading && filtered.length === 0\"\n class=\"v-command-palette-state\"\n aria-hidden=\"true\"\n >\n <slot name=\"loading\">\n <VSpinner />\n <span>{{ resolvedLoadingText }}</span>\n </slot>\n </div>\n <div v-else-if=\"filtered.length === 0\" class=\"v-command-palette-state\" aria-hidden=\"true\">\n <slot name=\"empty\" :query=\"term\">\n <VEmptyState\n size=\"sm\"\n class=\"v-command-palette-empty\"\n :icon=\"searchOffIcon\"\n :title=\"resolvedEmptyText\"\n />\n </slot>\n </div>\n </div>\n\n <footer v-if=\"!hideFooter\" class=\"v-command-palette-footer\">\n <slot name=\"footer\">\n <!-- A reminder for the eye: the combobox already tells assistive technology how it\n works, and every cap would otherwise be read as \"Keyboard shortcut\". -->\n <span class=\"v-command-palette-hints\" aria-hidden=\"true\">\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"up\" />\n <VHotkeys keys=\"down\" />\n {{ m.commandPalette.navigate }}\n </span>\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"enter\" />\n {{ m.commandPalette.choose }}\n </span>\n <span class=\"v-command-palette-hint\">\n <VHotkeys keys=\"esc\" />\n {{ m.common.close }}\n </span>\n </span>\n </slot>\n </footer>\n\n <!-- Outside the listbox, which may hold only options and groups. -->\n <span class=\"v-visually-hidden\" role=\"status\">{{ stateAnnouncement }}</span>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-command-palette {\n /*\n * Declared on the element rather than as a fallback: a custom property inherits, and a\n * palette opened inside a wider dialog would otherwise take that dialog's width.\n */\n --command-palette-width: var(--vectis-control-size-command-palette-width);\n /*\n * Where a row's icon and a block's name start: the list padding plus the inline padding of\n * a medium row. The field and the footer use it to line up with them.\n */\n --command-palette-inset: calc(var(--vectis-space-2) + var(--vectis-space-4));\n inline-size: var(--command-palette-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(\n 100dvb - var(--vectis-control-size-command-palette-offset) - var(--vectis-space-4)\n );\n /*\n * Pinned near the top rather than centred: the list grows and shrinks with every keystroke,\n * and a centred palette would move the field being typed in.\n */\n margin: var(--vectis-control-size-command-palette-offset) auto auto;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /* The display above beats the browser's own rule hiding a closed dialog. */\n .v-command-palette:not([open]) {\n display: none;\n }\n\n .v-command-palette:focus-visible {\n outline: none;\n }\n\n .v-command-palette-search {\n flex: none;\n display: flex;\n align-items: center;\n /* The gap of a medium row, so the typed text starts where the labels do. */\n gap: var(--vectis-space-2);\n min-block-size: var(--vectis-control-height-xl);\n padding-inline: var(--command-palette-inset);\n border-block-end: 1px solid var(--vectis-color-border);\n /* The magnifier matches the larger text of the field, through VIcon's context size. */\n --vectis-icon-size: var(--vectis-icon-size-md);\n }\n\n /* The field draws no ring of its own inside the palette: its edge takes the focus colour. */\n .v-command-palette-search:has(.v-command-palette-input:focus-visible) {\n border-block-end-color: var(--vectis-focus-ring-color);\n }\n\n .v-command-palette-search-icon,\n .v-command-palette-spinner {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n .v-command-palette-input {\n flex: 1;\n min-inline-size: 0;\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: var(--vectis-text-body-lg-size);\n font-weight: var(--vectis-text-body-lg-weight);\n line-height: var(--vectis-text-body-lg-leading);\n outline: none;\n }\n\n .v-command-palette-input::placeholder {\n color: var(--vectis-color-text-subtle);\n opacity: 1;\n }\n\n /*\n * The zero minimum is what lets the list shrink inside a short viewport and scroll, a flex\n * item otherwise refusing to go below its content.\n */\n .v-command-palette-list {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n min-block-size: 0;\n max-block-size: var(--vectis-control-size-command-palette-list-max-block);\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: var(--vectis-space-2);\n scroll-padding-block: var(--vectis-space-2);\n }\n\n /* Blocks and rows refuse to shrink: the list is a bounded column that scrolls instead. */\n .v-command-palette-group {\n display: flex;\n flex: none;\n flex-direction: column;\n gap: var(--vectis-space-1);\n }\n\n .v-command-palette-list > .v-command-palette-item {\n flex: none;\n }\n\n .v-command-palette-group-label {\n display: flex;\n align-items: center;\n min-block-size: var(--control-height);\n padding-inline: var(--control-padding-inline);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n\n /* Compounded with `.v-separator`, whose own sheet sets the margin and may load after this one. */\n .v-separator.v-command-palette-separator {\n flex: none;\n margin-inline: calc(-1 * var(--vectis-space-2));\n }\n\n .v-command-palette-state {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: center;\n gap: var(--control-gap);\n min-block-size: var(--control-height);\n padding: var(--vectis-space-1) var(--control-padding-inline);\n font-size: var(--control-font-size);\n color: var(--vectis-color-text-muted);\n }\n\n /* `[data-size]` lifts the selector above VEmptyState's size rules, whose sheet may load after. */\n .v-command-palette-state > .v-command-palette-empty[data-size] {\n flex: 1;\n padding-block: var(--vectis-space-4);\n padding-inline: 0;\n }\n\n .v-command-palette-footer {\n flex: none;\n display: flex;\n align-items: center;\n padding: var(--vectis-space-2) var(--command-palette-inset);\n border-block-start: 1px solid var(--vectis-color-border);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n color: var(--vectis-color-text-muted);\n }\n\n /* Keys mean nothing without a keyboard; a custom `#footer` stays. A trackpad or mouse keeps it. */\n @media not (any-pointer: fine) {\n .v-command-palette-footer:has(> .v-command-palette-hints) {\n display: none;\n }\n }\n\n .v-command-palette-hints {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vectis-space-1) var(--vectis-space-4);\n }\n\n .v-command-palette-hint {\n display: inline-flex;\n align-items: center;\n gap: var(--vectis-space-1);\n }\n\n .v-command-palette {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-command-palette[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-command-palette::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-command-palette[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-command-palette,\n .v-command-palette::backdrop {\n transition: none;\n }\n }\n\n @media (forced-colors: active) {\n .v-command-palette {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|