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,2 +1,2 @@
|
|
|
1
1
|
@layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
|
|
2
|
-
@layer vectis.components{.v-color-picker{--color-picker-checker:repeating-conic-gradient(var(--vectis-color-border) 0 25%, var(--vectis-color-surface) 0 50%) 0 0 / var(--vectis-control-size-color-picker-checker) var(--vectis-control-size-color-picker-checker);--color-picker-thumb:var(--vectis-control-size-color-picker-thumb);gap:var(--vectis-space-3);inline-size:var(--vectis-control-size-color-picker-width);max-inline-size:100%;padding:var(--vectis-space-3);color:var(--vectis-color-text);font-family:var(--vectis-text-family);flex-direction:column;display:inline-flex}.v-color-picker-area,.v-color-picker-track,.v-color-picker-preview,.v-color-picker-chip{forced-color-adjust:none;direction:ltr}.v-color-picker-area{block-size:var(--vectis-control-size-color-picker-area);border-radius:var(--vectis-radius-interactive);background:linear-gradient(to top, black, transparent), linear-gradient(to right, white, transparent), hsl(var(--color-picker-hue) 100% 50%);cursor:crosshair;touch-action:none;position:relative}.v-color-picker-thumb{inline-size:var(--color-picker-thumb);block-size:var(--color-picker-thumb);border-radius:var(--vectis-radius-pill);background:var(--color-picker-opaque);pointer-events:none;border:2px solid #fff;position:absolute;inset-block-start:calc((1 - var(--color-picker-v)) * 100%);inset-inline-start:calc(var(--color-picker-s) * 100%);translate:-50% -50%;box-shadow:0 0 0 1px #0006,inset 0 0 0 1px #0006}.v-color-picker-area:has(.v-hidden-input:focus-visible) .v-color-picker-thumb{outline:var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);outline-offset:var(--vectis-focus-ring-offset)}.v-color-picker-controls{align-items:center;gap:var(--vectis-space-2);display:flex}.v-color-picker-preview{inline-size:var(--vectis-control-size-color-picker-preview);block-size:var(--vectis-control-size-color-picker-preview);border-radius:var(--vectis-radius-pill);border:1px solid var(--vectis-color-border-on-fill);background:linear-gradient(var(--color-picker-color), var(--color-picker-color)) border-box, var(--color-picker-checker) padding-box;flex:none}.v-color-picker-tracks{gap:var(--vectis-space-3);flex-direction:column;flex:1;min-inline-size:0;display:flex}.v-color-picker-track{inline-size:100%;block-size:var(--vectis-control-size-color-picker-track);border-radius:var(--vectis-radius-pill);appearance:none;cursor:pointer;margin:0;display:block}.v-color-picker-hue{--color-picker-track-thumb:hsl(var(--color-picker-hue) 100% 50%);background:linear-gradient(90deg,red,#ff0,#0f0,#0ff,#00f,#f0f,red)}.v-color-picker-alpha{--color-picker-track-thumb:var(--color-picker-color);background:linear-gradient(to right, transparent, var(--color-picker-opaque)), var(--color-picker-checker)}.v-color-picker-track::-webkit-slider-thumb{appearance:none;inline-size:var(--color-picker-thumb);block-size:var(--color-picker-thumb);border-radius:var(--vectis-radius-pill);background:linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)), var(--color-picker-checker);border:2px solid #fff;box-shadow:0 0 0 1px #0006,inset 0 0 0 1px #0006}.v-color-picker-track::-moz-range-thumb{box-sizing:border-box;inline-size:var(--color-picker-thumb);block-size:var(--color-picker-thumb);border-radius:var(--vectis-radius-pill);background:linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)), var(--color-picker-checker);border:2px solid #fff;box-shadow:0 0 0 1px #0006,inset 0 0 0 1px #0006}.v-color-picker-track:focus-visible{outline:var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);outline-offset:var(--vectis-focus-ring-offset)}.v-color-picker-fields{gap:var(--vectis-space-2);display:flex}.v-color-picker-fields .v-color-picker-format{inline-size:var(--vectis-control-size-color-picker-format);flex:none}.v-color-picker-value{flex:1;min-inline-size:0}.v-color-picker-value .v-input-control{font-variant-numeric:tabular-nums;direction:ltr}.v-color-picker-value .v-input-control:dir(rtl){text-align:right}.v-color-picker-swatches{gap:var(--vectis-space-2);border:0;flex-wrap:wrap;min-inline-size:0;margin:0;padding:0;display:flex}.v-color-picker-swatch{cursor:pointer;display:inline-flex;position:relative}.v-color-picker-chip{inline-size:var(--vectis-control-size-color-picker-swatch);block-size:var(--vectis-control-size-color-picker-swatch);border-radius:var(--vectis-radius-interactive);border:1px solid var(--vectis-color-border-on-fill);background:linear-gradient(var(--color-picker-swatch), var(--color-picker-swatch)) border-box, var(--color-picker-checker) padding-box}.v-color-picker-swatch .v-hidden-input:checked+.v-color-picker-chip{outline:2px solid var(--vectis-color-text);outline-offset:1px}.v-color-picker-swatch .v-hidden-input:focus-visible+.v-color-picker-chip{outline:var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);outline-offset:var(--vectis-focus-ring-offset)}.v-color-picker[data-disabled] :is(.v-color-picker-area,.v-color-picker-preview,.v-color-picker-track,.v-color-picker-chip){opacity:.5}.v-color-picker[data-disabled] :is(.v-color-picker-area,.v-color-picker-track,.v-color-picker-swatch){cursor:not-allowed}.v-color-picker[data-readonly] :is(.v-color-picker-area,.v-color-picker-track,.v-color-picker-swatch){cursor:default}}
|
|
2
|
+
@layer vectis.components{.v-color-picker{--color-picker-checker:repeating-conic-gradient(var(--vectis-color-border) 0 25%, var(--vectis-color-surface) 0 50%) 0 0 / var(--vectis-control-size-color-picker-checker) var(--vectis-control-size-color-picker-checker);--color-picker-thumb:var(--vectis-control-size-color-picker-thumb);gap:var(--vectis-space-3);inline-size:var(--vectis-control-size-color-picker-width);max-inline-size:100%;padding:var(--vectis-space-3);color:var(--vectis-color-text);font-family:var(--vectis-text-family);flex-direction:column;display:inline-flex}.v-color-picker-area,.v-color-picker-track,.v-color-picker-preview,.v-color-picker-chip{forced-color-adjust:none;direction:ltr}.v-color-picker-area{block-size:var(--vectis-control-size-color-picker-area);border-radius:min(var(--vectis-radius-interactive), calc(var(--vectis-control-height-md) / 2));background:linear-gradient(to top, black, transparent), linear-gradient(to right, white, transparent), hsl(var(--color-picker-hue) 100% 50%);cursor:crosshair;touch-action:none;position:relative}.v-color-picker-thumb{inline-size:var(--color-picker-thumb);block-size:var(--color-picker-thumb);border-radius:var(--vectis-radius-pill);background:var(--color-picker-opaque);pointer-events:none;border:2px solid #fff;position:absolute;inset-block-start:calc((1 - var(--color-picker-v)) * 100%);inset-inline-start:calc(var(--color-picker-s) * 100%);translate:-50% -50%;box-shadow:0 0 0 1px #0006,inset 0 0 0 1px #0006}.v-color-picker-area:has(.v-hidden-input:focus-visible) .v-color-picker-thumb{outline:var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);outline-offset:var(--vectis-focus-ring-offset)}.v-color-picker-controls{align-items:center;gap:var(--vectis-space-2);display:flex}.v-color-picker-preview{inline-size:var(--vectis-control-size-color-picker-preview);block-size:var(--vectis-control-size-color-picker-preview);border-radius:var(--vectis-radius-pill);border:1px solid var(--vectis-color-border-on-fill);background:linear-gradient(var(--color-picker-color), var(--color-picker-color)) border-box, var(--color-picker-checker) padding-box;flex:none}.v-color-picker-tracks{gap:var(--vectis-space-3);flex-direction:column;flex:1;min-inline-size:0;display:flex}.v-color-picker-track{inline-size:100%;block-size:var(--vectis-control-size-color-picker-track);border-radius:var(--vectis-radius-pill);appearance:none;cursor:pointer;margin:0;display:block}.v-color-picker-hue{--color-picker-track-thumb:hsl(var(--color-picker-hue) 100% 50%);background:linear-gradient(90deg,red,#ff0,#0f0,#0ff,#00f,#f0f,red)}.v-color-picker-alpha{--color-picker-track-thumb:var(--color-picker-color);background:linear-gradient(to right, transparent, var(--color-picker-opaque)), var(--color-picker-checker)}.v-color-picker-track::-webkit-slider-thumb{appearance:none;inline-size:var(--color-picker-thumb);block-size:var(--color-picker-thumb);border-radius:var(--vectis-radius-pill);background:linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)), var(--color-picker-checker);border:2px solid #fff;box-shadow:0 0 0 1px #0006,inset 0 0 0 1px #0006}.v-color-picker-track::-moz-range-thumb{box-sizing:border-box;inline-size:var(--color-picker-thumb);block-size:var(--color-picker-thumb);border-radius:var(--vectis-radius-pill);background:linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)), var(--color-picker-checker);border:2px solid #fff;box-shadow:0 0 0 1px #0006,inset 0 0 0 1px #0006}.v-color-picker-track:focus-visible{outline:var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);outline-offset:var(--vectis-focus-ring-offset)}.v-color-picker-fields{gap:var(--vectis-space-2);display:flex}.v-color-picker-fields .v-color-picker-format{inline-size:var(--vectis-control-size-color-picker-format);flex:none}.v-color-picker-value{flex:1;min-inline-size:0}.v-color-picker-value .v-input-control{font-variant-numeric:tabular-nums;direction:ltr}.v-color-picker-value .v-input-control:dir(rtl){text-align:right}.v-color-picker-swatches{gap:var(--vectis-space-2);border:0;flex-wrap:wrap;min-inline-size:0;margin:0;padding:0;display:flex}.v-color-picker-swatch{cursor:pointer;display:inline-flex;position:relative}.v-color-picker-chip{inline-size:var(--vectis-control-size-color-picker-swatch);block-size:var(--vectis-control-size-color-picker-swatch);border-radius:var(--vectis-radius-interactive);border:1px solid var(--vectis-color-border-on-fill);background:linear-gradient(var(--color-picker-swatch), var(--color-picker-swatch)) border-box, var(--color-picker-checker) padding-box}.v-color-picker-swatch .v-hidden-input:checked+.v-color-picker-chip{outline:2px solid var(--vectis-color-text);outline-offset:1px}.v-color-picker-swatch .v-hidden-input:focus-visible+.v-color-picker-chip{outline:var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);outline-offset:var(--vectis-focus-ring-offset)}.v-color-picker[data-disabled] :is(.v-color-picker-area,.v-color-picker-preview,.v-color-picker-track,.v-color-picker-chip){opacity:.5}.v-color-picker[data-disabled] :is(.v-color-picker-area,.v-color-picker-track,.v-color-picker-swatch){cursor:not-allowed}.v-color-picker[data-readonly] :is(.v-color-picker-area,.v-color-picker-track,.v-color-picker-swatch){cursor:default}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VColorPicker.js","names":[],"sources":["../../../src/components/VColorPicker/VColorPicker.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * A colour picker built from native ranges: the hue and alpha tracks are `<input type=\"range\">`,\n * and the saturation-brightness area holds two hidden ones, one per axis, which assistive\n * technology adjusts on its own. JavaScript adds the pointer on the area and its two-axis arrow\n * keys, which no native control offers.\n */\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VInput from '../VInput/VInput.vue'\nimport VSelect from '../VSelect/VSelect.vue'\nimport { colorize as colorizeIcon } from '../VIcon/icons/colorize'\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { clamp } from '../../utils/number'\nimport { COLOR_FORMATS, formatColor, hsvToRgb, parseColor, rgbToHsv, sameColor } from './color'\nimport type { ColorFormat, Hsva, Rgba } from './color'\n\nexport type { ColorFormat } from './color'\n\n/** A preset colour: the colour alone, or the colour and the words that name it. */\nexport type ColorSwatch = string | { color: string; label: string }\n\ninterface ColorPickerProps {\n /**\n * How the value is written: `#rrggbb`, `rgb()`, `hsl()` or `oklch()`. The text field accepts\n * all four whatever this says.\n */\n format?: ColorFormat\n /** Offers an opacity track, and writes the alpha into the value when it is below 1. */\n alpha?: boolean\n /** Preset colours, offered as a row of swatches under the picker. */\n swatches?: ColorSwatch[]\n /**\n * Hides the text field and its format menu. VColorInput does, its own field playing that\n * part.\n */\n hideInput?: boolean\n /**\n * Hides the eyedropper button, shown otherwise wherever the browser offers the `EyeDropper`\n * API.\n */\n hideEyeDropper?: boolean\n /** Makes the picker unusable. */\n disabled?: boolean\n /**\n * Shows the colour without letting it be changed. The controls stay focusable and announce\n * their values, which separates it from `disabled`, and the value is still submitted.\n */\n readonly?: boolean\n /** What the picker is called. It falls back to the design system dictionary. */\n label?: string\n /** The name the value is sent under in a form. */\n name?: string\n}\n\nconst props = withDefaults(defineProps<ColorPickerProps>(), {\n format: 'hex',\n alpha: false,\n swatches: undefined,\n hideInput: false,\n hideEyeDropper: false,\n disabled: false,\n readonly: false,\n label: undefined,\n name: undefined,\n})\n\n/** The colour, written in `format`, or `null` when none has been chosen. */\nconst model = defineModel<string | null>({ default: null })\n\nconst m = useMessages()\nconst locale = useLocale()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.colorPicker.label)\n\n/** What the picker shows. With no colour yet, it rests on black, which writes nothing. */\nconst hsva = ref<Hsva>({ h: 0, s: 0, v: 0, a: 1 })\nconst rgba = computed(() => hsvToRgb(hsva.value))\n\n// @core\n// The value last written, so that its echo through the model is not parsed back: hex rounds the\n// channels, and a grey has no hue, so reading it back would move the thumbs under the pointer.\nlet written: string | null = null\n\nwatch(\n model,\n (value) => {\n if (value === written) return\n const parsed = parseColor(value)\n if (parsed) hsva.value = rgbToHsv(props.alpha ? parsed : { ...parsed, a: 1 }, hsva.value.h)\n },\n { immediate: true },\n)\n\nfunction write(next: Hsva) {\n hsva.value = next\n written = formatColor(hsvToRgb(next), props.format, props.alpha)\n model.value = written\n}\n\nfunction writeRgba(next: Rgba) {\n write(rgbToHsv(props.alpha ? next : { ...next, a: 1 }, hsva.value.h))\n}\n\nconst percent = computed(() => new Intl.NumberFormat(locale.value, { style: 'percent' }))\nconst degrees = computed(\n () =>\n new Intl.NumberFormat(locale.value, { style: 'unit', unit: 'degree', unitDisplay: 'narrow' }),\n)\n\n/** Both axes in words: read on either of the area's inputs, whichever one has the focus. */\nconst areaValueText = computed(() =>\n m.value.colorPicker.areaValue(\n percent.value.format(Math.round(hsva.value.s * 100) / 100),\n percent.value.format(Math.round(hsva.value.v * 100) / 100),\n ),\n)\n\nconst areaEl = ref<HTMLElement | null>(null)\nconst saturationEl = ref<HTMLInputElement | null>(null)\n\nfunction moveTo(event: PointerEvent) {\n const rect = areaEl.value!.getBoundingClientRect()\n write({\n ...hsva.value,\n s: clamp((event.clientX - rect.left) / rect.width, 0, 1),\n v: clamp(1 - (event.clientY - rect.top) / rect.height, 0, 1),\n })\n}\n\n/** The pointer dragging the area's thumb, if one is. */\nlet dragging: number | null = null\n\nfunction onAreaPointerdown(event: PointerEvent) {\n if (props.disabled || event.button !== 0) return\n // The focus is moved by hand, without the scroll a default focus on mousedown could cause.\n event.preventDefault()\n saturationEl.value?.focus({ preventScroll: true })\n if (props.readonly) return\n dragging = event.pointerId\n // @fallback\n // Capture keeps the drag going outside the area; synthetic events may not reference a\n // capturable pointer.\n try {\n areaEl.value!.setPointerCapture(event.pointerId)\n } catch {\n /* Synthetic pointers cannot be captured. */\n }\n moveTo(event)\n}\n\nfunction onAreaPointermove(event: PointerEvent) {\n if (event.pointerId === dragging) moveTo(event)\n}\n\nfunction onAreaPointerup(event: PointerEvent) {\n if (event.pointerId === dragging) dragging = null\n}\n\n// @keyboard\n/**\n * Both inputs of the area answer the same keys: the horizontal arrows and Home/End move the\n * saturation, the vertical arrows and PageUp/PageDown the brightness. Natively each input would\n * move its own axis on every arrow.\n */\nfunction onAreaKeydown(event: KeyboardEvent) {\n if (props.readonly) return\n const step = event.shiftKey ? 10 : 1\n const moves: Record<string, [axis: 's' | 'v', delta: number]> = {\n ArrowLeft: ['s', -step],\n ArrowRight: ['s', step],\n ArrowDown: ['v', -step],\n ArrowUp: ['v', step],\n PageDown: ['v', -10],\n PageUp: ['v', 10],\n Home: ['s', -100],\n End: ['s', 100],\n }\n const move = moves[event.key]\n if (!move) return\n event.preventDefault()\n const [axis, delta] = move\n write({ ...hsva.value, [axis]: clamp((Math.round(hsva.value[axis] * 100) + delta) / 100, 0, 1) })\n}\n\n/** What a native range reports on its own: a drag on a track, or assistive technology. */\nfunction onRangeInput(channel: keyof Hsva, scale: number, event: Event) {\n const el = event.target as HTMLInputElement\n // @core\n // A range has no native `readonly`: the move it has already made is put back.\n if (props.readonly) {\n el.value = String(Math.round(hsva.value[channel] * scale))\n return\n }\n write({ ...hsva.value, [channel]: Number(el.value) / scale })\n}\n\n/** A read-only swatch refuses the click, which is what checks a radio, arrows included. */\nfunction onSwatchClick(event: MouseEvent) {\n if (props.readonly) event.preventDefault()\n}\n\nconst shownFormat = ref<ColorFormat>(props.format)\nwatch(\n () => props.format,\n (format) => (shownFormat.value = format),\n)\n\nconst formatOptions = COLOR_FORMATS.map((format) => ({\n value: format,\n label: format.toUpperCase(),\n}))\n\n/** The text field's own copy, rewritten whenever the colour changes from elsewhere. */\nconst fieldText = ref('')\nwatch(\n [rgba, shownFormat, () => props.alpha, () => model.value === null],\n () => {\n fieldText.value =\n model.value === null ? '' : formatColor(rgba.value, shownFormat.value, props.alpha)\n },\n { immediate: true },\n)\n\n/** Takes what was typed, in any of the four formats, or puts the field back as it was. */\nfunction commitField() {\n const text = fieldText.value.trim()\n if (!text) {\n written = null\n model.value = null\n return\n }\n const parsed = parseColor(text)\n if (parsed) writeRgba(parsed)\n else fieldText.value = formatColor(rgba.value, shownFormat.value, props.alpha)\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter') return\n event.preventDefault()\n commitField()\n}\n\nconst items = computed(() =>\n (props.swatches ?? []).map((swatch) => {\n const color = typeof swatch === 'string' ? swatch : swatch.color\n return {\n color,\n label: typeof swatch === 'string' ? swatch : swatch.label,\n rgba: parseColor(color),\n }\n }),\n)\n\n// @core\n// The swatches are native radios, so the arrow keys move between them. Their name only groups\n// them: they are tied to a form that does not exist so that no enclosing form sends them, the\n// picker's value going through its own hidden input.\nconst swatchGroup = useId()\n\ninterface EyeDropperResult {\n sRGBHex: string\n}\n\n/** The EyeDropper API, which TypeScript's DOM library does not describe yet. */\ntype EyeDropperConstructor = new () => { open: () => Promise<EyeDropperResult> }\n\n// @fallback\n// Detected once mounted, never on the server: the button simply does not exist elsewhere.\nconst eyeDropper = ref<EyeDropperConstructor | null>(null)\nonMounted(() => {\n eyeDropper.value =\n (window as unknown as { EyeDropper?: EyeDropperConstructor }).EyeDropper ?? null\n})\n\nasync function pickFromScreen() {\n if (!eyeDropper.value) return\n try {\n const { sRGBHex } = await new eyeDropper.value().open()\n const picked = parseColor(sRGBHex)\n // The screen has no transparency: the opacity already chosen is kept.\n if (picked) writeRgba({ ...picked, a: hsva.value.a })\n } catch {\n // Escape cancels the eyedropper by rejecting the promise: nothing to report.\n }\n}\n\nconst opaque = computed(() => formatColor({ ...rgba.value, a: 1 }, 'rgb'))\n\nconst rootStyle = computed(() => ({\n '--color-picker-hue': String(hsva.value.h),\n '--color-picker-opaque': opaque.value,\n '--color-picker-color': formatColor(rgba.value, 'rgb', props.alpha),\n '--color-picker-s': String(hsva.value.s),\n '--color-picker-v': String(hsva.value.v),\n}))\n\nconst rootEl = ref<HTMLElement | null>(null)\n\ndefineExpose({\n /** Moves the focus to the saturation-brightness area. */\n focus: (options?: FocusOptions) => saturationEl.value?.focus(options),\n /** The root element. */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-color-picker\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :style=\"rootStyle\"\n >\n <div\n ref=\"areaEl\"\n class=\"v-color-picker-area\"\n @pointerdown=\"onAreaPointerdown\"\n @pointermove=\"onAreaPointermove\"\n @pointerup=\"onAreaPointerup\"\n @pointercancel=\"onAreaPointerup\"\n >\n <input\n ref=\"saturationEl\"\n type=\"range\"\n class=\"v-hidden-input\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.s * 100)\"\n :aria-label=\"m.colorPicker.saturation\"\n :aria-valuetext=\"areaValueText\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('s', 100, $event)\"\n @keydown=\"onAreaKeydown\"\n />\n <!-- Out of the tab order: one stop for the area, whose arrows move both axes. -->\n <input\n type=\"range\"\n class=\"v-hidden-input\"\n tabindex=\"-1\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.v * 100)\"\n :aria-label=\"m.colorPicker.brightness\"\n :aria-valuetext=\"areaValueText\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('v', 100, $event)\"\n @keydown=\"onAreaKeydown\"\n />\n <span class=\"v-color-picker-thumb\" aria-hidden=\"true\" />\n </div>\n\n <div class=\"v-color-picker-controls\">\n <VIconButton\n v-if=\"eyeDropper && !hideEyeDropper\"\n class=\"v-color-picker-eye-dropper\"\n :icon=\"colorizeIcon\"\n :label=\"m.colorPicker.eyeDropper\"\n size=\"sm\"\n :disabled=\"disabled || readonly\"\n @click=\"pickFromScreen\"\n />\n <span class=\"v-color-picker-preview\" aria-hidden=\"true\" />\n <div class=\"v-color-picker-tracks\">\n <input\n type=\"range\"\n class=\"v-color-picker-track v-color-picker-hue\"\n min=\"0\"\n max=\"360\"\n :value=\"Math.round(hsva.h)\"\n :aria-label=\"m.colorPicker.hue\"\n :aria-valuetext=\"degrees.format(Math.round(hsva.h))\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('h', 1, $event)\"\n />\n <input\n v-if=\"alpha\"\n type=\"range\"\n class=\"v-color-picker-track v-color-picker-alpha\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.a * 100)\"\n :aria-label=\"m.colorPicker.alpha\"\n :aria-valuetext=\"percent.format(Math.round(hsva.a * 100) / 100)\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('a', 100, $event)\"\n />\n </div>\n </div>\n\n <div v-if=\"!hideInput\" class=\"v-color-picker-fields\">\n <VSelect\n :model-value=\"shownFormat\"\n class=\"v-color-picker-format\"\n :options=\"formatOptions\"\n size=\"sm\"\n :aria-label=\"m.colorPicker.format\"\n :disabled=\"disabled\"\n @update:model-value=\"shownFormat = $event as ColorFormat\"\n />\n <VInput\n v-model=\"fieldText\"\n class=\"v-color-picker-value\"\n size=\"sm\"\n :aria-label=\"m.colorPicker.value\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n @change=\"commitField\"\n @keydown=\"onFieldKeydown\"\n />\n </div>\n\n <fieldset\n v-if=\"items.length\"\n class=\"v-color-picker-swatches\"\n :aria-label=\"m.colorPicker.swatches\"\n :disabled=\"disabled\"\n >\n <label\n v-for=\"item in items\"\n :key=\"item.color\"\n class=\"v-color-picker-swatch\"\n :style=\"{ '--color-picker-swatch': item.color }\"\n >\n <input\n type=\"radio\"\n class=\"v-hidden-input\"\n :name=\"swatchGroup\"\n :form=\"`${swatchGroup}-none`\"\n :value=\"item.color\"\n :checked=\"model !== null && sameColor(item.rgba, rgba, alpha)\"\n :aria-disabled=\"readonly || undefined\"\n @click=\"onSwatchClick\"\n @change=\"item.rgba && writeRgba(item.rgba)\"\n />\n <span class=\"v-color-picker-chip\" />\n <span class=\"v-visually-hidden\">{{ item.label }}</span>\n </label>\n </fieldset>\n\n <input v-if=\"name && model\" type=\"hidden\" :name=\"name\" :value=\"model\" />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-color-picker {\n /*\n * The checkerboard that shows through a translucent colour. It is drawn in the theme's own\n * surface and border colours, so it reads as \"transparent\" in both themes.\n */\n --color-picker-checker: repeating-conic-gradient(\n var(--vectis-color-border) 0 25%,\n var(--vectis-color-surface) 0 50%\n )\n 0 0 / var(--vectis-control-size-color-picker-checker)\n var(--vectis-control-size-color-picker-checker);\n --color-picker-thumb: var(--vectis-control-size-color-picker-thumb);\n\n display: inline-flex;\n flex-direction: column;\n gap: var(--vectis-space-3);\n inline-size: var(--vectis-control-size-color-picker-width);\n max-inline-size: 100%;\n padding: var(--vectis-space-3);\n color: var(--vectis-color-text);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * The colour content (area, tracks, preview, swatches) is the value itself, not the theme's\n * paint: it is never mirrored in right-to-left text, and forced colours leave it untouched.\n * Its gradients run between literal black and white, the ends of the colour model.\n */\n\n /*\n * The edge of the preview and the swatches is a translucent border over the colour, which it\n * shades rather than covers: an opaque grey ring left a pale fringe on the antialiased pixels.\n * The colour runs under the border and the checkerboard stops inside it, so no square of the\n * checkerboard shows through those pixels either.\n */\n .v-color-picker-area,\n .v-color-picker-track,\n .v-color-picker-preview,\n .v-color-picker-chip {\n direction: ltr;\n forced-color-adjust: none;\n }\n\n .v-color-picker-area {\n position: relative;\n block-size: var(--vectis-control-size-color-picker-area);\n border-radius: var(--vectis-radius-interactive);\n background:\n linear-gradient(to top, black, transparent), linear-gradient(to right, white, transparent),\n hsl(var(--color-picker-hue) 100% 50%);\n cursor: crosshair;\n touch-action: none;\n }\n\n /*\n * Every thumb carries a light ring and a dark one, whatever the theme: it sits on colours of\n * any lightness, against which a single themed ring would vanish.\n */\n .v-color-picker-thumb {\n position: absolute;\n inset-inline-start: calc(var(--color-picker-s) * 100%);\n inset-block-start: calc((1 - var(--color-picker-v)) * 100%);\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background: var(--color-picker-opaque);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n translate: -50% -50%;\n pointer-events: none;\n }\n\n .v-color-picker-area:has(.v-hidden-input:focus-visible) .v-color-picker-thumb {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n .v-color-picker-controls {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-2);\n }\n\n .v-color-picker-preview {\n flex: none;\n inline-size: var(--vectis-control-size-color-picker-preview);\n block-size: var(--vectis-control-size-color-picker-preview);\n border-radius: var(--vectis-radius-pill);\n border: 1px solid var(--vectis-color-border-on-fill);\n background:\n linear-gradient(var(--color-picker-color), var(--color-picker-color)) border-box,\n var(--color-picker-checker) padding-box;\n }\n\n .v-color-picker-tracks {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: var(--vectis-space-3);\n min-inline-size: 0;\n }\n\n .v-color-picker-track {\n display: block;\n inline-size: 100%;\n block-size: var(--vectis-control-size-color-picker-track);\n margin: 0;\n border-radius: var(--vectis-radius-pill);\n appearance: none;\n cursor: pointer;\n }\n\n .v-color-picker-hue {\n --color-picker-track-thumb: hsl(var(--color-picker-hue) 100% 50%);\n background: linear-gradient(\n to right,\n hsl(0 100% 50%),\n hsl(60 100% 50%),\n hsl(120 100% 50%),\n hsl(180 100% 50%),\n hsl(240 100% 50%),\n hsl(300 100% 50%),\n hsl(360 100% 50%)\n );\n }\n\n .v-color-picker-alpha {\n --color-picker-track-thumb: var(--color-picker-color);\n background:\n linear-gradient(to right, transparent, var(--color-picker-opaque)),\n var(--color-picker-checker);\n }\n\n /*\n * The two vendor pseudo-elements stay in two separate rules: a selector list holding one a\n * browser does not know is dropped whole.\n */\n .v-color-picker-track::-webkit-slider-thumb {\n appearance: none;\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background:\n linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)),\n var(--color-picker-checker);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n }\n\n .v-color-picker-track::-moz-range-thumb {\n box-sizing: border-box;\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background:\n linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)),\n var(--color-picker-checker);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n }\n\n .v-color-picker-track:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n .v-color-picker-fields {\n display: flex;\n gap: var(--vectis-space-2);\n }\n\n /* Qualified to outweigh the full width VSelect takes by default. */\n .v-color-picker-fields .v-color-picker-format {\n flex: none;\n inline-size: var(--vectis-control-size-color-picker-format);\n }\n\n .v-color-picker-value {\n flex: 1;\n min-inline-size: 0;\n }\n\n /*\n * A colour is written left to right in every language: in right-to-left text the bidi\n * algorithm would move the `#` of `#f59e0b` to the end. The field keeps its right alignment.\n */\n .v-color-picker-value .v-input-control {\n direction: ltr;\n font-variant-numeric: tabular-nums;\n }\n\n .v-color-picker-value .v-input-control:dir(rtl) {\n text-align: right;\n }\n\n .v-color-picker-swatches {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vectis-space-2);\n min-inline-size: 0;\n margin: 0;\n padding: 0;\n border: 0;\n }\n\n .v-color-picker-swatch {\n position: relative;\n display: inline-flex;\n cursor: pointer;\n }\n\n .v-color-picker-chip {\n inline-size: var(--vectis-control-size-color-picker-swatch);\n block-size: var(--vectis-control-size-color-picker-swatch);\n border-radius: var(--vectis-radius-interactive);\n border: 1px solid var(--vectis-color-border-on-fill);\n background:\n linear-gradient(var(--color-picker-swatch), var(--color-picker-swatch)) border-box,\n var(--color-picker-checker) padding-box;\n }\n\n /* An outline rather than a shadow marks the chosen swatch: forced colours keep outlines. */\n .v-color-picker-swatch .v-hidden-input:checked + .v-color-picker-chip {\n outline: 2px solid var(--vectis-color-text);\n outline-offset: 1px;\n }\n\n .v-color-picker-swatch .v-hidden-input:focus-visible + .v-color-picker-chip {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /* Colour content is media, which may fade; the text controls take the disabled tokens. */\n .v-color-picker[data-disabled]\n :is(\n .v-color-picker-area,\n .v-color-picker-preview,\n .v-color-picker-track,\n .v-color-picker-chip\n ) {\n opacity: 0.5;\n }\n\n .v-color-picker[data-disabled]\n :is(.v-color-picker-area, .v-color-picker-track, .v-color-picker-swatch) {\n cursor: not-allowed;\n }\n\n /* A read-only picker is still read, so it keeps the ordinary cursor, as VTimePicker does. */\n .v-color-picker[data-readonly]\n :is(.v-color-picker-area, .v-color-picker-track, .v-color-picker-swatch) {\n cursor: default;\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VColorPicker.js","names":[],"sources":["../../../src/components/VColorPicker/VColorPicker.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * A colour picker built from native ranges: the hue and alpha tracks are `<input type=\"range\">`,\n * and the saturation-brightness area holds two hidden ones, one per axis, which assistive\n * technology adjusts on its own. JavaScript adds the pointer on the area and its two-axis arrow\n * keys, which no native control offers.\n */\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VInput from '../VInput/VInput.vue'\nimport VSelect from '../VSelect/VSelect.vue'\nimport { colorize as colorizeIcon } from '../VIcon/icons/colorize'\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { clamp } from '../../utils/number'\nimport { COLOR_FORMATS, formatColor, hsvToRgb, parseColor, rgbToHsv, sameColor } from './color'\nimport type { ColorFormat, Hsva, Rgba } from './color'\n\nexport type { ColorFormat } from './color'\n\n/** A preset colour: the colour alone, or the colour and the words that name it. */\nexport type ColorSwatch = string | { color: string; label: string }\n\ninterface ColorPickerProps {\n /**\n * How the value is written: `#rrggbb`, `rgb()`, `hsl()` or `oklch()`. The text field accepts\n * all four whatever this says.\n */\n format?: ColorFormat\n /** Offers an opacity track, and writes the alpha into the value when it is below 1. */\n alpha?: boolean\n /** Preset colours, offered as a row of swatches under the picker. */\n swatches?: ColorSwatch[]\n /**\n * Hides the text field and its format menu. VColorInput does, its own field playing that\n * part.\n */\n hideInput?: boolean\n /**\n * Hides the eyedropper button, shown otherwise wherever the browser offers the `EyeDropper`\n * API.\n */\n hideEyeDropper?: boolean\n /** Makes the picker unusable. */\n disabled?: boolean\n /**\n * Shows the colour without letting it be changed. The controls stay focusable and announce\n * their values, which separates it from `disabled`, and the value is still submitted.\n */\n readonly?: boolean\n /** What the picker is called. It falls back to the design system dictionary. */\n label?: string\n /** The name the value is sent under in a form. */\n name?: string\n}\n\nconst props = withDefaults(defineProps<ColorPickerProps>(), {\n format: 'hex',\n alpha: false,\n swatches: undefined,\n hideInput: false,\n hideEyeDropper: false,\n disabled: false,\n readonly: false,\n label: undefined,\n name: undefined,\n})\n\n/** The colour, written in `format`, or `null` when none has been chosen. */\nconst model = defineModel<string | null>({ default: null })\n\nconst m = useMessages()\nconst locale = useLocale()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.colorPicker.label)\n\n/** What the picker shows. With no colour yet, it rests on black, which writes nothing. */\nconst hsva = ref<Hsva>({ h: 0, s: 0, v: 0, a: 1 })\nconst rgba = computed(() => hsvToRgb(hsva.value))\n\n// @core\n// The value last written, so that its echo through the model is not parsed back: hex rounds the\n// channels, and a grey has no hue, so reading it back would move the thumbs under the pointer.\nlet written: string | null = null\n\nwatch(\n model,\n (value) => {\n if (value === written) return\n const parsed = parseColor(value)\n if (parsed) hsva.value = rgbToHsv(props.alpha ? parsed : { ...parsed, a: 1 }, hsva.value.h)\n },\n { immediate: true },\n)\n\nfunction write(next: Hsva) {\n hsva.value = next\n written = formatColor(hsvToRgb(next), props.format, props.alpha)\n model.value = written\n}\n\nfunction writeRgba(next: Rgba) {\n write(rgbToHsv(props.alpha ? next : { ...next, a: 1 }, hsva.value.h))\n}\n\nconst percent = computed(() => new Intl.NumberFormat(locale.value, { style: 'percent' }))\nconst degrees = computed(\n () =>\n new Intl.NumberFormat(locale.value, { style: 'unit', unit: 'degree', unitDisplay: 'narrow' }),\n)\n\n/** Both axes in words: read on either of the area's inputs, whichever one has the focus. */\nconst areaValueText = computed(() =>\n m.value.colorPicker.areaValue(\n percent.value.format(Math.round(hsva.value.s * 100) / 100),\n percent.value.format(Math.round(hsva.value.v * 100) / 100),\n ),\n)\n\nconst areaEl = ref<HTMLElement | null>(null)\nconst saturationEl = ref<HTMLInputElement | null>(null)\n\nfunction moveTo(event: PointerEvent) {\n const rect = areaEl.value!.getBoundingClientRect()\n write({\n ...hsva.value,\n s: clamp((event.clientX - rect.left) / rect.width, 0, 1),\n v: clamp(1 - (event.clientY - rect.top) / rect.height, 0, 1),\n })\n}\n\n/** The pointer dragging the area's thumb, if one is. */\nlet dragging: number | null = null\n\nfunction onAreaPointerdown(event: PointerEvent) {\n if (props.disabled || event.button !== 0) return\n // The focus is moved by hand, without the scroll a default focus on mousedown could cause.\n event.preventDefault()\n saturationEl.value?.focus({ preventScroll: true })\n if (props.readonly) return\n dragging = event.pointerId\n // @fallback\n // Capture keeps the drag going outside the area; synthetic events may not reference a\n // capturable pointer.\n try {\n areaEl.value!.setPointerCapture(event.pointerId)\n } catch {\n /* Synthetic pointers cannot be captured. */\n }\n moveTo(event)\n}\n\nfunction onAreaPointermove(event: PointerEvent) {\n if (event.pointerId === dragging) moveTo(event)\n}\n\nfunction onAreaPointerup(event: PointerEvent) {\n if (event.pointerId === dragging) dragging = null\n}\n\n// @keyboard\n/**\n * Both inputs of the area answer the same keys: the horizontal arrows and Home/End move the\n * saturation, the vertical arrows and PageUp/PageDown the brightness. Natively each input would\n * move its own axis on every arrow.\n */\nfunction onAreaKeydown(event: KeyboardEvent) {\n if (props.readonly) return\n const step = event.shiftKey ? 10 : 1\n const moves: Record<string, [axis: 's' | 'v', delta: number]> = {\n ArrowLeft: ['s', -step],\n ArrowRight: ['s', step],\n ArrowDown: ['v', -step],\n ArrowUp: ['v', step],\n PageDown: ['v', -10],\n PageUp: ['v', 10],\n Home: ['s', -100],\n End: ['s', 100],\n }\n const move = moves[event.key]\n if (!move) return\n event.preventDefault()\n const [axis, delta] = move\n write({ ...hsva.value, [axis]: clamp((Math.round(hsva.value[axis] * 100) + delta) / 100, 0, 1) })\n}\n\n/** What a native range reports on its own: a drag on a track, or assistive technology. */\nfunction onRangeInput(channel: keyof Hsva, scale: number, event: Event) {\n const el = event.target as HTMLInputElement\n // @core\n // A range has no native `readonly`: the move it has already made is put back.\n if (props.readonly) {\n el.value = String(Math.round(hsva.value[channel] * scale))\n return\n }\n write({ ...hsva.value, [channel]: Number(el.value) / scale })\n}\n\n/** A read-only swatch refuses the click, which is what checks a radio, arrows included. */\nfunction onSwatchClick(event: MouseEvent) {\n if (props.readonly) event.preventDefault()\n}\n\nconst shownFormat = ref<ColorFormat>(props.format)\nwatch(\n () => props.format,\n (format) => (shownFormat.value = format),\n)\n\nconst formatOptions = COLOR_FORMATS.map((format) => ({\n value: format,\n label: format.toUpperCase(),\n}))\n\n/** The text field's own copy, rewritten whenever the colour changes from elsewhere. */\nconst fieldText = ref('')\nwatch(\n [rgba, shownFormat, () => props.alpha, () => model.value === null],\n () => {\n fieldText.value =\n model.value === null ? '' : formatColor(rgba.value, shownFormat.value, props.alpha)\n },\n { immediate: true },\n)\n\n/** Takes what was typed, in any of the four formats, or puts the field back as it was. */\nfunction commitField() {\n const text = fieldText.value.trim()\n if (!text) {\n written = null\n model.value = null\n return\n }\n const parsed = parseColor(text)\n if (parsed) writeRgba(parsed)\n else fieldText.value = formatColor(rgba.value, shownFormat.value, props.alpha)\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter') return\n event.preventDefault()\n commitField()\n}\n\nconst items = computed(() =>\n (props.swatches ?? []).map((swatch) => {\n const color = typeof swatch === 'string' ? swatch : swatch.color\n return {\n color,\n label: typeof swatch === 'string' ? swatch : swatch.label,\n rgba: parseColor(color),\n }\n }),\n)\n\n// @core\n// The swatches are native radios, so the arrow keys move between them. Their name only groups\n// them: they are tied to a form that does not exist so that no enclosing form sends them, the\n// picker's value going through its own hidden input.\nconst swatchGroup = useId()\n\ninterface EyeDropperResult {\n sRGBHex: string\n}\n\n/** The EyeDropper API, which TypeScript's DOM library does not describe yet. */\ntype EyeDropperConstructor = new () => { open: () => Promise<EyeDropperResult> }\n\n// @fallback\n// Detected once mounted, never on the server: the button simply does not exist elsewhere.\nconst eyeDropper = ref<EyeDropperConstructor | null>(null)\nonMounted(() => {\n eyeDropper.value =\n (window as unknown as { EyeDropper?: EyeDropperConstructor }).EyeDropper ?? null\n})\n\nasync function pickFromScreen() {\n if (!eyeDropper.value) return\n try {\n const { sRGBHex } = await new eyeDropper.value().open()\n const picked = parseColor(sRGBHex)\n // The screen has no transparency: the opacity already chosen is kept.\n if (picked) writeRgba({ ...picked, a: hsva.value.a })\n } catch {\n // Escape cancels the eyedropper by rejecting the promise: nothing to report.\n }\n}\n\nconst opaque = computed(() => formatColor({ ...rgba.value, a: 1 }, 'rgb'))\n\nconst rootStyle = computed(() => ({\n '--color-picker-hue': String(hsva.value.h),\n '--color-picker-opaque': opaque.value,\n '--color-picker-color': formatColor(rgba.value, 'rgb', props.alpha),\n '--color-picker-s': String(hsva.value.s),\n '--color-picker-v': String(hsva.value.v),\n}))\n\nconst rootEl = ref<HTMLElement | null>(null)\n\ndefineExpose({\n /** Moves the focus to the saturation-brightness area. */\n focus: (options?: FocusOptions) => saturationEl.value?.focus(options),\n /** The root element. */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-color-picker\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :style=\"rootStyle\"\n >\n <div\n ref=\"areaEl\"\n class=\"v-color-picker-area\"\n @pointerdown=\"onAreaPointerdown\"\n @pointermove=\"onAreaPointermove\"\n @pointerup=\"onAreaPointerup\"\n @pointercancel=\"onAreaPointerup\"\n >\n <input\n ref=\"saturationEl\"\n type=\"range\"\n class=\"v-hidden-input\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.s * 100)\"\n :aria-label=\"m.colorPicker.saturation\"\n :aria-valuetext=\"areaValueText\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('s', 100, $event)\"\n @keydown=\"onAreaKeydown\"\n />\n <!-- Out of the tab order: one stop for the area, whose arrows move both axes. -->\n <input\n type=\"range\"\n class=\"v-hidden-input\"\n tabindex=\"-1\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.v * 100)\"\n :aria-label=\"m.colorPicker.brightness\"\n :aria-valuetext=\"areaValueText\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('v', 100, $event)\"\n @keydown=\"onAreaKeydown\"\n />\n <span class=\"v-color-picker-thumb\" aria-hidden=\"true\" />\n </div>\n\n <div class=\"v-color-picker-controls\">\n <VIconButton\n v-if=\"eyeDropper && !hideEyeDropper\"\n class=\"v-color-picker-eye-dropper\"\n :icon=\"colorizeIcon\"\n :label=\"m.colorPicker.eyeDropper\"\n size=\"sm\"\n :disabled=\"disabled || readonly\"\n @click=\"pickFromScreen\"\n />\n <span class=\"v-color-picker-preview\" aria-hidden=\"true\" />\n <div class=\"v-color-picker-tracks\">\n <input\n type=\"range\"\n class=\"v-color-picker-track v-color-picker-hue\"\n min=\"0\"\n max=\"360\"\n :value=\"Math.round(hsva.h)\"\n :aria-label=\"m.colorPicker.hue\"\n :aria-valuetext=\"degrees.format(Math.round(hsva.h))\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('h', 1, $event)\"\n />\n <input\n v-if=\"alpha\"\n type=\"range\"\n class=\"v-color-picker-track v-color-picker-alpha\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.a * 100)\"\n :aria-label=\"m.colorPicker.alpha\"\n :aria-valuetext=\"percent.format(Math.round(hsva.a * 100) / 100)\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('a', 100, $event)\"\n />\n </div>\n </div>\n\n <div v-if=\"!hideInput\" class=\"v-color-picker-fields\">\n <VSelect\n :model-value=\"shownFormat\"\n class=\"v-color-picker-format\"\n :options=\"formatOptions\"\n size=\"sm\"\n :aria-label=\"m.colorPicker.format\"\n :disabled=\"disabled\"\n @update:model-value=\"shownFormat = $event as ColorFormat\"\n />\n <VInput\n v-model=\"fieldText\"\n class=\"v-color-picker-value\"\n size=\"sm\"\n :aria-label=\"m.colorPicker.value\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n @change=\"commitField\"\n @keydown=\"onFieldKeydown\"\n />\n </div>\n\n <fieldset\n v-if=\"items.length\"\n class=\"v-color-picker-swatches\"\n :aria-label=\"m.colorPicker.swatches\"\n :disabled=\"disabled\"\n >\n <label\n v-for=\"item in items\"\n :key=\"item.color\"\n class=\"v-color-picker-swatch\"\n :style=\"{ '--color-picker-swatch': item.color }\"\n >\n <input\n type=\"radio\"\n class=\"v-hidden-input\"\n :name=\"swatchGroup\"\n :form=\"`${swatchGroup}-none`\"\n :value=\"item.color\"\n :checked=\"model !== null && sameColor(item.rgba, rgba, alpha)\"\n :aria-disabled=\"readonly || undefined\"\n @click=\"onSwatchClick\"\n @change=\"item.rgba && writeRgba(item.rgba)\"\n />\n <span class=\"v-color-picker-chip\" />\n <span class=\"v-visually-hidden\">{{ item.label }}</span>\n </label>\n </fieldset>\n\n <input v-if=\"name && model\" type=\"hidden\" :name=\"name\" :value=\"model\" />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-color-picker {\n /*\n * The checkerboard that shows through a translucent colour. It is drawn in the theme's own\n * surface and border colours, so it reads as \"transparent\" in both themes.\n */\n --color-picker-checker: repeating-conic-gradient(\n var(--vectis-color-border) 0 25%,\n var(--vectis-color-surface) 0 50%\n )\n 0 0 / var(--vectis-control-size-color-picker-checker)\n var(--vectis-control-size-color-picker-checker);\n --color-picker-thumb: var(--vectis-control-size-color-picker-thumb);\n\n display: inline-flex;\n flex-direction: column;\n gap: var(--vectis-space-3);\n inline-size: var(--vectis-control-size-color-picker-width);\n max-inline-size: 100%;\n padding: var(--vectis-space-3);\n color: var(--vectis-color-text);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * The colour content (area, tracks, preview, swatches) is the value itself, not the theme's\n * paint: it is never mirrored in right-to-left text, and forced colours leave it untouched.\n * Its gradients run between literal black and white, the ends of the colour model.\n */\n\n /*\n * The edge of the preview and the swatches is a translucent border over the colour, which it\n * shades rather than covers: an opaque grey ring left a pale fringe on the antialiased pixels.\n * The colour runs under the border and the checkerboard stops inside it, so no square of the\n * checkerboard shows through those pixels either.\n */\n .v-color-picker-area,\n .v-color-picker-track,\n .v-color-picker-preview,\n .v-color-picker-chip {\n direction: ltr;\n forced-color-adjust: none;\n }\n\n .v-color-picker-area {\n position: relative;\n block-size: var(--vectis-control-size-color-picker-area);\n /* Capped like VTextarea so a pill theme rounds the area as much as a md VInput, not into a stadium. */\n border-radius: min(var(--vectis-radius-interactive), calc(var(--vectis-control-height-md) / 2));\n background:\n linear-gradient(to top, black, transparent), linear-gradient(to right, white, transparent),\n hsl(var(--color-picker-hue) 100% 50%);\n cursor: crosshair;\n touch-action: none;\n }\n\n /*\n * Every thumb carries a light ring and a dark one, whatever the theme: it sits on colours of\n * any lightness, against which a single themed ring would vanish.\n */\n .v-color-picker-thumb {\n position: absolute;\n inset-inline-start: calc(var(--color-picker-s) * 100%);\n inset-block-start: calc((1 - var(--color-picker-v)) * 100%);\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background: var(--color-picker-opaque);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n translate: -50% -50%;\n pointer-events: none;\n }\n\n .v-color-picker-area:has(.v-hidden-input:focus-visible) .v-color-picker-thumb {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n .v-color-picker-controls {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-2);\n }\n\n .v-color-picker-preview {\n flex: none;\n inline-size: var(--vectis-control-size-color-picker-preview);\n block-size: var(--vectis-control-size-color-picker-preview);\n border-radius: var(--vectis-radius-pill);\n border: 1px solid var(--vectis-color-border-on-fill);\n background:\n linear-gradient(var(--color-picker-color), var(--color-picker-color)) border-box,\n var(--color-picker-checker) padding-box;\n }\n\n .v-color-picker-tracks {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: var(--vectis-space-3);\n min-inline-size: 0;\n }\n\n .v-color-picker-track {\n display: block;\n inline-size: 100%;\n block-size: var(--vectis-control-size-color-picker-track);\n margin: 0;\n border-radius: var(--vectis-radius-pill);\n appearance: none;\n cursor: pointer;\n }\n\n .v-color-picker-hue {\n --color-picker-track-thumb: hsl(var(--color-picker-hue) 100% 50%);\n background: linear-gradient(\n to right,\n hsl(0 100% 50%),\n hsl(60 100% 50%),\n hsl(120 100% 50%),\n hsl(180 100% 50%),\n hsl(240 100% 50%),\n hsl(300 100% 50%),\n hsl(360 100% 50%)\n );\n }\n\n .v-color-picker-alpha {\n --color-picker-track-thumb: var(--color-picker-color);\n background:\n linear-gradient(to right, transparent, var(--color-picker-opaque)),\n var(--color-picker-checker);\n }\n\n /*\n * The two vendor pseudo-elements stay in two separate rules: a selector list holding one a\n * browser does not know is dropped whole.\n */\n .v-color-picker-track::-webkit-slider-thumb {\n appearance: none;\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background:\n linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)),\n var(--color-picker-checker);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n }\n\n .v-color-picker-track::-moz-range-thumb {\n box-sizing: border-box;\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background:\n linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)),\n var(--color-picker-checker);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n }\n\n .v-color-picker-track:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n .v-color-picker-fields {\n display: flex;\n gap: var(--vectis-space-2);\n }\n\n /* Qualified to outweigh the full width VSelect takes by default. */\n .v-color-picker-fields .v-color-picker-format {\n flex: none;\n inline-size: var(--vectis-control-size-color-picker-format);\n }\n\n .v-color-picker-value {\n flex: 1;\n min-inline-size: 0;\n }\n\n /*\n * A colour is written left to right in every language: in right-to-left text the bidi\n * algorithm would move the `#` of `#f59e0b` to the end. The field keeps its right alignment.\n */\n .v-color-picker-value .v-input-control {\n direction: ltr;\n font-variant-numeric: tabular-nums;\n }\n\n .v-color-picker-value .v-input-control:dir(rtl) {\n text-align: right;\n }\n\n .v-color-picker-swatches {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vectis-space-2);\n min-inline-size: 0;\n margin: 0;\n padding: 0;\n border: 0;\n }\n\n .v-color-picker-swatch {\n position: relative;\n display: inline-flex;\n cursor: pointer;\n }\n\n .v-color-picker-chip {\n inline-size: var(--vectis-control-size-color-picker-swatch);\n block-size: var(--vectis-control-size-color-picker-swatch);\n border-radius: var(--vectis-radius-interactive);\n border: 1px solid var(--vectis-color-border-on-fill);\n background:\n linear-gradient(var(--color-picker-swatch), var(--color-picker-swatch)) border-box,\n var(--color-picker-checker) padding-box;\n }\n\n /* An outline rather than a shadow marks the chosen swatch: forced colours keep outlines. */\n .v-color-picker-swatch .v-hidden-input:checked + .v-color-picker-chip {\n outline: 2px solid var(--vectis-color-text);\n outline-offset: 1px;\n }\n\n .v-color-picker-swatch .v-hidden-input:focus-visible + .v-color-picker-chip {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /* Colour content is media, which may fade; the text controls take the disabled tokens. */\n .v-color-picker[data-disabled]\n :is(\n .v-color-picker-area,\n .v-color-picker-preview,\n .v-color-picker-track,\n .v-color-picker-chip\n ) {\n opacity: 0.5;\n }\n\n .v-color-picker[data-disabled]\n :is(.v-color-picker-area, .v-color-picker-track, .v-color-picker-swatch) {\n cursor: not-allowed;\n }\n\n /* A read-only picker is still read, so it keeps the ordinary cursor, as VTimePicker does. */\n .v-color-picker[data-readonly]\n :is(.v-color-picker-area, .v-color-picker-track, .v-color-picker-swatch) {\n cursor: default;\n }\n}\n</style>\n"],"mappings":""}
|
package/dist/components/VColorPicker/VColorPicker.vue_vue_type_script_setup_true_lang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VColorPicker.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VColorPicker/VColorPicker.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * A colour picker built from native ranges: the hue and alpha tracks are `<input type=\"range\">`,\n * and the saturation-brightness area holds two hidden ones, one per axis, which assistive\n * technology adjusts on its own. JavaScript adds the pointer on the area and its two-axis arrow\n * keys, which no native control offers.\n */\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VInput from '../VInput/VInput.vue'\nimport VSelect from '../VSelect/VSelect.vue'\nimport { colorize as colorizeIcon } from '../VIcon/icons/colorize'\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { clamp } from '../../utils/number'\nimport { COLOR_FORMATS, formatColor, hsvToRgb, parseColor, rgbToHsv, sameColor } from './color'\nimport type { ColorFormat, Hsva, Rgba } from './color'\n\nexport type { ColorFormat } from './color'\n\n/** A preset colour: the colour alone, or the colour and the words that name it. */\nexport type ColorSwatch = string | { color: string; label: string }\n\ninterface ColorPickerProps {\n /**\n * How the value is written: `#rrggbb`, `rgb()`, `hsl()` or `oklch()`. The text field accepts\n * all four whatever this says.\n */\n format?: ColorFormat\n /** Offers an opacity track, and writes the alpha into the value when it is below 1. */\n alpha?: boolean\n /** Preset colours, offered as a row of swatches under the picker. */\n swatches?: ColorSwatch[]\n /**\n * Hides the text field and its format menu. VColorInput does, its own field playing that\n * part.\n */\n hideInput?: boolean\n /**\n * Hides the eyedropper button, shown otherwise wherever the browser offers the `EyeDropper`\n * API.\n */\n hideEyeDropper?: boolean\n /** Makes the picker unusable. */\n disabled?: boolean\n /**\n * Shows the colour without letting it be changed. The controls stay focusable and announce\n * their values, which separates it from `disabled`, and the value is still submitted.\n */\n readonly?: boolean\n /** What the picker is called. It falls back to the design system dictionary. */\n label?: string\n /** The name the value is sent under in a form. */\n name?: string\n}\n\nconst props = withDefaults(defineProps<ColorPickerProps>(), {\n format: 'hex',\n alpha: false,\n swatches: undefined,\n hideInput: false,\n hideEyeDropper: false,\n disabled: false,\n readonly: false,\n label: undefined,\n name: undefined,\n})\n\n/** The colour, written in `format`, or `null` when none has been chosen. */\nconst model = defineModel<string | null>({ default: null })\n\nconst m = useMessages()\nconst locale = useLocale()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.colorPicker.label)\n\n/** What the picker shows. With no colour yet, it rests on black, which writes nothing. */\nconst hsva = ref<Hsva>({ h: 0, s: 0, v: 0, a: 1 })\nconst rgba = computed(() => hsvToRgb(hsva.value))\n\n// @core\n// The value last written, so that its echo through the model is not parsed back: hex rounds the\n// channels, and a grey has no hue, so reading it back would move the thumbs under the pointer.\nlet written: string | null = null\n\nwatch(\n model,\n (value) => {\n if (value === written) return\n const parsed = parseColor(value)\n if (parsed) hsva.value = rgbToHsv(props.alpha ? parsed : { ...parsed, a: 1 }, hsva.value.h)\n },\n { immediate: true },\n)\n\nfunction write(next: Hsva) {\n hsva.value = next\n written = formatColor(hsvToRgb(next), props.format, props.alpha)\n model.value = written\n}\n\nfunction writeRgba(next: Rgba) {\n write(rgbToHsv(props.alpha ? next : { ...next, a: 1 }, hsva.value.h))\n}\n\nconst percent = computed(() => new Intl.NumberFormat(locale.value, { style: 'percent' }))\nconst degrees = computed(\n () =>\n new Intl.NumberFormat(locale.value, { style: 'unit', unit: 'degree', unitDisplay: 'narrow' }),\n)\n\n/** Both axes in words: read on either of the area's inputs, whichever one has the focus. */\nconst areaValueText = computed(() =>\n m.value.colorPicker.areaValue(\n percent.value.format(Math.round(hsva.value.s * 100) / 100),\n percent.value.format(Math.round(hsva.value.v * 100) / 100),\n ),\n)\n\nconst areaEl = ref<HTMLElement | null>(null)\nconst saturationEl = ref<HTMLInputElement | null>(null)\n\nfunction moveTo(event: PointerEvent) {\n const rect = areaEl.value!.getBoundingClientRect()\n write({\n ...hsva.value,\n s: clamp((event.clientX - rect.left) / rect.width, 0, 1),\n v: clamp(1 - (event.clientY - rect.top) / rect.height, 0, 1),\n })\n}\n\n/** The pointer dragging the area's thumb, if one is. */\nlet dragging: number | null = null\n\nfunction onAreaPointerdown(event: PointerEvent) {\n if (props.disabled || event.button !== 0) return\n // The focus is moved by hand, without the scroll a default focus on mousedown could cause.\n event.preventDefault()\n saturationEl.value?.focus({ preventScroll: true })\n if (props.readonly) return\n dragging = event.pointerId\n // @fallback\n // Capture keeps the drag going outside the area; synthetic events may not reference a\n // capturable pointer.\n try {\n areaEl.value!.setPointerCapture(event.pointerId)\n } catch {\n /* Synthetic pointers cannot be captured. */\n }\n moveTo(event)\n}\n\nfunction onAreaPointermove(event: PointerEvent) {\n if (event.pointerId === dragging) moveTo(event)\n}\n\nfunction onAreaPointerup(event: PointerEvent) {\n if (event.pointerId === dragging) dragging = null\n}\n\n// @keyboard\n/**\n * Both inputs of the area answer the same keys: the horizontal arrows and Home/End move the\n * saturation, the vertical arrows and PageUp/PageDown the brightness. Natively each input would\n * move its own axis on every arrow.\n */\nfunction onAreaKeydown(event: KeyboardEvent) {\n if (props.readonly) return\n const step = event.shiftKey ? 10 : 1\n const moves: Record<string, [axis: 's' | 'v', delta: number]> = {\n ArrowLeft: ['s', -step],\n ArrowRight: ['s', step],\n ArrowDown: ['v', -step],\n ArrowUp: ['v', step],\n PageDown: ['v', -10],\n PageUp: ['v', 10],\n Home: ['s', -100],\n End: ['s', 100],\n }\n const move = moves[event.key]\n if (!move) return\n event.preventDefault()\n const [axis, delta] = move\n write({ ...hsva.value, [axis]: clamp((Math.round(hsva.value[axis] * 100) + delta) / 100, 0, 1) })\n}\n\n/** What a native range reports on its own: a drag on a track, or assistive technology. */\nfunction onRangeInput(channel: keyof Hsva, scale: number, event: Event) {\n const el = event.target as HTMLInputElement\n // @core\n // A range has no native `readonly`: the move it has already made is put back.\n if (props.readonly) {\n el.value = String(Math.round(hsva.value[channel] * scale))\n return\n }\n write({ ...hsva.value, [channel]: Number(el.value) / scale })\n}\n\n/** A read-only swatch refuses the click, which is what checks a radio, arrows included. */\nfunction onSwatchClick(event: MouseEvent) {\n if (props.readonly) event.preventDefault()\n}\n\nconst shownFormat = ref<ColorFormat>(props.format)\nwatch(\n () => props.format,\n (format) => (shownFormat.value = format),\n)\n\nconst formatOptions = COLOR_FORMATS.map((format) => ({\n value: format,\n label: format.toUpperCase(),\n}))\n\n/** The text field's own copy, rewritten whenever the colour changes from elsewhere. */\nconst fieldText = ref('')\nwatch(\n [rgba, shownFormat, () => props.alpha, () => model.value === null],\n () => {\n fieldText.value =\n model.value === null ? '' : formatColor(rgba.value, shownFormat.value, props.alpha)\n },\n { immediate: true },\n)\n\n/** Takes what was typed, in any of the four formats, or puts the field back as it was. */\nfunction commitField() {\n const text = fieldText.value.trim()\n if (!text) {\n written = null\n model.value = null\n return\n }\n const parsed = parseColor(text)\n if (parsed) writeRgba(parsed)\n else fieldText.value = formatColor(rgba.value, shownFormat.value, props.alpha)\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter') return\n event.preventDefault()\n commitField()\n}\n\nconst items = computed(() =>\n (props.swatches ?? []).map((swatch) => {\n const color = typeof swatch === 'string' ? swatch : swatch.color\n return {\n color,\n label: typeof swatch === 'string' ? swatch : swatch.label,\n rgba: parseColor(color),\n }\n }),\n)\n\n// @core\n// The swatches are native radios, so the arrow keys move between them. Their name only groups\n// them: they are tied to a form that does not exist so that no enclosing form sends them, the\n// picker's value going through its own hidden input.\nconst swatchGroup = useId()\n\ninterface EyeDropperResult {\n sRGBHex: string\n}\n\n/** The EyeDropper API, which TypeScript's DOM library does not describe yet. */\ntype EyeDropperConstructor = new () => { open: () => Promise<EyeDropperResult> }\n\n// @fallback\n// Detected once mounted, never on the server: the button simply does not exist elsewhere.\nconst eyeDropper = ref<EyeDropperConstructor | null>(null)\nonMounted(() => {\n eyeDropper.value =\n (window as unknown as { EyeDropper?: EyeDropperConstructor }).EyeDropper ?? null\n})\n\nasync function pickFromScreen() {\n if (!eyeDropper.value) return\n try {\n const { sRGBHex } = await new eyeDropper.value().open()\n const picked = parseColor(sRGBHex)\n // The screen has no transparency: the opacity already chosen is kept.\n if (picked) writeRgba({ ...picked, a: hsva.value.a })\n } catch {\n // Escape cancels the eyedropper by rejecting the promise: nothing to report.\n }\n}\n\nconst opaque = computed(() => formatColor({ ...rgba.value, a: 1 }, 'rgb'))\n\nconst rootStyle = computed(() => ({\n '--color-picker-hue': String(hsva.value.h),\n '--color-picker-opaque': opaque.value,\n '--color-picker-color': formatColor(rgba.value, 'rgb', props.alpha),\n '--color-picker-s': String(hsva.value.s),\n '--color-picker-v': String(hsva.value.v),\n}))\n\nconst rootEl = ref<HTMLElement | null>(null)\n\ndefineExpose({\n /** Moves the focus to the saturation-brightness area. */\n focus: (options?: FocusOptions) => saturationEl.value?.focus(options),\n /** The root element. */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-color-picker\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :style=\"rootStyle\"\n >\n <div\n ref=\"areaEl\"\n class=\"v-color-picker-area\"\n @pointerdown=\"onAreaPointerdown\"\n @pointermove=\"onAreaPointermove\"\n @pointerup=\"onAreaPointerup\"\n @pointercancel=\"onAreaPointerup\"\n >\n <input\n ref=\"saturationEl\"\n type=\"range\"\n class=\"v-hidden-input\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.s * 100)\"\n :aria-label=\"m.colorPicker.saturation\"\n :aria-valuetext=\"areaValueText\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('s', 100, $event)\"\n @keydown=\"onAreaKeydown\"\n />\n <!-- Out of the tab order: one stop for the area, whose arrows move both axes. -->\n <input\n type=\"range\"\n class=\"v-hidden-input\"\n tabindex=\"-1\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.v * 100)\"\n :aria-label=\"m.colorPicker.brightness\"\n :aria-valuetext=\"areaValueText\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('v', 100, $event)\"\n @keydown=\"onAreaKeydown\"\n />\n <span class=\"v-color-picker-thumb\" aria-hidden=\"true\" />\n </div>\n\n <div class=\"v-color-picker-controls\">\n <VIconButton\n v-if=\"eyeDropper && !hideEyeDropper\"\n class=\"v-color-picker-eye-dropper\"\n :icon=\"colorizeIcon\"\n :label=\"m.colorPicker.eyeDropper\"\n size=\"sm\"\n :disabled=\"disabled || readonly\"\n @click=\"pickFromScreen\"\n />\n <span class=\"v-color-picker-preview\" aria-hidden=\"true\" />\n <div class=\"v-color-picker-tracks\">\n <input\n type=\"range\"\n class=\"v-color-picker-track v-color-picker-hue\"\n min=\"0\"\n max=\"360\"\n :value=\"Math.round(hsva.h)\"\n :aria-label=\"m.colorPicker.hue\"\n :aria-valuetext=\"degrees.format(Math.round(hsva.h))\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('h', 1, $event)\"\n />\n <input\n v-if=\"alpha\"\n type=\"range\"\n class=\"v-color-picker-track v-color-picker-alpha\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.a * 100)\"\n :aria-label=\"m.colorPicker.alpha\"\n :aria-valuetext=\"percent.format(Math.round(hsva.a * 100) / 100)\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('a', 100, $event)\"\n />\n </div>\n </div>\n\n <div v-if=\"!hideInput\" class=\"v-color-picker-fields\">\n <VSelect\n :model-value=\"shownFormat\"\n class=\"v-color-picker-format\"\n :options=\"formatOptions\"\n size=\"sm\"\n :aria-label=\"m.colorPicker.format\"\n :disabled=\"disabled\"\n @update:model-value=\"shownFormat = $event as ColorFormat\"\n />\n <VInput\n v-model=\"fieldText\"\n class=\"v-color-picker-value\"\n size=\"sm\"\n :aria-label=\"m.colorPicker.value\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n @change=\"commitField\"\n @keydown=\"onFieldKeydown\"\n />\n </div>\n\n <fieldset\n v-if=\"items.length\"\n class=\"v-color-picker-swatches\"\n :aria-label=\"m.colorPicker.swatches\"\n :disabled=\"disabled\"\n >\n <label\n v-for=\"item in items\"\n :key=\"item.color\"\n class=\"v-color-picker-swatch\"\n :style=\"{ '--color-picker-swatch': item.color }\"\n >\n <input\n type=\"radio\"\n class=\"v-hidden-input\"\n :name=\"swatchGroup\"\n :form=\"`${swatchGroup}-none`\"\n :value=\"item.color\"\n :checked=\"model !== null && sameColor(item.rgba, rgba, alpha)\"\n :aria-disabled=\"readonly || undefined\"\n @click=\"onSwatchClick\"\n @change=\"item.rgba && writeRgba(item.rgba)\"\n />\n <span class=\"v-color-picker-chip\" />\n <span class=\"v-visually-hidden\">{{ item.label }}</span>\n </label>\n </fieldset>\n\n <input v-if=\"name && model\" type=\"hidden\" :name=\"name\" :value=\"model\" />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-color-picker {\n /*\n * The checkerboard that shows through a translucent colour. It is drawn in the theme's own\n * surface and border colours, so it reads as \"transparent\" in both themes.\n */\n --color-picker-checker: repeating-conic-gradient(\n var(--vectis-color-border) 0 25%,\n var(--vectis-color-surface) 0 50%\n )\n 0 0 / var(--vectis-control-size-color-picker-checker)\n var(--vectis-control-size-color-picker-checker);\n --color-picker-thumb: var(--vectis-control-size-color-picker-thumb);\n\n display: inline-flex;\n flex-direction: column;\n gap: var(--vectis-space-3);\n inline-size: var(--vectis-control-size-color-picker-width);\n max-inline-size: 100%;\n padding: var(--vectis-space-3);\n color: var(--vectis-color-text);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * The colour content (area, tracks, preview, swatches) is the value itself, not the theme's\n * paint: it is never mirrored in right-to-left text, and forced colours leave it untouched.\n * Its gradients run between literal black and white, the ends of the colour model.\n */\n\n /*\n * The edge of the preview and the swatches is a translucent border over the colour, which it\n * shades rather than covers: an opaque grey ring left a pale fringe on the antialiased pixels.\n * The colour runs under the border and the checkerboard stops inside it, so no square of the\n * checkerboard shows through those pixels either.\n */\n .v-color-picker-area,\n .v-color-picker-track,\n .v-color-picker-preview,\n .v-color-picker-chip {\n direction: ltr;\n forced-color-adjust: none;\n }\n\n .v-color-picker-area {\n position: relative;\n block-size: var(--vectis-control-size-color-picker-area);\n border-radius: var(--vectis-radius-interactive);\n background:\n linear-gradient(to top, black, transparent), linear-gradient(to right, white, transparent),\n hsl(var(--color-picker-hue) 100% 50%);\n cursor: crosshair;\n touch-action: none;\n }\n\n /*\n * Every thumb carries a light ring and a dark one, whatever the theme: it sits on colours of\n * any lightness, against which a single themed ring would vanish.\n */\n .v-color-picker-thumb {\n position: absolute;\n inset-inline-start: calc(var(--color-picker-s) * 100%);\n inset-block-start: calc((1 - var(--color-picker-v)) * 100%);\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background: var(--color-picker-opaque);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n translate: -50% -50%;\n pointer-events: none;\n }\n\n .v-color-picker-area:has(.v-hidden-input:focus-visible) .v-color-picker-thumb {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n .v-color-picker-controls {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-2);\n }\n\n .v-color-picker-preview {\n flex: none;\n inline-size: var(--vectis-control-size-color-picker-preview);\n block-size: var(--vectis-control-size-color-picker-preview);\n border-radius: var(--vectis-radius-pill);\n border: 1px solid var(--vectis-color-border-on-fill);\n background:\n linear-gradient(var(--color-picker-color), var(--color-picker-color)) border-box,\n var(--color-picker-checker) padding-box;\n }\n\n .v-color-picker-tracks {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: var(--vectis-space-3);\n min-inline-size: 0;\n }\n\n .v-color-picker-track {\n display: block;\n inline-size: 100%;\n block-size: var(--vectis-control-size-color-picker-track);\n margin: 0;\n border-radius: var(--vectis-radius-pill);\n appearance: none;\n cursor: pointer;\n }\n\n .v-color-picker-hue {\n --color-picker-track-thumb: hsl(var(--color-picker-hue) 100% 50%);\n background: linear-gradient(\n to right,\n hsl(0 100% 50%),\n hsl(60 100% 50%),\n hsl(120 100% 50%),\n hsl(180 100% 50%),\n hsl(240 100% 50%),\n hsl(300 100% 50%),\n hsl(360 100% 50%)\n );\n }\n\n .v-color-picker-alpha {\n --color-picker-track-thumb: var(--color-picker-color);\n background:\n linear-gradient(to right, transparent, var(--color-picker-opaque)),\n var(--color-picker-checker);\n }\n\n /*\n * The two vendor pseudo-elements stay in two separate rules: a selector list holding one a\n * browser does not know is dropped whole.\n */\n .v-color-picker-track::-webkit-slider-thumb {\n appearance: none;\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background:\n linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)),\n var(--color-picker-checker);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n }\n\n .v-color-picker-track::-moz-range-thumb {\n box-sizing: border-box;\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background:\n linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)),\n var(--color-picker-checker);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n }\n\n .v-color-picker-track:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n .v-color-picker-fields {\n display: flex;\n gap: var(--vectis-space-2);\n }\n\n /* Qualified to outweigh the full width VSelect takes by default. */\n .v-color-picker-fields .v-color-picker-format {\n flex: none;\n inline-size: var(--vectis-control-size-color-picker-format);\n }\n\n .v-color-picker-value {\n flex: 1;\n min-inline-size: 0;\n }\n\n /*\n * A colour is written left to right in every language: in right-to-left text the bidi\n * algorithm would move the `#` of `#f59e0b` to the end. The field keeps its right alignment.\n */\n .v-color-picker-value .v-input-control {\n direction: ltr;\n font-variant-numeric: tabular-nums;\n }\n\n .v-color-picker-value .v-input-control:dir(rtl) {\n text-align: right;\n }\n\n .v-color-picker-swatches {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vectis-space-2);\n min-inline-size: 0;\n margin: 0;\n padding: 0;\n border: 0;\n }\n\n .v-color-picker-swatch {\n position: relative;\n display: inline-flex;\n cursor: pointer;\n }\n\n .v-color-picker-chip {\n inline-size: var(--vectis-control-size-color-picker-swatch);\n block-size: var(--vectis-control-size-color-picker-swatch);\n border-radius: var(--vectis-radius-interactive);\n border: 1px solid var(--vectis-color-border-on-fill);\n background:\n linear-gradient(var(--color-picker-swatch), var(--color-picker-swatch)) border-box,\n var(--color-picker-checker) padding-box;\n }\n\n /* An outline rather than a shadow marks the chosen swatch: forced colours keep outlines. */\n .v-color-picker-swatch .v-hidden-input:checked + .v-color-picker-chip {\n outline: 2px solid var(--vectis-color-text);\n outline-offset: 1px;\n }\n\n .v-color-picker-swatch .v-hidden-input:focus-visible + .v-color-picker-chip {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /* Colour content is media, which may fade; the text controls take the disabled tokens. */\n .v-color-picker[data-disabled]\n :is(\n .v-color-picker-area,\n .v-color-picker-preview,\n .v-color-picker-track,\n .v-color-picker-chip\n ) {\n opacity: 0.5;\n }\n\n .v-color-picker[data-disabled]\n :is(.v-color-picker-area, .v-color-picker-track, .v-color-picker-swatch) {\n cursor: not-allowed;\n }\n\n /* A read-only picker is still read, so it keeps the ordinary cursor, as VTimePicker does. */\n .v-color-picker[data-readonly]\n :is(.v-color-picker-area, .v-color-picker-track, .v-color-picker-swatch) {\n cursor: default;\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VColorPicker.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VColorPicker/VColorPicker.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * A colour picker built from native ranges: the hue and alpha tracks are `<input type=\"range\">`,\n * and the saturation-brightness area holds two hidden ones, one per axis, which assistive\n * technology adjusts on its own. JavaScript adds the pointer on the area and its two-axis arrow\n * keys, which no native control offers.\n */\nimport { computed, onMounted, ref, useId, watch } from 'vue'\n\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VInput from '../VInput/VInput.vue'\nimport VSelect from '../VSelect/VSelect.vue'\nimport { colorize as colorizeIcon } from '../VIcon/icons/colorize'\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { clamp } from '../../utils/number'\nimport { COLOR_FORMATS, formatColor, hsvToRgb, parseColor, rgbToHsv, sameColor } from './color'\nimport type { ColorFormat, Hsva, Rgba } from './color'\n\nexport type { ColorFormat } from './color'\n\n/** A preset colour: the colour alone, or the colour and the words that name it. */\nexport type ColorSwatch = string | { color: string; label: string }\n\ninterface ColorPickerProps {\n /**\n * How the value is written: `#rrggbb`, `rgb()`, `hsl()` or `oklch()`. The text field accepts\n * all four whatever this says.\n */\n format?: ColorFormat\n /** Offers an opacity track, and writes the alpha into the value when it is below 1. */\n alpha?: boolean\n /** Preset colours, offered as a row of swatches under the picker. */\n swatches?: ColorSwatch[]\n /**\n * Hides the text field and its format menu. VColorInput does, its own field playing that\n * part.\n */\n hideInput?: boolean\n /**\n * Hides the eyedropper button, shown otherwise wherever the browser offers the `EyeDropper`\n * API.\n */\n hideEyeDropper?: boolean\n /** Makes the picker unusable. */\n disabled?: boolean\n /**\n * Shows the colour without letting it be changed. The controls stay focusable and announce\n * their values, which separates it from `disabled`, and the value is still submitted.\n */\n readonly?: boolean\n /** What the picker is called. It falls back to the design system dictionary. */\n label?: string\n /** The name the value is sent under in a form. */\n name?: string\n}\n\nconst props = withDefaults(defineProps<ColorPickerProps>(), {\n format: 'hex',\n alpha: false,\n swatches: undefined,\n hideInput: false,\n hideEyeDropper: false,\n disabled: false,\n readonly: false,\n label: undefined,\n name: undefined,\n})\n\n/** The colour, written in `format`, or `null` when none has been chosen. */\nconst model = defineModel<string | null>({ default: null })\n\nconst m = useMessages()\nconst locale = useLocale()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.colorPicker.label)\n\n/** What the picker shows. With no colour yet, it rests on black, which writes nothing. */\nconst hsva = ref<Hsva>({ h: 0, s: 0, v: 0, a: 1 })\nconst rgba = computed(() => hsvToRgb(hsva.value))\n\n// @core\n// The value last written, so that its echo through the model is not parsed back: hex rounds the\n// channels, and a grey has no hue, so reading it back would move the thumbs under the pointer.\nlet written: string | null = null\n\nwatch(\n model,\n (value) => {\n if (value === written) return\n const parsed = parseColor(value)\n if (parsed) hsva.value = rgbToHsv(props.alpha ? parsed : { ...parsed, a: 1 }, hsva.value.h)\n },\n { immediate: true },\n)\n\nfunction write(next: Hsva) {\n hsva.value = next\n written = formatColor(hsvToRgb(next), props.format, props.alpha)\n model.value = written\n}\n\nfunction writeRgba(next: Rgba) {\n write(rgbToHsv(props.alpha ? next : { ...next, a: 1 }, hsva.value.h))\n}\n\nconst percent = computed(() => new Intl.NumberFormat(locale.value, { style: 'percent' }))\nconst degrees = computed(\n () =>\n new Intl.NumberFormat(locale.value, { style: 'unit', unit: 'degree', unitDisplay: 'narrow' }),\n)\n\n/** Both axes in words: read on either of the area's inputs, whichever one has the focus. */\nconst areaValueText = computed(() =>\n m.value.colorPicker.areaValue(\n percent.value.format(Math.round(hsva.value.s * 100) / 100),\n percent.value.format(Math.round(hsva.value.v * 100) / 100),\n ),\n)\n\nconst areaEl = ref<HTMLElement | null>(null)\nconst saturationEl = ref<HTMLInputElement | null>(null)\n\nfunction moveTo(event: PointerEvent) {\n const rect = areaEl.value!.getBoundingClientRect()\n write({\n ...hsva.value,\n s: clamp((event.clientX - rect.left) / rect.width, 0, 1),\n v: clamp(1 - (event.clientY - rect.top) / rect.height, 0, 1),\n })\n}\n\n/** The pointer dragging the area's thumb, if one is. */\nlet dragging: number | null = null\n\nfunction onAreaPointerdown(event: PointerEvent) {\n if (props.disabled || event.button !== 0) return\n // The focus is moved by hand, without the scroll a default focus on mousedown could cause.\n event.preventDefault()\n saturationEl.value?.focus({ preventScroll: true })\n if (props.readonly) return\n dragging = event.pointerId\n // @fallback\n // Capture keeps the drag going outside the area; synthetic events may not reference a\n // capturable pointer.\n try {\n areaEl.value!.setPointerCapture(event.pointerId)\n } catch {\n /* Synthetic pointers cannot be captured. */\n }\n moveTo(event)\n}\n\nfunction onAreaPointermove(event: PointerEvent) {\n if (event.pointerId === dragging) moveTo(event)\n}\n\nfunction onAreaPointerup(event: PointerEvent) {\n if (event.pointerId === dragging) dragging = null\n}\n\n// @keyboard\n/**\n * Both inputs of the area answer the same keys: the horizontal arrows and Home/End move the\n * saturation, the vertical arrows and PageUp/PageDown the brightness. Natively each input would\n * move its own axis on every arrow.\n */\nfunction onAreaKeydown(event: KeyboardEvent) {\n if (props.readonly) return\n const step = event.shiftKey ? 10 : 1\n const moves: Record<string, [axis: 's' | 'v', delta: number]> = {\n ArrowLeft: ['s', -step],\n ArrowRight: ['s', step],\n ArrowDown: ['v', -step],\n ArrowUp: ['v', step],\n PageDown: ['v', -10],\n PageUp: ['v', 10],\n Home: ['s', -100],\n End: ['s', 100],\n }\n const move = moves[event.key]\n if (!move) return\n event.preventDefault()\n const [axis, delta] = move\n write({ ...hsva.value, [axis]: clamp((Math.round(hsva.value[axis] * 100) + delta) / 100, 0, 1) })\n}\n\n/** What a native range reports on its own: a drag on a track, or assistive technology. */\nfunction onRangeInput(channel: keyof Hsva, scale: number, event: Event) {\n const el = event.target as HTMLInputElement\n // @core\n // A range has no native `readonly`: the move it has already made is put back.\n if (props.readonly) {\n el.value = String(Math.round(hsva.value[channel] * scale))\n return\n }\n write({ ...hsva.value, [channel]: Number(el.value) / scale })\n}\n\n/** A read-only swatch refuses the click, which is what checks a radio, arrows included. */\nfunction onSwatchClick(event: MouseEvent) {\n if (props.readonly) event.preventDefault()\n}\n\nconst shownFormat = ref<ColorFormat>(props.format)\nwatch(\n () => props.format,\n (format) => (shownFormat.value = format),\n)\n\nconst formatOptions = COLOR_FORMATS.map((format) => ({\n value: format,\n label: format.toUpperCase(),\n}))\n\n/** The text field's own copy, rewritten whenever the colour changes from elsewhere. */\nconst fieldText = ref('')\nwatch(\n [rgba, shownFormat, () => props.alpha, () => model.value === null],\n () => {\n fieldText.value =\n model.value === null ? '' : formatColor(rgba.value, shownFormat.value, props.alpha)\n },\n { immediate: true },\n)\n\n/** Takes what was typed, in any of the four formats, or puts the field back as it was. */\nfunction commitField() {\n const text = fieldText.value.trim()\n if (!text) {\n written = null\n model.value = null\n return\n }\n const parsed = parseColor(text)\n if (parsed) writeRgba(parsed)\n else fieldText.value = formatColor(rgba.value, shownFormat.value, props.alpha)\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter') return\n event.preventDefault()\n commitField()\n}\n\nconst items = computed(() =>\n (props.swatches ?? []).map((swatch) => {\n const color = typeof swatch === 'string' ? swatch : swatch.color\n return {\n color,\n label: typeof swatch === 'string' ? swatch : swatch.label,\n rgba: parseColor(color),\n }\n }),\n)\n\n// @core\n// The swatches are native radios, so the arrow keys move between them. Their name only groups\n// them: they are tied to a form that does not exist so that no enclosing form sends them, the\n// picker's value going through its own hidden input.\nconst swatchGroup = useId()\n\ninterface EyeDropperResult {\n sRGBHex: string\n}\n\n/** The EyeDropper API, which TypeScript's DOM library does not describe yet. */\ntype EyeDropperConstructor = new () => { open: () => Promise<EyeDropperResult> }\n\n// @fallback\n// Detected once mounted, never on the server: the button simply does not exist elsewhere.\nconst eyeDropper = ref<EyeDropperConstructor | null>(null)\nonMounted(() => {\n eyeDropper.value =\n (window as unknown as { EyeDropper?: EyeDropperConstructor }).EyeDropper ?? null\n})\n\nasync function pickFromScreen() {\n if (!eyeDropper.value) return\n try {\n const { sRGBHex } = await new eyeDropper.value().open()\n const picked = parseColor(sRGBHex)\n // The screen has no transparency: the opacity already chosen is kept.\n if (picked) writeRgba({ ...picked, a: hsva.value.a })\n } catch {\n // Escape cancels the eyedropper by rejecting the promise: nothing to report.\n }\n}\n\nconst opaque = computed(() => formatColor({ ...rgba.value, a: 1 }, 'rgb'))\n\nconst rootStyle = computed(() => ({\n '--color-picker-hue': String(hsva.value.h),\n '--color-picker-opaque': opaque.value,\n '--color-picker-color': formatColor(rgba.value, 'rgb', props.alpha),\n '--color-picker-s': String(hsva.value.s),\n '--color-picker-v': String(hsva.value.v),\n}))\n\nconst rootEl = ref<HTMLElement | null>(null)\n\ndefineExpose({\n /** Moves the focus to the saturation-brightness area. */\n focus: (options?: FocusOptions) => saturationEl.value?.focus(options),\n /** The root element. */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-color-picker\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :data-disabled=\"disabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :style=\"rootStyle\"\n >\n <div\n ref=\"areaEl\"\n class=\"v-color-picker-area\"\n @pointerdown=\"onAreaPointerdown\"\n @pointermove=\"onAreaPointermove\"\n @pointerup=\"onAreaPointerup\"\n @pointercancel=\"onAreaPointerup\"\n >\n <input\n ref=\"saturationEl\"\n type=\"range\"\n class=\"v-hidden-input\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.s * 100)\"\n :aria-label=\"m.colorPicker.saturation\"\n :aria-valuetext=\"areaValueText\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('s', 100, $event)\"\n @keydown=\"onAreaKeydown\"\n />\n <!-- Out of the tab order: one stop for the area, whose arrows move both axes. -->\n <input\n type=\"range\"\n class=\"v-hidden-input\"\n tabindex=\"-1\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.v * 100)\"\n :aria-label=\"m.colorPicker.brightness\"\n :aria-valuetext=\"areaValueText\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('v', 100, $event)\"\n @keydown=\"onAreaKeydown\"\n />\n <span class=\"v-color-picker-thumb\" aria-hidden=\"true\" />\n </div>\n\n <div class=\"v-color-picker-controls\">\n <VIconButton\n v-if=\"eyeDropper && !hideEyeDropper\"\n class=\"v-color-picker-eye-dropper\"\n :icon=\"colorizeIcon\"\n :label=\"m.colorPicker.eyeDropper\"\n size=\"sm\"\n :disabled=\"disabled || readonly\"\n @click=\"pickFromScreen\"\n />\n <span class=\"v-color-picker-preview\" aria-hidden=\"true\" />\n <div class=\"v-color-picker-tracks\">\n <input\n type=\"range\"\n class=\"v-color-picker-track v-color-picker-hue\"\n min=\"0\"\n max=\"360\"\n :value=\"Math.round(hsva.h)\"\n :aria-label=\"m.colorPicker.hue\"\n :aria-valuetext=\"degrees.format(Math.round(hsva.h))\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('h', 1, $event)\"\n />\n <input\n v-if=\"alpha\"\n type=\"range\"\n class=\"v-color-picker-track v-color-picker-alpha\"\n min=\"0\"\n max=\"100\"\n :value=\"Math.round(hsva.a * 100)\"\n :aria-label=\"m.colorPicker.alpha\"\n :aria-valuetext=\"percent.format(Math.round(hsva.a * 100) / 100)\"\n :aria-readonly=\"readonly || undefined\"\n :disabled=\"disabled\"\n @input=\"onRangeInput('a', 100, $event)\"\n />\n </div>\n </div>\n\n <div v-if=\"!hideInput\" class=\"v-color-picker-fields\">\n <VSelect\n :model-value=\"shownFormat\"\n class=\"v-color-picker-format\"\n :options=\"formatOptions\"\n size=\"sm\"\n :aria-label=\"m.colorPicker.format\"\n :disabled=\"disabled\"\n @update:model-value=\"shownFormat = $event as ColorFormat\"\n />\n <VInput\n v-model=\"fieldText\"\n class=\"v-color-picker-value\"\n size=\"sm\"\n :aria-label=\"m.colorPicker.value\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n @change=\"commitField\"\n @keydown=\"onFieldKeydown\"\n />\n </div>\n\n <fieldset\n v-if=\"items.length\"\n class=\"v-color-picker-swatches\"\n :aria-label=\"m.colorPicker.swatches\"\n :disabled=\"disabled\"\n >\n <label\n v-for=\"item in items\"\n :key=\"item.color\"\n class=\"v-color-picker-swatch\"\n :style=\"{ '--color-picker-swatch': item.color }\"\n >\n <input\n type=\"radio\"\n class=\"v-hidden-input\"\n :name=\"swatchGroup\"\n :form=\"`${swatchGroup}-none`\"\n :value=\"item.color\"\n :checked=\"model !== null && sameColor(item.rgba, rgba, alpha)\"\n :aria-disabled=\"readonly || undefined\"\n @click=\"onSwatchClick\"\n @change=\"item.rgba && writeRgba(item.rgba)\"\n />\n <span class=\"v-color-picker-chip\" />\n <span class=\"v-visually-hidden\">{{ item.label }}</span>\n </label>\n </fieldset>\n\n <input v-if=\"name && model\" type=\"hidden\" :name=\"name\" :value=\"model\" />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-color-picker {\n /*\n * The checkerboard that shows through a translucent colour. It is drawn in the theme's own\n * surface and border colours, so it reads as \"transparent\" in both themes.\n */\n --color-picker-checker: repeating-conic-gradient(\n var(--vectis-color-border) 0 25%,\n var(--vectis-color-surface) 0 50%\n )\n 0 0 / var(--vectis-control-size-color-picker-checker)\n var(--vectis-control-size-color-picker-checker);\n --color-picker-thumb: var(--vectis-control-size-color-picker-thumb);\n\n display: inline-flex;\n flex-direction: column;\n gap: var(--vectis-space-3);\n inline-size: var(--vectis-control-size-color-picker-width);\n max-inline-size: 100%;\n padding: var(--vectis-space-3);\n color: var(--vectis-color-text);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * The colour content (area, tracks, preview, swatches) is the value itself, not the theme's\n * paint: it is never mirrored in right-to-left text, and forced colours leave it untouched.\n * Its gradients run between literal black and white, the ends of the colour model.\n */\n\n /*\n * The edge of the preview and the swatches is a translucent border over the colour, which it\n * shades rather than covers: an opaque grey ring left a pale fringe on the antialiased pixels.\n * The colour runs under the border and the checkerboard stops inside it, so no square of the\n * checkerboard shows through those pixels either.\n */\n .v-color-picker-area,\n .v-color-picker-track,\n .v-color-picker-preview,\n .v-color-picker-chip {\n direction: ltr;\n forced-color-adjust: none;\n }\n\n .v-color-picker-area {\n position: relative;\n block-size: var(--vectis-control-size-color-picker-area);\n /* Capped like VTextarea so a pill theme rounds the area as much as a md VInput, not into a stadium. */\n border-radius: min(var(--vectis-radius-interactive), calc(var(--vectis-control-height-md) / 2));\n background:\n linear-gradient(to top, black, transparent), linear-gradient(to right, white, transparent),\n hsl(var(--color-picker-hue) 100% 50%);\n cursor: crosshair;\n touch-action: none;\n }\n\n /*\n * Every thumb carries a light ring and a dark one, whatever the theme: it sits on colours of\n * any lightness, against which a single themed ring would vanish.\n */\n .v-color-picker-thumb {\n position: absolute;\n inset-inline-start: calc(var(--color-picker-s) * 100%);\n inset-block-start: calc((1 - var(--color-picker-v)) * 100%);\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background: var(--color-picker-opaque);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n translate: -50% -50%;\n pointer-events: none;\n }\n\n .v-color-picker-area:has(.v-hidden-input:focus-visible) .v-color-picker-thumb {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n .v-color-picker-controls {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-2);\n }\n\n .v-color-picker-preview {\n flex: none;\n inline-size: var(--vectis-control-size-color-picker-preview);\n block-size: var(--vectis-control-size-color-picker-preview);\n border-radius: var(--vectis-radius-pill);\n border: 1px solid var(--vectis-color-border-on-fill);\n background:\n linear-gradient(var(--color-picker-color), var(--color-picker-color)) border-box,\n var(--color-picker-checker) padding-box;\n }\n\n .v-color-picker-tracks {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: var(--vectis-space-3);\n min-inline-size: 0;\n }\n\n .v-color-picker-track {\n display: block;\n inline-size: 100%;\n block-size: var(--vectis-control-size-color-picker-track);\n margin: 0;\n border-radius: var(--vectis-radius-pill);\n appearance: none;\n cursor: pointer;\n }\n\n .v-color-picker-hue {\n --color-picker-track-thumb: hsl(var(--color-picker-hue) 100% 50%);\n background: linear-gradient(\n to right,\n hsl(0 100% 50%),\n hsl(60 100% 50%),\n hsl(120 100% 50%),\n hsl(180 100% 50%),\n hsl(240 100% 50%),\n hsl(300 100% 50%),\n hsl(360 100% 50%)\n );\n }\n\n .v-color-picker-alpha {\n --color-picker-track-thumb: var(--color-picker-color);\n background:\n linear-gradient(to right, transparent, var(--color-picker-opaque)),\n var(--color-picker-checker);\n }\n\n /*\n * The two vendor pseudo-elements stay in two separate rules: a selector list holding one a\n * browser does not know is dropped whole.\n */\n .v-color-picker-track::-webkit-slider-thumb {\n appearance: none;\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background:\n linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)),\n var(--color-picker-checker);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n }\n\n .v-color-picker-track::-moz-range-thumb {\n box-sizing: border-box;\n inline-size: var(--color-picker-thumb);\n block-size: var(--color-picker-thumb);\n border: 2px solid white;\n border-radius: var(--vectis-radius-pill);\n background:\n linear-gradient(var(--color-picker-track-thumb), var(--color-picker-track-thumb)),\n var(--color-picker-checker);\n box-shadow:\n 0 0 0 1px rgb(0 0 0 / 0.4),\n inset 0 0 0 1px rgb(0 0 0 / 0.4);\n }\n\n .v-color-picker-track:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n .v-color-picker-fields {\n display: flex;\n gap: var(--vectis-space-2);\n }\n\n /* Qualified to outweigh the full width VSelect takes by default. */\n .v-color-picker-fields .v-color-picker-format {\n flex: none;\n inline-size: var(--vectis-control-size-color-picker-format);\n }\n\n .v-color-picker-value {\n flex: 1;\n min-inline-size: 0;\n }\n\n /*\n * A colour is written left to right in every language: in right-to-left text the bidi\n * algorithm would move the `#` of `#f59e0b` to the end. The field keeps its right alignment.\n */\n .v-color-picker-value .v-input-control {\n direction: ltr;\n font-variant-numeric: tabular-nums;\n }\n\n .v-color-picker-value .v-input-control:dir(rtl) {\n text-align: right;\n }\n\n .v-color-picker-swatches {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vectis-space-2);\n min-inline-size: 0;\n margin: 0;\n padding: 0;\n border: 0;\n }\n\n .v-color-picker-swatch {\n position: relative;\n display: inline-flex;\n cursor: pointer;\n }\n\n .v-color-picker-chip {\n inline-size: var(--vectis-control-size-color-picker-swatch);\n block-size: var(--vectis-control-size-color-picker-swatch);\n border-radius: var(--vectis-radius-interactive);\n border: 1px solid var(--vectis-color-border-on-fill);\n background:\n linear-gradient(var(--color-picker-swatch), var(--color-picker-swatch)) border-box,\n var(--color-picker-checker) padding-box;\n }\n\n /* An outline rather than a shadow marks the chosen swatch: forced colours keep outlines. */\n .v-color-picker-swatch .v-hidden-input:checked + .v-color-picker-chip {\n outline: 2px solid var(--vectis-color-text);\n outline-offset: 1px;\n }\n\n .v-color-picker-swatch .v-hidden-input:focus-visible + .v-color-picker-chip {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /* Colour content is media, which may fade; the text controls take the disabled tokens. */\n .v-color-picker[data-disabled]\n :is(\n .v-color-picker-area,\n .v-color-picker-preview,\n .v-color-picker-track,\n .v-color-picker-chip\n ) {\n opacity: 0.5;\n }\n\n .v-color-picker[data-disabled]\n :is(.v-color-picker-area, .v-color-picker-track, .v-color-picker-swatch) {\n cursor: not-allowed;\n }\n\n /* A read-only picker is still read, so it keeps the ordinary cursor, as VTimePicker does. */\n .v-color-picker[data-readonly]\n :is(.v-color-picker-area, .v-color-picker-track, .v-color-picker-swatch) {\n cursor: default;\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VCombobox.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VCombobox/VCombobox.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * Keep DOM focus on the field and move aria-activedescendant through options. JavaScript\n * supplies filtering, selection and keyboard navigation unavailable to native text inputs.\n */\n\nimport { computed, inject, ref, useId, watch } from 'vue'\n\nimport VEmptyState from '../VEmptyState/VEmptyState.vue'\nimport { expand_more as expandMoreIcon } from '../VIcon/icons/expand_more'\nimport { inbox as inboxIcon } from '../VIcon/icons/inbox'\nimport { search_off as searchOffIcon } from '../VIcon/icons/search_off'\nimport type { IconSource } from '../VIcon/types'\nimport type { FieldLabelPosition } from '../VField/VField.vue'\nimport VInput from '../VInput/VInput.vue'\nimport VListboxChevron from '../VListbox/VListboxChevron.vue'\nimport VListboxPanel from '../VListbox/VListboxPanel.vue'\nimport VListboxValues from '../VListbox/VListboxValues.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\n\nimport { toggleValue } from '../../utils/array'\nimport type {\n ItemValue,\n ListboxGroup,\n ListboxItem,\n ListboxChipSlotProps,\n ListboxOption,\n ListboxOptionSlotProps,\n ListboxOverflowSlotProps,\n ListboxSeparator,\n} from '../../types'\nimport { inputGroupKey } from '../VInput/context'\n\nimport { chipScaleFor } from '../../utils/chip'\nimport { createNormalizedCache, normalizeText } from '../../utils/text'\n\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\n\nimport { useFocusoutDismiss } from '../../composables/useFocusoutDismiss'\nimport { useInfiniteScroll } from '../../composables/useInfiniteScroll'\nimport { selectionOf, useListbox } from '../../composables/useListbox'\n\nimport { canClear } from '../../composables/useClearable'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\n\nimport { useDebouncedSearch } from '../../composables/useDebouncedSearch'\nimport { useMessages } from '../../i18n/state'\n\n/** One thing that can be chosen. */\nexport type ComboboxOption = ListboxOption\n\n/**\n * A named block of options, the equivalent of the grouping a native list offers. A\n * group none of whose options survive the search disappears entirely, its name\n * included.\n */\nexport type ComboboxGroup = ListboxGroup\n\n/**\n * A rule drawn between two blocks of options. It is purely decorative, and a separator the\n * search leaves stranded (at the top, at the bottom, or against another one) is simply not\n * drawn.\n */\nexport type ComboboxSeparator = ListboxSeparator\n\n/** Anything the list may hold: an option, a named block, or a separator. */\nexport type ComboboxItem = ListboxItem\n\n/**\n * How the list is narrowed as one types: by the component itself, not at all (when the\n * options already arrive filtered by a server), or by a rule of your own.\n */\nexport type ComboboxFilter = boolean | ((option: ComboboxOption, query: string) => boolean)\n\n/** Where the list opens relative to the field. */\nexport type ComboboxPlacement =\n 'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end'\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type ComboboxSize = 'sm' | 'md' | 'lg'\n\n/**\n * How the chosen values are shown in a multiple field: one chip each, or their labels\n * joined by commas.\n */\nexport type ComboboxDisplay = 'chip' | 'text'\n\n/** What the `#option` slot receives. */\nexport type ComboboxOptionSlotProps = ListboxOptionSlotProps\n\n/** What the `#chip` slot receives. */\nexport type ComboboxChipSlotProps = ListboxChipSlotProps\n\n/** What the `#overflow` slot receives. */\nexport type ComboboxOverflowSlotProps = ListboxOverflowSlotProps\n\n/** What the `#empty` slot receives. */\nexport interface ComboboxEmptySlotProps {\n query: string\n}\n\ninterface ComboboxProps {\n /**\n * What the list offers. An entry may be an option, a named block of options, or a\n * separator; a plain list of options remains perfectly valid.\n */\n options: ComboboxItem[]\n /**\n * Allows several values to be chosen, which makes the value a list and shows what has\n * been chosen inside the field, as chips or as text depending on `display`.\n */\n multiple?: boolean\n /**\n * How the chosen values are shown when several can be chosen: one dismissible chip each, or\n * their labels joined by commas on a single line, cut short with an ellipsis when they run\n * out of room.\n */\n display?: ComboboxDisplay\n /**\n * How many chosen values to show before the rest are summed up as \"+X\", as chips or as text\n * alike. Left out, or set to 0, every value is shown.\n */\n max?: number\n /**\n * Rephrases the \"+X\" standing for the values beyond `max`, \"+5 products\" for instance.\n * It receives the number of values being hidden.\n */\n overflowText?: (count: number) => string\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field. It is tied to the field for assistive technology,\n * so it is read out along with the label.\n */\n hint?: string\n /**\n * A message saying what is wrong with the value, shown in place of the hint so that the field\n * does not grow. It marks the field invalid, is read along with it, and is announced when it\n * appears or changes.\n */\n error?: string\n /** Marks the field as required: an asterisk follows the label, and the field is `required`. */\n required?: boolean\n /** Hides the label visually. It still names the field for assistive technology. */\n hideLabel?: boolean\n /**\n * Where the label sits: above the field (`top`, the default), or at its start, in a column of\n * its own, moving back above when there is not room for both.\n */\n labelPosition?: FieldLabelPosition\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: ComboboxSize\n /** Takes 4px off the height, as everywhere else in the design system. */\n compact?: boolean\n /** What the field says while nothing is chosen and nothing has been typed. */\n placeholder?: string\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what has been chosen without letting it be changed: nothing can be typed, the list\n * never opens, the chips lose their crosses and no clear cross is offered.\n */\n readonly?: boolean\n /** Marks the field as invalid, for a rule of your own. */\n invalid?: boolean\n /**\n * An icon inside the field, at the start, before the chips when several values can\n * be chosen. It is decorative by default and becomes a real button as soon as a\n * `@click:icon-start` listener is attached, in which case it needs `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * The chevron at the end of the field, which turns as the list opens: an icon name, or an\n * explicit render.\n */\n expandIcon?: IconSource\n /** Offers a cross that empties both the selection and the search. */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n /**\n * The title of the empty state the panel shows when the search matches nothing. It is also\n * what a screen reader hears, even when the `#empty` slot draws something else: set both\n * together.\n */\n emptyText?: string\n /** How the list is narrowed as one types. */\n filter?: ComboboxFilter\n /**\n * How long to wait before telling the source what is being searched for, in\n * milliseconds. Zero tells it at once, which suits a source that is not a network\n * request.\n */\n searchDebounce?: number\n /**\n * Says that something is being loaded. Either way the field replaces its chevron with a\n * spinner.\n */\n loading?: boolean\n /**\n * What is said while loading, and what the spinner is announced as. A screen reader hears\n * it even when the `#loading` slot draws something else: set both together.\n */\n loadingText?: string\n /**\n * Says that there are more pages to come, which makes the component ask for the next one as\n * the end of the list comes into view.\n */\n hasMore?: boolean\n /**\n * Renders only the rows near the visible part of the panel, for lists of thousands of options.\n * Rows are measured as they render, so a custom `#option` may vary in height. Options are then\n * announced with their position in the list.\n */\n virtual?: boolean\n /** Where the list opens relative to the field. */\n placement?: ComboboxPlacement\n}\n\nconst props = withDefaults(defineProps<ComboboxProps>(), {\n multiple: false,\n display: 'chip',\n max: undefined,\n overflowText: undefined,\n label: undefined,\n hint: undefined,\n error: undefined,\n required: false,\n hideLabel: false,\n labelPosition: 'top',\n size: 'md',\n compact: false,\n placeholder: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n iconStart: undefined,\n iconStartLabel: undefined,\n expandIcon: () => expandMoreIcon,\n clearable: false,\n clearLabel: undefined,\n emptyText: undefined,\n filter: true,\n searchDebounce: 250,\n loading: false,\n loadingText: undefined,\n hasMore: false,\n virtual: false,\n placement: 'bottom-start',\n})\n\nconst m = useMessages()\nconst resolvedEmptyText = computed(() => props.emptyText ?? m.value.combobox.empty)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.combobox.clear)\nconst resolvedLoadingText = computed(() => props.loadingText ?? m.value.common.loading)\n\nconst emit = defineEmits<{\n /** What is being searched for, to be sent to the source. */\n search: [query: string]\n /** The end of the list has come into view: send the next page. */\n 'load-more': []\n /** The clear cross emptied the selection and the search. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after the chips standing for the chosen\n * values rather than in their place.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, placed before the ones the field owns: the clear\n * cross and the chevron.\n */\n 'value-end'?(): unknown\n /** What a row of the list shows, in place of the plain label: a subtitle, an avatar, a badge. */\n option?(props: ComboboxOptionSlotProps): unknown\n /** Replaces the chip standing for one chosen value. */\n chip?(props: ComboboxChipSlotProps): unknown\n /** Replaces the \"+X\" standing for the values beyond `max`. */\n overflow?(props: ComboboxOverflowSlotProps): unknown\n /** What the panel shows when nothing matches. It receives the term that was searched. */\n empty?(props: ComboboxEmptySlotProps): unknown\n /** What the panel shows while loading its first options. */\n loading?(): unknown\n}>()\n\n// Everything below reads the RESOLVED values and never the props: the chips, the panel and the\n// field would otherwise come out at three different scales inside the same row.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n// The size, the density and the HEIGHT of the chips sitting inside the field, worked out\n// once in `utils/chip.ts` and shared with VFileInput. The height is set inline below\n// rather than restated as a table of CSS rules: it is the height the field forces on its\n// input, and deriving both from the same pair is what stops them drifting apart.\nconst chipScale = computed(() => chipScaleFor(resolvedSize.value, resolvedCompact.value))\n\n/** The chosen option's `value`, or the list of them when `multiple` is set. */\nconst model = defineModel<ItemValue | ItemValue[]>({ default: '' })\n\n// `class` and `style` stay on the wrapper, where a consumer expects to style the component;\n// everything else; a name above all; goes down to the text field, which is the element\n// assistive technology treats as the combobox.\ndefineOptions({ inheritAttrs: false })\nconst { rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand; and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\n/** What reaches the field: the consumer's own attributes, plus that listener. */\nconst fieldAttrs = computed(() => ({ ...forwardedAttrs.value, ...iconStartClick }))\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\n// One generated identifier serves twice: as the panel's own id, which the field points\n// at, and as the prefix of every option's id. The two strings can never collide, one\n// being a prefix of longer ones.\nconst optionsId = useId()\n\nconst open = ref(false)\nconst query = ref('')\nconst focused = ref(false)\n// The text in the field serves two purposes at once: it SHOWS the chosen label when a single\n// value is picked, and it is what one searches with. This flag tells the two apart; while it is\n// false the text is merely a label and narrows nothing, so reopening the panel offers the whole\n// list again.\nconst typed = ref(false)\n\nconst selectedValues = computed(() => selectionOf(model.value, props.multiple))\n\n/*\n * Reading the label on every call is what keeps the filter reactive: flattening the options\n * only ever touches the containers, so a label changed in place would go unnoticed by anything\n * keyed on the list.\n */\nconst normalizedOf = createNormalizedCache<ComboboxOption>()\nconst normalizedLabelOf = (option: ComboboxOption) => normalizedOf(option, option.label)\n\n// ONE search term, read both by the local filter and by what is sent to the source: the\n// list on screen and the request in flight therefore cannot describe different searches.\n// With nothing typed the term is empty, so a label merely being displayed never narrows\n// the list.\nconst searchTerm = computed(() => (typed.value ? query.value.trim() : ''))\n\n// Closing empties `searchTerm` (the typing flag drops) in the same tick as it hides the panel,\n// but the panel fades out rather than vanishing. Filtered on the live term, the list would\n// spring back to every option during that fade, and the panel grow under the pointer that just\n// chose.\nconst closedTerm = ref<string | null>(null)\n\nconst panelRef = ref<InstanceType<typeof VListboxPanel> | null>(null)\n\nconst {\n allOptions,\n visible: filtered,\n optionOf,\n labelOf,\n remember,\n activeIndex,\n optionId,\n highlightFirst,\n highlightSelected,\n move,\n hover,\n blocks,\n measureRow,\n} = useListbox({\n items: () => props.options,\n filter: (list) => {\n const matcher = props.filter\n if (matcher === false) return list\n const q = closedTerm.value ?? searchTerm.value\n if (!q) return list\n if (typeof matcher === 'function') return list.filter((o) => matcher(o, q))\n const needle = normalizeText(q)\n return list.filter((o) => normalizedLabelOf(o).includes(needle))\n },\n selected: () => selectedValues.value,\n open: () => open.value,\n id: optionsId,\n panelEl: () => panelRef.value?.el ?? null,\n virtual: () => props.virtual,\n hasMore: () => props.hasMore,\n size: () => resolvedSize.value,\n compact: () => resolvedCompact.value,\n})\n\n// @a11y\n/**\n * What is announced about the state of the panel; that nothing matches, or that something is\n * loading.\n */\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: (term) => emit('search', term),\n})\n\nwatch(searchTerm, (term) => {\n if (!open.value) return\n // A new search means a new list, so the highlight cannot survive it: with an outside source\n // the options are replaced wholesale, and the old position would name a completely unrelated\n // option. It is moved to the first available option rather than dropped altogether, and that\n // matters when the filtering is left to the source: the visible list keeps the very same\n // reference until the answer arrives, so the watcher below would never fire and Enter would\n // do nothing in the meantime.\n highlightFirst()\n // The page asked for under the previous term will never arrive, and the first page of the new\n // one may be exactly as long as the list it replaces; which the paging would not recognise as\n // an arrival.\n infiniteScroll.restart()\n emitSearch(term)\n})\n\n// With a single value, the field shows its label as ordinary text whenever it is not being\n// edited. Read through `selectedValues`, which knows that a number is a value even at zero: a\n// test on the string type would leave a numeric value unlabelled.\nfunction singleLabel() {\n const value = props.multiple ? undefined : selectedValues.value[0]\n return value === undefined ? '' : labelOf(value)\n}\n\nquery.value = singleLabel()\n\n// Refresh copied selection labels when options arrive or the parent updates the value.\n// Post-flush timing reads the accepted model after rendering.\nwatch(\n [allOptions, selectedValues],\n () => {\n if (props.multiple || open.value || typed.value) return\n query.value = singleLabel()\n },\n { flush: 'post' },\n)\n\n/** The chosen values are spelled out as one line of text rather than drawn as chips. */\nconst textDisplay = computed(() => props.multiple && props.display === 'text')\n\n// A read-only TEXT display keeps it folded under the focus too: nothing can be typed, and the\n// input's share of the row would only cut the line of values short for nothing.\nconst collapsed = computed(\n () =>\n props.multiple &&\n selectedValues.value.length > 0 &&\n (!focused.value || (textDisplay.value && props.readonly)),\n)\n\n// The field cannot work that out for itself, since the chips live outside its value, so the\n// answer is given to it explicitly, and the room for the cross is reserved accordingly.\nconst clearVisible = computed(() =>\n canClear(\n props,\n resolvedDisabled.value,\n selectedValues.value.length > 0 || query.value.length > 0,\n ),\n)\n\n// The single cut-off point of a frozen field: every route into the list; a click on the\n// control, the focus, a keystroke, typing; ends up here, so `readonly` is refused once rather\n// than guarded in each handler.\nfunction openPanel() {\n if (resolvedDisabled.value || props.readonly || open.value) return\n closedTerm.value = null\n open.value = true\n highlightSelected()\n // Told to the source after the panel is marked open; the delayed emission checks that flag\n // before firing; which lets an outside source load its first page as the panel appears.\n emitSearch(searchTerm.value, true)\n}\n\n// Asking for the next page as the end of the list is reached. The observer itself, its\n// re-arming after each page and the lock that stops it firing twice all live in\n// `useInfiniteScroll`; it is declared here so that closing the panel can reset it.\nconst sentinelEl = ref<HTMLElement | null>(null)\n\nconst infiniteScroll = useInfiniteScroll({\n sentinelEl,\n // Found by its ARIA role, which is public API, rather than by an internal class.\n root: (sentinel) => sentinel.closest('[role=\"listbox\"]'),\n canLoad: () => open.value && props.hasMore && !props.loading,\n loaded: () => allOptions.value,\n onLoadMore: () => emit('load-more'),\n})\n\n/**\n * Closes the list. The field then shows `label`: the chosen value's, unless the caller passes the\n * label of the option it has just chosen, which the value read back may not yet carry.\n */\nfunction closePanel(label = singleLabel()) {\n if (!open.value) return\n cancelSearch()\n // A page that never arrived; a request that failed; must not leave the paging frozen for\n // good, so the lock is released here.\n infiniteScroll.reset()\n closedTerm.value = searchTerm.value\n open.value = false\n activeIndex.value = -1\n typed.value = false\n query.value = label\n}\n\nwatch(\n () => props.readonly || resolvedDisabled.value,\n (frozen) => {\n if (frozen) closePanel()\n },\n)\n\n/**\n * Closes as soon as the focus leaves the component; the panel included, which is a descendant\n * of it even while floating above the page.\n */\nconst onFocusout = useFocusoutDismiss(rootEl, () => closePanel())\n\n/** The reader is typing: the text becomes a search, and the panel opens. */\nfunction onInput() {\n typed.value = true\n openPanel()\n}\n\n/**\n * With a single value chosen, the label shown in the field is selected on focus, so that typing\n * replaces it rather than appending to it; and until something IS typed, the whole list stays\n * on offer.\n */\nfunction selectQuery() {\n if (!props.multiple && query.value) inputRef.value?.select()\n}\n\nfunction onFocus() {\n focused.value = true\n selectQuery()\n}\n\n/** Whether a pointer event landed on the chevron, or on the spinner standing in its place. */\nconst onChevron = (event: Event) =>\n event.target instanceof Element &&\n event.target.closest('.v-combobox-chevron, .v-combobox-spinner') !== null\n\n// @core\n/*\n * The chevron is not focusable, so pressing it would hand the focus to the page: the root's\n * `focusout` then closes the panel, and the click that follows reopens it. A click meant to\n * close would flash the list instead.\n */\nfunction onControlMousedown(event: MouseEvent) {\n if (onChevron(event)) event.preventDefault()\n}\n\n/**\n * A click anywhere on the field focuses it and opens the panel; except on the chevron of an\n * open panel, which closes it.\n */\nfunction onControlClick(event: MouseEvent) {\n if (resolvedDisabled.value) return\n inputRef.value?.focus()\n if (open.value && onChevron(event)) {\n closePanel()\n return\n }\n openPanel()\n // Selected again after the click, which has just placed the caret somewhere in the middle of\n // the label.\n selectQuery()\n}\n\nfunction select(option: ComboboxOption) {\n if (option.disabled || props.readonly || resolvedDisabled.value) return\n // Remembered immediately: the option may vanish from the list; on the next search; before the\n // parent has even passed the new value back down.\n remember(option)\n if (props.multiple) {\n typed.value = false\n model.value = toggleValue(selectedValues.value, option.value)\n query.value = ''\n inputRef.value?.focus()\n } else {\n model.value = option.value\n closePanel(option.label)\n }\n}\n\nfunction removeValue(value: ItemValue) {\n if (!props.multiple || props.readonly || resolvedDisabled.value) return\n model.value = selectedValues.value.filter((v) => v !== value)\n inputRef.value?.focus()\n}\n\n/**\n * The clear cross was pressed. The field has already emptied the text it holds; what is left to\n * empty is the selection itself; the single value, or all the chips.\n */\nfunction onClear() {\n model.value = props.multiple ? [] : ''\n typed.value = false\n // The list may still be open (a pointer press on the cross does not close it): the\n // highlight is moved to the first option rather than dropped, or Enter would do nothing.\n if (open.value) highlightFirst()\n else activeIndex.value = -1\n emit('clear')\n}\n\n// @keyboard @a11y\n// Naming the current option and never the focus itself, which stays in the field throughout;\n// Backspace on an empty field takes back the last chip, the usual gesture in a field holding\n// several values.\nfunction onKeydown(event: KeyboardEvent) {\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n if (open.value) move(1)\n else openPanel()\n break\n case 'ArrowUp':\n event.preventDefault()\n if (open.value) move(-1)\n else openPanel()\n break\n case 'Enter':\n if (open.value && activeIndex.value >= 0) {\n event.preventDefault()\n const option = filtered.value[activeIndex.value]\n if (option) select(option)\n }\n break\n case 'Escape':\n // Consumed only when it closes the list, so a surrounding VDialog stays open; on a\n // closed list it is left to go on and close the dialog.\n if (open.value) {\n event.preventDefault()\n closePanel()\n }\n break\n case 'Tab':\n closePanel()\n break\n case 'Backspace':\n if (props.multiple && !query.value) {\n const last = selectedValues.value.at(-1)\n if (last !== undefined) removeValue(last)\n }\n break\n }\n}\n\ndefineExpose({\n /** Moves the focus to the search field. */\n focus: (options?: FocusOptions) => inputRef.value?.focus(options),\n /** Selects what has been typed in the search field. */\n select: () => inputRef.value?.select(),\n /** The real `<input>` behind the field, for what neither of the two above covers. */\n el: computed(() => inputRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-combobox\"\n :class=\"rootClass\"\n :style=\"[{ '--chip-height': chipScale.height }, rootStyle]\"\n :data-size=\"resolvedSize\"\n :data-compact=\"resolvedCompact ? '' : undefined\"\n :data-multiple=\"multiple ? '' : undefined\"\n :data-collapsed=\"collapsed ? '' : undefined\"\n :data-open=\"open ? '' : undefined\"\n @focusout=\"onFocusout\"\n >\n <div class=\"v-combobox-control\" @mousedown=\"onControlMousedown\" @click=\"onControlClick\">\n <!--\n The two arrangements VInput offers a field holding chips: its end controls lifted out of\n the flow, always; the chevron and the cross stay put whether or not the input has folded\n away; and the chips wrapping, when there are chips at all. A text display keeps the\n field on one line and needs only the first.\n -->\n <VInput\n ref=\"inputRef\"\n v-model=\"query\"\n class=\"v-input-end-pinned\"\n :class=\"{ 'v-input-chips': multiple && !textDisplay }\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-autocomplete=\"list\"\n :aria-expanded=\"open\"\n :aria-controls=\"optionsId\"\n v-bind=\"fieldAttrs\"\n :label=\"label\"\n :hint=\"hint\"\n :error=\"error\"\n :required=\"required\"\n :hide-label=\"hideLabel\"\n :label-position=\"labelPosition\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :invalid=\"invalid\"\n :disabled=\"resolvedDisabled\"\n :readonly=\"readonly\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :placeholder=\"selectedValues.length === 0 ? placeholder : undefined\"\n :aria-activedescendant=\"open && activeIndex >= 0 ? optionId(activeIndex) : undefined\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @focus=\"onFocus\"\n @blur=\"focused = false\"\n @clear=\"onClear\"\n >\n <template v-if=\"multiple || $slots.start\" #start>\n <VListboxValues\n v-if=\"multiple\"\n :values=\"selectedValues\"\n :display=\"display\"\n :max=\"max\"\n :collapsed=\"collapsed\"\n :overflow-text=\"overflowText\"\n :scale=\"chipScale\"\n :removable=\"!readonly\"\n :disabled=\"resolvedDisabled\"\n :label-of=\"labelOf\"\n :option-of=\"optionOf\"\n @remove=\"removeValue\"\n >\n <template v-if=\"$slots.chip\" #chip=\"chipProps\">\n <slot name=\"chip\" v-bind=\"chipProps\" />\n </template>\n <template v-if=\"$slots.overflow\" #overflow=\"overflowProps\">\n <slot name=\"overflow\" v-bind=\"overflowProps\" />\n </template>\n </VListboxValues>\n <slot name=\"start\" />\n </template>\n\n <template v-if=\"$slots['value-end']\" #value-end><slot name=\"value-end\" /></template>\n\n <!--\n The chevron sits at the end of the field and turns as the panel opens; the clear cross\n is rendered by the field itself, to its left. While loading, the spinner takes EXACTLY\n the chevron's place; the field gives a spinner among its direct children the size of\n an icon; so nothing shifts.\n -->\n <template #end>\n <VSpinner v-if=\"loading\" class=\"v-combobox-spinner v-input-icon-end\" aria-hidden=\"true\" />\n <VListboxChevron v-else class=\"v-combobox-chevron\" :icon=\"expandIcon\" :open=\"open\" />\n </template>\n </VInput>\n </div>\n\n <VListboxPanel\n :id=\"optionsId\"\n ref=\"panelRef\"\n v-model:open=\"open\"\n anchor=\"--combobox-anchor\"\n :placement=\"placement\"\n class=\"v-combobox-panel\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :multiple=\"multiple\"\n :virtual=\"virtual\"\n :blocks=\"blocks\"\n :measure=\"measureRow\"\n @select=\"select\"\n @highlight=\"hover\"\n >\n <template v-if=\"$slots.option\" #option=\"slotProps\">\n <slot name=\"option\" v-bind=\"slotProps\" />\n </template>\n\n <!--\n The order matters: loading is checked before emptiness, so that a panel waiting for an\n answer never claims there is no result. Both are hidden from screen readers, and that is\n not an oversight.\n -->\n <div v-if=\"loading && filtered.length === 0\" class=\"v-combobox-state\" aria-hidden=\"true\">\n <slot name=\"loading\">\n <VSpinner :label=\"resolvedLoadingText\" />\n <span>{{ resolvedLoadingText }}</span>\n </slot>\n </div>\n <div v-else-if=\"filtered.length === 0\" class=\"v-combobox-state\" aria-hidden=\"true\">\n <slot name=\"empty\" :query=\"searchTerm\">\n <VEmptyState\n size=\"sm\"\n class=\"v-combobox-empty\"\n :icon=\"searchTerm ? searchOffIcon : inboxIcon\"\n :title=\"resolvedEmptyText\"\n />\n </slot>\n </div>\n\n <!--\n It has to stay a SINGLE element: rendering the spinner conditionally in its place would\n destroy the marker and silently cancel the observation, and no further page would ever\n be asked for.\n -->\n <div v-if=\"hasMore\" ref=\"sentinelEl\" class=\"v-combobox-more\" aria-hidden=\"true\">\n <VSpinner v-if=\"loading\" :label=\"resolvedLoadingText\" />\n </div>\n </VListboxPanel>\n\n <!-- The spoken counterpart of the two panel states above. It sits OUTSIDE the\n listbox, being exactly the kind of content a listbox may not contain. -->\n <span class=\"v-visually-hidden\" role=\"status\">{{ stateAnnouncement }}</span>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-combobox {\n /*\n * It is declared on the root because that is the common ancestor of the field and the\n * panel; a panel drawn above the page remains a descendant of it in the document.\n */\n anchor-scope: --combobox-anchor;\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n .v-combobox-control {\n display: block;\n }\n\n /*\n * The anchor is the FIELD's box and not this wrapper's, which also holds the label and the\n * hint: anchored to the wrapper, the panel opens a hint's height below the field, and a\n * label's height above it once there is no room below and `flip-block` turns it over. Against\n * the field it covers whichever of the two it lands on, which a panel belonging to a control\n * is supposed to do.\n */\n .v-combobox-control .v-input-field {\n anchor-name: --combobox-anchor;\n }\n\n .v-combobox-panel {\n max-block-size: var(--vectis-control-size-combobox-list-max-block);\n }\n\n /*\n * The chevron and the clear cross are pinned to the end of the field by VInput's\n * `.v-input-end-pinned`, which also reserves their room. The chevron and the spinner take\n * turns in the same place and occupy the same box; the field gives a spinner among its direct\n * children the size of an icon; so swapping one for the other shifts nothing.\n */\n .v-combobox-spinner {\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * It remains in the page and remains focusable; clicking the field or tabbing into it brings\n * it straight back, the folded state ending as soon as it has the focus.\n */\n .v-combobox[data-collapsed] .v-input-control {\n position: absolute;\n width: 0;\n height: 0;\n padding: 0;\n }\n\n /*\n * Under the focus the two share the row, and the line is capped at half of it so that a long\n * selection cannot squeeze what is being typed down to nothing. Folded, the input is out of\n * the flow and the line takes the whole field.\n */\n .v-combobox:not([data-collapsed]) .v-listbox-text {\n max-inline-size: 50%;\n }\n\n /*\n * An unbroken line has the whole of its text as its min-content width, and that width travels\n * up through the field to the root. In a grid track or a flex row sized `auto`, the component\n * then widens its container to fit every label instead of cutting them short, and the\n * ellipsis never shows.\n */\n .v-combobox:has(.v-listbox-text) {\n min-inline-size: 0;\n }\n\n /* The rows saying \"nothing matches\" or \"loading\" are built like an option: they read\n the same inherited dimensions from the panel, and the spinner follows through the\n same block's icon context. They refuse to shrink, the panel being a bounded column\n that would otherwise squash them. */\n .v-combobox-state {\n display: flex;\n flex: none;\n align-items: center;\n gap: var(--control-gap);\n min-height: 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 /*\n * The default empty state spans the panel. `[data-size]` lifts the selector above VEmptyState's\n * own size rules, whose sheet may load after this one.\n */\n .v-combobox-state > .v-combobox-empty[data-size] {\n flex: 1;\n padding-block: var(--vectis-space-3);\n padding-inline: 0;\n }\n\n /*\n * The foot of the list: the marker watched for the end, and the place the next-page spinner\n * appears. It is given a real height on purpose; a box of no height at all makes the crossing\n * it is watched for unreliable.\n */\n .v-combobox-more {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: center;\n min-height: var(--vectis-control-height-xs);\n color: var(--vectis-color-text-muted);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-combobox-chevron {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VCombobox.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VCombobox/VCombobox.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @a11y @core\n/**\n * Keep DOM focus on the field and move aria-activedescendant through options. JavaScript\n * supplies filtering, selection and keyboard navigation unavailable to native text inputs.\n */\n\nimport { computed, inject, ref, useId, watch } from 'vue'\n\nimport VEmptyState from '../VEmptyState/VEmptyState.vue'\nimport { expand_more as expandMoreIcon } from '../VIcon/icons/expand_more'\nimport { inbox as inboxIcon } from '../VIcon/icons/inbox'\nimport { search_off as searchOffIcon } from '../VIcon/icons/search_off'\nimport type { IconSource } from '../VIcon/types'\nimport type { FieldLabelPosition } from '../VField/VField.vue'\nimport VInput from '../VInput/VInput.vue'\nimport VListboxChevron from '../VListbox/VListboxChevron.vue'\nimport VListboxPanel from '../VListbox/VListboxPanel.vue'\nimport VListboxValues from '../VListbox/VListboxValues.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\n\nimport { toggleValue } from '../../utils/array'\nimport type {\n ItemValue,\n ListboxGroup,\n ListboxItem,\n ListboxChipSlotProps,\n ListboxOption,\n ListboxOptionSlotProps,\n ListboxOverflowSlotProps,\n ListboxSeparator,\n} from '../../types'\nimport { inputGroupKey } from '../VInput/context'\n\nimport { chipScaleFor } from '../../utils/chip'\nimport { createNormalizedCache, normalizeText } from '../../utils/text'\n\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\n\nimport { useFocusoutDismiss } from '../../composables/useFocusoutDismiss'\nimport { useInfiniteScroll } from '../../composables/useInfiniteScroll'\nimport { selectionOf, useListbox } from '../../composables/useListbox'\n\nimport { canClear } from '../../composables/useClearable'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\n\nimport { useDebouncedSearch } from '../../composables/useDebouncedSearch'\nimport { useMessages } from '../../i18n/state'\n\n/** One thing that can be chosen. */\nexport type ComboboxOption = ListboxOption\n\n/**\n * A named block of options, the equivalent of the grouping a native list offers. A\n * group none of whose options survive the search disappears entirely, its name\n * included.\n */\nexport type ComboboxGroup = ListboxGroup\n\n/**\n * A rule drawn between two blocks of options. It is purely decorative, and a separator the\n * search leaves stranded (at the top, at the bottom, or against another one) is simply not\n * drawn.\n */\nexport type ComboboxSeparator = ListboxSeparator\n\n/** Anything the list may hold: an option, a named block, or a separator. */\nexport type ComboboxItem = ListboxItem\n\n/**\n * How the list is narrowed as one types: by the component itself, not at all (when the\n * options already arrive filtered by a server), or by a rule of your own.\n */\nexport type ComboboxFilter = boolean | ((option: ComboboxOption, query: string) => boolean)\n\n/** Where the list opens relative to the field. */\nexport type ComboboxPlacement =\n 'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end'\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type ComboboxSize = 'sm' | 'md' | 'lg'\n\n/**\n * How the chosen values are shown in a multiple field: one chip each, or their labels\n * joined by commas.\n */\nexport type ComboboxDisplay = 'chip' | 'text'\n\n/** What the `#option` slot receives. */\nexport type ComboboxOptionSlotProps = ListboxOptionSlotProps\n\n/** What the `#chip` slot receives. */\nexport type ComboboxChipSlotProps = ListboxChipSlotProps\n\n/** What the `#overflow` slot receives. */\nexport type ComboboxOverflowSlotProps = ListboxOverflowSlotProps\n\n/** What the `#empty` slot receives. */\nexport interface ComboboxEmptySlotProps {\n query: string\n}\n\ninterface ComboboxProps {\n /**\n * What the list offers. An entry may be an option, a named block of options, or a\n * separator; a plain list of options remains perfectly valid.\n */\n options: ComboboxItem[]\n /**\n * Allows several values to be chosen, which makes the value a list and shows what has\n * been chosen inside the field, as chips or as text depending on `display`.\n */\n multiple?: boolean\n /**\n * How the chosen values are shown when several can be chosen: one dismissible chip each, or\n * their labels joined by commas on a single line, cut short with an ellipsis when they run\n * out of room.\n */\n display?: ComboboxDisplay\n /**\n * How many chosen values to show before the rest are summed up as \"+X\", as chips or as text\n * alike. Left out, or set to 0, every value is shown.\n */\n max?: number\n /**\n * Rephrases the \"+X\" standing for the values beyond `max`, \"+5 products\" for instance.\n * It receives the number of values being hidden.\n */\n overflowText?: (count: number) => string\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field. It is tied to the field for assistive technology,\n * so it is read out along with the label.\n */\n hint?: string\n /**\n * A message saying what is wrong with the value, shown in place of the hint so that the field\n * does not grow. It marks the field invalid, is read along with it, and is announced when it\n * appears or changes.\n */\n error?: string\n /** Marks the field as required: an asterisk follows the label, and the field is `required`. */\n required?: boolean\n /** Hides the label visually. It still names the field for assistive technology. */\n hideLabel?: boolean\n /**\n * Where the label sits: above the field (`top`, the default), or at its start, in a column of\n * its own, moving back above when there is not room for both.\n */\n labelPosition?: FieldLabelPosition\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: ComboboxSize\n /** Takes 4px off the height, as everywhere else in the design system. */\n compact?: boolean\n /** What the field says while nothing is chosen and nothing has been typed. */\n placeholder?: string\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what has been chosen without letting it be changed: nothing can be typed, the list\n * never opens, the chips lose their crosses and no clear cross is offered.\n */\n readonly?: boolean\n /** Marks the field as invalid, for a rule of your own. */\n invalid?: boolean\n /**\n * An icon inside the field, at the start, before the chips when several values can\n * be chosen. It is decorative by default and becomes a real button as soon as a\n * `@click:icon-start` listener is attached, in which case it needs `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * The chevron at the end of the field, which turns as the list opens: an icon name, or an\n * explicit render.\n */\n expandIcon?: IconSource\n /** Offers a cross that empties both the selection and the search. */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n /**\n * The title of the empty state the panel shows when the search matches nothing. It is also\n * what a screen reader hears, even when the `#empty` slot draws something else: set both\n * together.\n */\n emptyText?: string\n /** How the list is narrowed as one types. */\n filter?: ComboboxFilter\n /**\n * How long to wait before telling the source what is being searched for, in\n * milliseconds. Zero tells it at once, which suits a source that is not a network\n * request.\n */\n searchDebounce?: number\n /**\n * Says that something is being loaded. Either way the field replaces its chevron with a\n * spinner.\n */\n loading?: boolean\n /**\n * What is said while loading, and what the spinner is announced as. A screen reader hears\n * it even when the `#loading` slot draws something else: set both together.\n */\n loadingText?: string\n /**\n * Says that there are more pages to come, which makes the component ask for the next one as\n * the end of the list comes into view.\n */\n hasMore?: boolean\n /**\n * Renders only the rows near the visible part of the panel, for lists of thousands of options.\n * Rows are measured as they render, so a custom `#option` may vary in height. Options are then\n * announced with their position in the list.\n */\n virtual?: boolean\n /** Where the list opens relative to the field. */\n placement?: ComboboxPlacement\n}\n\nconst props = withDefaults(defineProps<ComboboxProps>(), {\n multiple: false,\n display: 'chip',\n max: undefined,\n overflowText: undefined,\n label: undefined,\n hint: undefined,\n error: undefined,\n required: false,\n hideLabel: false,\n labelPosition: 'top',\n size: 'md',\n compact: false,\n placeholder: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n iconStart: undefined,\n iconStartLabel: undefined,\n expandIcon: () => expandMoreIcon,\n clearable: false,\n clearLabel: undefined,\n emptyText: undefined,\n filter: true,\n searchDebounce: 250,\n loading: false,\n loadingText: undefined,\n hasMore: false,\n virtual: false,\n placement: 'bottom-start',\n})\n\nconst m = useMessages()\nconst resolvedEmptyText = computed(() => props.emptyText ?? m.value.combobox.empty)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.combobox.clear)\nconst resolvedLoadingText = computed(() => props.loadingText ?? m.value.common.loading)\n\nconst emit = defineEmits<{\n /** What is being searched for, to be sent to the source. */\n search: [query: string]\n /** The end of the list has come into view: send the next page. */\n 'load-more': []\n /** The clear cross emptied the selection and the search. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after the chips standing for the chosen\n * values rather than in their place.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, placed before the ones the field owns: the clear\n * cross and the chevron.\n */\n 'value-end'?(): unknown\n /** What a row of the list shows, in place of the plain label: a subtitle, an avatar, a badge. */\n option?(props: ComboboxOptionSlotProps): unknown\n /** Replaces the chip standing for one chosen value. */\n chip?(props: ComboboxChipSlotProps): unknown\n /** Replaces the \"+X\" standing for the values beyond `max`. */\n overflow?(props: ComboboxOverflowSlotProps): unknown\n /** What the panel shows when nothing matches. It receives the term that was searched. */\n empty?(props: ComboboxEmptySlotProps): unknown\n /** What the panel shows while loading its first options. */\n loading?(): unknown\n}>()\n\n// Everything below reads the RESOLVED values and never the props: the chips, the panel and the\n// field would otherwise come out at three different scales inside the same row.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n// The size, the density and the HEIGHT of the chips sitting inside the field, worked out\n// once in `utils/chip.ts` and shared with VFileInput. The height is set inline below\n// rather than restated as a table of CSS rules: it is the height the field forces on its\n// input, and deriving both from the same pair is what stops them drifting apart.\nconst chipScale = computed(() => chipScaleFor(resolvedSize.value, resolvedCompact.value))\n\n/** The chosen option's `value`, or the list of them when `multiple` is set. */\nconst model = defineModel<ItemValue | ItemValue[]>({ default: '' })\n\n// `class` and `style` stay on the wrapper, where a consumer expects to style the component;\n// everything else; a name above all; goes down to the text field, which is the element\n// assistive technology treats as the combobox.\ndefineOptions({ inheritAttrs: false })\nconst { rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand; and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\n/** What reaches the field: the consumer's own attributes, plus that listener. */\nconst fieldAttrs = computed(() => ({ ...forwardedAttrs.value, ...iconStartClick }))\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\n// One generated identifier serves twice: as the panel's own id, which the field points\n// at, and as the prefix of every option's id. The two strings can never collide, one\n// being a prefix of longer ones.\nconst optionsId = useId()\n\nconst open = ref(false)\nconst query = ref('')\nconst focused = ref(false)\n// The text in the field serves two purposes at once: it SHOWS the chosen label when a single\n// value is picked, and it is what one searches with. This flag tells the two apart; while it is\n// false the text is merely a label and narrows nothing, so reopening the panel offers the whole\n// list again.\nconst typed = ref(false)\n\nconst selectedValues = computed(() => selectionOf(model.value, props.multiple))\n\n/*\n * Reading the label on every call is what keeps the filter reactive: flattening the options\n * only ever touches the containers, so a label changed in place would go unnoticed by anything\n * keyed on the list.\n */\nconst normalizedOf = createNormalizedCache<ComboboxOption>()\nconst normalizedLabelOf = (option: ComboboxOption) => normalizedOf(option, option.label)\n\n// ONE search term, read both by the local filter and by what is sent to the source: the\n// list on screen and the request in flight therefore cannot describe different searches.\n// With nothing typed the term is empty, so a label merely being displayed never narrows\n// the list.\nconst searchTerm = computed(() => (typed.value ? query.value.trim() : ''))\n\n// Closing empties `searchTerm` (the typing flag drops) in the same tick as it hides the panel,\n// but the panel fades out rather than vanishing. Filtered on the live term, the list would\n// spring back to every option during that fade, and the panel grow under the pointer that just\n// chose.\nconst closedTerm = ref<string | null>(null)\n\nconst panelRef = ref<InstanceType<typeof VListboxPanel> | null>(null)\n\nconst {\n allOptions,\n visible: filtered,\n optionOf,\n labelOf,\n remember,\n activeIndex,\n optionId,\n highlightFirst,\n highlightSelected,\n move,\n hover,\n blocks,\n measureRow,\n} = useListbox({\n items: () => props.options,\n filter: (list) => {\n const matcher = props.filter\n if (matcher === false) return list\n const q = closedTerm.value ?? searchTerm.value\n if (!q) return list\n if (typeof matcher === 'function') return list.filter((o) => matcher(o, q))\n const needle = normalizeText(q)\n return list.filter((o) => normalizedLabelOf(o).includes(needle))\n },\n selected: () => selectedValues.value,\n open: () => open.value,\n id: optionsId,\n panelEl: () => panelRef.value?.el ?? null,\n virtual: () => props.virtual,\n hasMore: () => props.hasMore,\n size: () => resolvedSize.value,\n compact: () => resolvedCompact.value,\n})\n\n// @a11y\n/**\n * What is announced about the state of the panel; that nothing matches, or that something is\n * loading.\n */\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: (term) => emit('search', term),\n})\n\nwatch(searchTerm, (term) => {\n if (!open.value) return\n // A new search means a new list, so the highlight cannot survive it: with an outside source\n // the options are replaced wholesale, and the old position would name a completely unrelated\n // option. It is moved to the first available option rather than dropped altogether, and that\n // matters when the filtering is left to the source: the visible list keeps the very same\n // reference until the answer arrives, so the watcher below would never fire and Enter would\n // do nothing in the meantime.\n highlightFirst()\n // The page asked for under the previous term will never arrive, and the first page of the new\n // one may be exactly as long as the list it replaces; which the paging would not recognise as\n // an arrival.\n infiniteScroll.restart()\n emitSearch(term)\n})\n\n// With a single value, the field shows its label as ordinary text whenever it is not being\n// edited. Read through `selectedValues`, which knows that a number is a value even at zero: a\n// test on the string type would leave a numeric value unlabelled.\nfunction singleLabel() {\n const value = props.multiple ? undefined : selectedValues.value[0]\n return value === undefined ? '' : labelOf(value)\n}\n\nquery.value = singleLabel()\n\n// Refresh copied selection labels when options arrive or the parent updates the value.\n// Post-flush timing reads the accepted model after rendering.\nwatch(\n [allOptions, selectedValues],\n () => {\n if (props.multiple || open.value || typed.value) return\n query.value = singleLabel()\n },\n { flush: 'post' },\n)\n\n/** The chosen values are spelled out as one line of text rather than drawn as chips. */\nconst textDisplay = computed(() => props.multiple && props.display === 'text')\n\n// A read-only TEXT display keeps it folded under the focus too: nothing can be typed, and the\n// input's share of the row would only cut the line of values short for nothing.\nconst collapsed = computed(\n () =>\n props.multiple &&\n selectedValues.value.length > 0 &&\n (!focused.value || (textDisplay.value && props.readonly)),\n)\n\n// The field cannot work that out for itself, since the chips live outside its value, so the\n// answer is given to it explicitly, and the room for the cross is reserved accordingly.\nconst clearVisible = computed(() =>\n canClear(\n props,\n resolvedDisabled.value,\n selectedValues.value.length > 0 || query.value.length > 0,\n ),\n)\n\n// The single cut-off point of a frozen field: every route into the list; a click on the\n// control, the focus, a keystroke, typing; ends up here, so `readonly` is refused once rather\n// than guarded in each handler.\nfunction openPanel() {\n if (resolvedDisabled.value || props.readonly || open.value) return\n closedTerm.value = null\n open.value = true\n highlightSelected()\n // Told to the source after the panel is marked open; the delayed emission checks that flag\n // before firing; which lets an outside source load its first page as the panel appears.\n emitSearch(searchTerm.value, true)\n}\n\n// Asking for the next page as the end of the list is reached. The observer itself, its\n// re-arming after each page and the lock that stops it firing twice all live in\n// `useInfiniteScroll`; it is declared here so that closing the panel can reset it.\nconst sentinelEl = ref<HTMLElement | null>(null)\n\nconst infiniteScroll = useInfiniteScroll({\n sentinelEl,\n // Found by its ARIA role, which is public API, rather than by an internal class.\n root: (sentinel) => sentinel.closest('[role=\"listbox\"]'),\n canLoad: () => open.value && props.hasMore && !props.loading,\n loaded: () => allOptions.value,\n onLoadMore: () => emit('load-more'),\n})\n\n/**\n * Closes the list. The field then shows `label`: the chosen value's, unless the caller passes the\n * label of the option it has just chosen, which the value read back may not yet carry.\n */\nfunction closePanel(label = singleLabel()) {\n if (!open.value) return\n cancelSearch()\n // A page that never arrived; a request that failed; must not leave the paging frozen for\n // good, so the lock is released here.\n infiniteScroll.reset()\n closedTerm.value = searchTerm.value\n open.value = false\n activeIndex.value = -1\n typed.value = false\n query.value = label\n}\n\nwatch(\n () => props.readonly || resolvedDisabled.value,\n (frozen) => {\n if (frozen) closePanel()\n },\n)\n\n/**\n * Closes as soon as the focus leaves the component; the panel included, which is a descendant\n * of it even while floating above the page.\n */\nconst onFocusout = useFocusoutDismiss(rootEl, () => closePanel())\n\n/** The reader is typing: the text becomes a search, and the panel opens. */\nfunction onInput() {\n typed.value = true\n openPanel()\n}\n\n/**\n * With a single value chosen, the label shown in the field is selected on focus, so that typing\n * replaces it rather than appending to it; and until something IS typed, the whole list stays\n * on offer.\n */\nfunction selectQuery() {\n if (!props.multiple && query.value) inputRef.value?.select()\n}\n\nfunction onFocus() {\n focused.value = true\n selectQuery()\n}\n\n/** Whether a pointer event landed on the chevron, or on the spinner standing in its place. */\nconst onChevron = (event: Event) =>\n event.target instanceof Element &&\n event.target.closest('.v-combobox-chevron, .v-combobox-spinner') !== null\n\n// @core\n/*\n * The chevron is not focusable, so pressing it would hand the focus to the page: the root's\n * `focusout` then closes the panel, and the click that follows reopens it. A click meant to\n * close would flash the list instead.\n */\nfunction onControlMousedown(event: MouseEvent) {\n if (onChevron(event)) event.preventDefault()\n}\n\n/**\n * A click anywhere on the field focuses it and opens the panel; except on the chevron of an\n * open panel, which closes it.\n */\nfunction onControlClick(event: MouseEvent) {\n if (resolvedDisabled.value) return\n inputRef.value?.focus()\n if (open.value && onChevron(event)) {\n closePanel()\n return\n }\n openPanel()\n // Selected again after the click, which has just placed the caret somewhere in the middle of\n // the label.\n selectQuery()\n}\n\nfunction select(option: ComboboxOption) {\n if (option.disabled || props.readonly || resolvedDisabled.value) return\n // Remembered immediately: the option may vanish from the list; on the next search; before the\n // parent has even passed the new value back down.\n remember(option)\n if (props.multiple) {\n typed.value = false\n model.value = toggleValue(selectedValues.value, option.value)\n query.value = ''\n inputRef.value?.focus()\n } else {\n model.value = option.value\n closePanel(option.label)\n }\n}\n\nfunction removeValue(value: ItemValue) {\n if (!props.multiple || props.readonly || resolvedDisabled.value) return\n model.value = selectedValues.value.filter((v) => v !== value)\n inputRef.value?.focus()\n}\n\n/**\n * The clear cross was pressed. The field has already emptied the text it holds; what is left to\n * empty is the selection itself; the single value, or all the chips.\n */\nfunction onClear() {\n model.value = props.multiple ? [] : ''\n typed.value = false\n // The list may still be open (a pointer press on the cross does not close it): the\n // highlight is moved to the first option rather than dropped, or Enter would do nothing.\n if (open.value) highlightFirst()\n else activeIndex.value = -1\n emit('clear')\n}\n\n// @keyboard @a11y\n// Naming the current option and never the focus itself, which stays in the field throughout;\n// Backspace on an empty field takes back the last chip, the usual gesture in a field holding\n// several values.\nfunction onKeydown(event: KeyboardEvent) {\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n if (open.value) move(1)\n else openPanel()\n break\n case 'ArrowUp':\n event.preventDefault()\n if (open.value) move(-1)\n else openPanel()\n break\n case 'Enter':\n if (open.value && activeIndex.value >= 0) {\n event.preventDefault()\n const option = filtered.value[activeIndex.value]\n if (option) select(option)\n }\n break\n case 'Escape':\n // Consumed only when it closes the list, so a surrounding VDialog stays open; on a\n // closed list it is left to go on and close the dialog.\n if (open.value) {\n event.preventDefault()\n closePanel()\n }\n break\n case 'Tab':\n closePanel()\n break\n case 'Backspace':\n if (props.multiple && !query.value) {\n const last = selectedValues.value.at(-1)\n if (last !== undefined) removeValue(last)\n }\n break\n }\n}\n\ndefineExpose({\n /** Moves the focus to the search field. */\n focus: (options?: FocusOptions) => inputRef.value?.focus(options),\n /** Selects what has been typed in the search field. */\n select: () => inputRef.value?.select(),\n /** The real `<input>` behind the field, for what neither of the two above covers. */\n el: computed(() => inputRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-combobox\"\n :class=\"rootClass\"\n :style=\"[{ '--chip-height': chipScale.height }, rootStyle]\"\n :data-size=\"resolvedSize\"\n :data-compact=\"resolvedCompact ? '' : undefined\"\n :data-multiple=\"multiple ? '' : undefined\"\n :data-collapsed=\"collapsed ? '' : undefined\"\n :data-open=\"open ? '' : undefined\"\n @focusout=\"onFocusout\"\n >\n <div class=\"v-combobox-control\" @mousedown=\"onControlMousedown\" @click=\"onControlClick\">\n <!--\n The two arrangements VInput offers a field holding chips: its end controls lifted out of\n the flow, always; the chevron and the cross stay put whether or not the input has folded\n away; and the chips wrapping, when there are chips at all. A text display keeps the\n field on one line and needs only the first.\n -->\n <VInput\n ref=\"inputRef\"\n v-model=\"query\"\n class=\"v-input-end-pinned\"\n :class=\"{ 'v-input-chips': multiple && !textDisplay }\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-autocomplete=\"list\"\n :aria-expanded=\"open\"\n :aria-controls=\"optionsId\"\n v-bind=\"fieldAttrs\"\n :label=\"label\"\n :hint=\"hint\"\n :error=\"error\"\n :required=\"required\"\n :hide-label=\"hideLabel\"\n :label-position=\"labelPosition\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :invalid=\"invalid\"\n :disabled=\"resolvedDisabled\"\n :readonly=\"readonly\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :placeholder=\"selectedValues.length === 0 ? placeholder : undefined\"\n :aria-activedescendant=\"open && activeIndex >= 0 ? optionId(activeIndex) : undefined\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @focus=\"onFocus\"\n @blur=\"focused = false\"\n @clear=\"onClear\"\n >\n <template v-if=\"multiple || $slots.start\" #start>\n <VListboxValues\n v-if=\"multiple\"\n :values=\"selectedValues\"\n :display=\"display\"\n :max=\"max\"\n :collapsed=\"collapsed\"\n :overflow-text=\"overflowText\"\n :scale=\"chipScale\"\n :removable=\"!readonly\"\n :disabled=\"resolvedDisabled\"\n :label-of=\"labelOf\"\n :option-of=\"optionOf\"\n @remove=\"removeValue\"\n >\n <template v-if=\"$slots.chip\" #chip=\"chipProps\">\n <slot name=\"chip\" v-bind=\"chipProps\" />\n </template>\n <template v-if=\"$slots.overflow\" #overflow=\"overflowProps\">\n <slot name=\"overflow\" v-bind=\"overflowProps\" />\n </template>\n </VListboxValues>\n <slot name=\"start\" />\n </template>\n\n <template v-if=\"$slots['value-end']\" #value-end><slot name=\"value-end\" /></template>\n\n <!--\n The chevron sits at the end of the field and turns as the panel opens; the clear cross\n is rendered by the field itself, to its left. While loading, the spinner takes EXACTLY\n the chevron's place; the field gives a spinner among its direct children the size of\n an icon; so nothing shifts.\n -->\n <template #end>\n <VSpinner v-if=\"loading\" class=\"v-combobox-spinner v-input-icon-end\" aria-hidden=\"true\" />\n <VListboxChevron v-else class=\"v-combobox-chevron\" :icon=\"expandIcon\" :open=\"open\" />\n </template>\n </VInput>\n </div>\n\n <VListboxPanel\n :id=\"optionsId\"\n ref=\"panelRef\"\n v-model:open=\"open\"\n anchor=\"--combobox-anchor\"\n :placement=\"placement\"\n class=\"v-combobox-panel\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :multiple=\"multiple\"\n :virtual=\"virtual\"\n :blocks=\"blocks\"\n :measure=\"measureRow\"\n @select=\"select\"\n @highlight=\"hover\"\n >\n <template v-if=\"$slots.option\" #option=\"slotProps\">\n <slot name=\"option\" v-bind=\"slotProps\" />\n </template>\n\n <!--\n The order matters: loading is checked before emptiness, so that a panel waiting for an\n answer never claims there is no result. Both are hidden from screen readers, and that is\n not an oversight.\n -->\n <div v-if=\"loading && filtered.length === 0\" class=\"v-combobox-state\" aria-hidden=\"true\">\n <slot name=\"loading\">\n <VSpinner :label=\"resolvedLoadingText\" />\n <span>{{ resolvedLoadingText }}</span>\n </slot>\n </div>\n <div v-else-if=\"filtered.length === 0\" class=\"v-combobox-state\" aria-hidden=\"true\">\n <slot name=\"empty\" :query=\"searchTerm\">\n <VEmptyState\n size=\"sm\"\n class=\"v-combobox-empty\"\n :icon=\"searchTerm ? searchOffIcon : inboxIcon\"\n :title=\"resolvedEmptyText\"\n />\n </slot>\n </div>\n\n <!--\n It has to stay a SINGLE element: rendering the spinner conditionally in its place would\n destroy the marker and silently cancel the observation, and no further page would ever\n be asked for.\n -->\n <div v-if=\"hasMore\" ref=\"sentinelEl\" class=\"v-combobox-more\" aria-hidden=\"true\">\n <VSpinner v-if=\"loading\" :label=\"resolvedLoadingText\" />\n </div>\n </VListboxPanel>\n\n <!-- The spoken counterpart of the two panel states above. It sits OUTSIDE the\n listbox, being exactly the kind of content a listbox may not contain. -->\n <span class=\"v-visually-hidden\" role=\"status\">{{ stateAnnouncement }}</span>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-combobox {\n /*\n * It is declared on the root because that is the common ancestor of the field and the\n * panel; a panel drawn above the page remains a descendant of it in the document.\n */\n anchor-scope: --combobox-anchor;\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n .v-combobox-control {\n display: block;\n }\n\n /*\n * The anchor is the FIELD's box and not this wrapper's, which also holds the label and the\n * hint: anchored to the wrapper, the panel opens a hint's height below the field, and a\n * label's height above it once there is no room below and `flip-block` turns it over. Against\n * the field it covers whichever of the two it lands on, which a panel belonging to a control\n * is supposed to do.\n */\n .v-combobox-control .v-input-field {\n anchor-name: --combobox-anchor;\n }\n\n .v-combobox-panel {\n max-block-size: var(--vectis-control-size-combobox-list-max-block);\n }\n\n /*\n * The chevron and the clear cross are pinned to the end of the field by VInput's\n * `.v-input-end-pinned`, which also reserves their room. The chevron and the spinner take\n * turns in the same place and occupy the same box; the field gives a spinner among its direct\n * children the size of an icon; so swapping one for the other shifts nothing.\n */\n .v-combobox-spinner {\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * It remains in the page and remains focusable; clicking the field or tabbing into it brings\n * it straight back, the folded state ending as soon as it has the focus.\n */\n .v-combobox[data-collapsed] .v-input-control {\n position: absolute;\n width: 0;\n height: 0;\n padding: 0;\n }\n\n /*\n * Under the focus the two share the row, and the line is capped at half of it so that a long\n * selection cannot squeeze what is being typed down to nothing. Folded, the input is out of\n * the flow and the line takes the whole field.\n */\n .v-combobox:not([data-collapsed]) .v-listbox-text {\n max-inline-size: 50%;\n }\n\n /*\n * An unbroken line has the whole of its text as its min-content width, and that width travels\n * up through the field to the root. In a grid track or a flex row sized `auto`, the component\n * then widens its container to fit every label instead of cutting them short, and the\n * ellipsis never shows.\n */\n .v-combobox:has(.v-listbox-text) {\n min-inline-size: 0;\n }\n\n /* The rows saying \"nothing matches\" or \"loading\" are built like an option: they read\n the same inherited dimensions from the panel, and the spinner follows through the\n same block's icon context. They refuse to shrink, the panel being a bounded column\n that would otherwise squash them. */\n .v-combobox-state {\n display: flex;\n flex: none;\n align-items: center;\n gap: var(--control-gap);\n min-height: 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 /*\n * The default empty state spans the panel. `[data-size]` lifts the selector above VEmptyState's\n * own size rules, whose sheet may load after this one.\n */\n .v-combobox-state > .v-combobox-empty[data-size] {\n flex: 1;\n padding-block: var(--vectis-space-3);\n padding-inline: 0;\n }\n\n /*\n * The foot of the list: the marker watched for the end, and the place the next-page spinner\n * appears. It is given a real height on purpose; a box of no height at all makes the crossing\n * it is watched for unreliable.\n */\n .v-combobox-more {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: center;\n min-height: var(--vectis-control-height-xs);\n color: var(--vectis-color-text-muted);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-combobox-chevron {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
@layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
|
|
2
|
-
@layer vectis.components{.v-command-palette{--command-palette-width:var(--vectis-control-size-command-palette-width);--command-palette-inset:calc(var(--vectis-space-2) + var(--vectis-space-4));inline-size:var(--command-palette-width);max-inline-size:calc(100dvi - 2 * var(--vectis-space-4));max-block-size:calc(100dvb - var(--vectis-control-size-command-palette-offset) - var(--vectis-space-4));margin:var(--vectis-control-size-command-palette-offset) auto auto;border-radius:var(--vectis-radius-overlay);background:var(--vectis-color-surface-overlay);color:var(--vectis-color-text);box-shadow:var(--vectis-shadow-xl);font-family:var(--vectis-text-family);border:none;flex-direction:column;padding:0;display:flex;overflow:hidden}.v-command-palette:not([open]){display:none}.v-command-palette:focus-visible{outline:none}.v-command-palette-search{align-items:center;gap:var(--vectis-space-2);min-block-size:var(--vectis-control-height-xl);padding-inline:var(--command-palette-inset);border-block-end:1px solid var(--vectis-color-border);--vectis-icon-size:var(--vectis-icon-size-md);flex:none;display:flex}.v-command-palette-search:has(.v-command-palette-input:focus-visible){border-block-end-color:var(--vectis-focus-ring-color)}.v-command-palette-search-icon,.v-command-palette-spinner{color:var(--vectis-color-text-muted);flex:none}.v-command-palette-input{min-inline-size:0;color:inherit;font-family:inherit;font-size:var(--vectis-text-body-lg-size);font-weight:var(--vectis-text-body-lg-weight);line-height:var(--vectis-text-body-lg-leading);background:0 0;border:none;outline:none;flex:1;padding:0}.v-command-palette-input::placeholder{color:var(--vectis-color-text-subtle);opacity:1}.v-command-palette-list{gap:var(--vectis-space-1);min-block-size:0;max-block-size:var(--vectis-control-size-command-palette-list-max-block);overscroll-behavior:contain;padding:var(--vectis-space-2);scroll-padding-block:var(--vectis-space-2);flex-direction:column;flex:auto;display:flex;overflow-y:auto}.v-command-palette-group{gap:var(--vectis-space-1);flex-direction:column;flex:none;display:flex}.v-command-palette-list>.v-command-palette-item{flex:none}.v-command-palette-group-label{min-block-size:var(--control-height);padding-inline:var(--control-padding-inline);font-size:var(--vectis-text-overline-size);font-weight:var(--vectis-text-overline-weight);letter-spacing:var(--vectis-text-overline-tracking);color:var(--vectis-color-text-muted);align-items:center;display:flex}.v-separator.v-command-palette-separator{margin-inline:calc(-1 * var(--vectis-space-2));flex:none}.v-command-palette-state{justify-content:center;align-items:center;gap:var(--control-gap);min-block-size:var(--control-height);padding:var(--vectis-space-1) var(--control-padding-inline);font-size:var(--control-font-size);color:var(--vectis-color-text-muted);flex:none;display:flex}.v-command-palette-state>.v-command-palette-empty[data-size]{padding-block:var(--vectis-space-4);flex:1;padding-inline:0}.v-command-palette-footer{padding:var(--vectis-space-2) var(--command-palette-inset);border-block-start:1px solid var(--vectis-color-border);font-size:var(--vectis-text-caption-size);line-height:var(--vectis-text-caption-leading);color:var(--vectis-color-text-muted);flex:none;align-items:center;display:flex}.v-command-palette-hints{gap:var(--vectis-space-1) var(--vectis-space-4);flex-wrap:wrap;display:flex}.v-command-palette-hint{align-items:center;gap:var(--vectis-space-1);display:inline-flex}.v-command-palette{transition:opacity var(--vectis-duration-base) var(--vectis-ease-default), transform var(--vectis-duration-base) var(--vectis-ease-default)}@starting-style{.v-command-palette[open]{opacity:0;transform:scale(.97)}}.v-command-palette::backdrop{background:var(--vectis-color-backdrop);transition:opacity var(--vectis-duration-base) var(--vectis-ease-default)}@starting-style{.v-command-palette[open]::backdrop{opacity:0}}@media (prefers-reduced-motion:reduce){.v-command-palette,.v-command-palette::backdrop{transition:none}}@media (forced-colors:active){.v-command-palette{outline:1px solid canvastext}}}
|
|
2
|
+
@layer vectis.components{.v-command-palette{--command-palette-width:var(--vectis-control-size-command-palette-width);--command-palette-inset:calc(var(--vectis-space-2) + var(--vectis-space-4));inline-size:var(--command-palette-width);max-inline-size:calc(100dvi - 2 * var(--vectis-space-4));max-block-size:calc(100dvb - var(--vectis-control-size-command-palette-offset) - var(--vectis-space-4));margin:var(--vectis-control-size-command-palette-offset) auto auto;border-radius:var(--vectis-radius-overlay);background:var(--vectis-color-surface-overlay);color:var(--vectis-color-text);box-shadow:var(--vectis-shadow-xl);font-family:var(--vectis-text-family);border:none;flex-direction:column;padding:0;display:flex;overflow:hidden}.v-command-palette:not([open]){display:none}.v-command-palette:focus-visible{outline:none}.v-command-palette-search{align-items:center;gap:var(--vectis-space-2);min-block-size:var(--vectis-control-height-xl);padding-inline:var(--command-palette-inset);border-block-end:1px solid var(--vectis-color-border);--vectis-icon-size:var(--vectis-icon-size-md);flex:none;display:flex}.v-command-palette-search:has(.v-command-palette-input:focus-visible){border-block-end-color:var(--vectis-focus-ring-color)}.v-command-palette-search-icon,.v-command-palette-spinner{color:var(--vectis-color-text-muted);flex:none}.v-command-palette-input{min-inline-size:0;color:inherit;font-family:inherit;font-size:var(--vectis-text-body-lg-size);font-weight:var(--vectis-text-body-lg-weight);line-height:var(--vectis-text-body-lg-leading);background:0 0;border:none;outline:none;flex:1;padding:0}.v-command-palette-input::placeholder{color:var(--vectis-color-text-subtle);opacity:1}.v-command-palette-list{gap:var(--vectis-space-1);min-block-size:0;max-block-size:var(--vectis-control-size-command-palette-list-max-block);overscroll-behavior:contain;padding:var(--vectis-space-2);scroll-padding-block:var(--vectis-space-2);flex-direction:column;flex:auto;display:flex;overflow-y:auto}.v-command-palette-group{gap:var(--vectis-space-1);flex-direction:column;flex:none;display:flex}.v-command-palette-list>.v-command-palette-item{flex:none}.v-command-palette-group-label{min-block-size:var(--control-height);padding-inline:var(--control-padding-inline);font-size:var(--vectis-text-overline-size);font-weight:var(--vectis-text-overline-weight);letter-spacing:var(--vectis-text-overline-tracking);color:var(--vectis-color-text-muted);align-items:center;display:flex}.v-separator.v-command-palette-separator{margin-inline:calc(-1 * var(--vectis-space-2));flex:none}.v-command-palette-state{justify-content:center;align-items:center;gap:var(--control-gap);min-block-size:var(--control-height);padding:var(--vectis-space-1) var(--control-padding-inline);font-size:var(--control-font-size);color:var(--vectis-color-text-muted);flex:none;display:flex}.v-command-palette-state>.v-command-palette-empty[data-size]{padding-block:var(--vectis-space-4);flex:1;padding-inline:0}.v-command-palette-footer{padding:var(--vectis-space-2) var(--command-palette-inset);border-block-start:1px solid var(--vectis-color-border);font-size:var(--vectis-text-caption-size);line-height:var(--vectis-text-caption-leading);color:var(--vectis-color-text-muted);flex:none;align-items:center;display:flex}@media not (any-pointer:fine){.v-command-palette-footer:has(>.v-command-palette-hints){display:none}}.v-command-palette-hints{gap:var(--vectis-space-1) var(--vectis-space-4);flex-wrap:wrap;display:flex}.v-command-palette-hint{align-items:center;gap:var(--vectis-space-1);display:inline-flex}.v-command-palette{transition:opacity var(--vectis-duration-base) var(--vectis-ease-default), transform var(--vectis-duration-base) var(--vectis-ease-default)}@starting-style{.v-command-palette[open]{opacity:0;transform:scale(.97)}}.v-command-palette::backdrop{background:var(--vectis-color-backdrop);transition:opacity var(--vectis-duration-base) var(--vectis-ease-default)}@starting-style{.v-command-palette[open]::backdrop{opacity:0}}@media (prefers-reduced-motion:reduce){.v-command-palette,.v-command-palette::backdrop{transition:none}}@media (forced-colors:active){.v-command-palette{outline:1px solid canvastext}}}
|