|
|
|
@ -1,6 +1,7 @@
|
|
|
|
<script setup lang="ts">
|
|
|
|
<script setup lang="ts">
|
|
|
|
import { computed } from 'vue'
|
|
|
|
import { computed, nextTick, ref, useId, watch } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import CButton from '../button/CButton.vue'
|
|
|
|
import type { CFormControlSize } from '../form/types'
|
|
|
|
import type { CFormControlSize } from '../form/types'
|
|
|
|
import { useFormControl } from '../form/useFormControl'
|
|
|
|
import { useFormControl } from '../form/useFormControl'
|
|
|
|
import type {
|
|
|
|
import type {
|
|
|
|
@ -21,6 +22,8 @@ const props = withDefaults(
|
|
|
|
optionValue?: CSelectValueAccessor
|
|
|
|
optionValue?: CSelectValueAccessor
|
|
|
|
optionKey?: CSelectKeyAccessor
|
|
|
|
optionKey?: CSelectKeyAccessor
|
|
|
|
placeholder?: string
|
|
|
|
placeholder?: string
|
|
|
|
|
|
|
|
clearable?: boolean
|
|
|
|
|
|
|
|
filterable?: boolean
|
|
|
|
size?: CFormControlSize
|
|
|
|
size?: CFormControlSize
|
|
|
|
disabled?: boolean
|
|
|
|
disabled?: boolean
|
|
|
|
required?: boolean
|
|
|
|
required?: boolean
|
|
|
|
@ -33,6 +36,8 @@ const props = withDefaults(
|
|
|
|
optionValue: undefined,
|
|
|
|
optionValue: undefined,
|
|
|
|
optionKey: undefined,
|
|
|
|
optionKey: undefined,
|
|
|
|
placeholder: undefined,
|
|
|
|
placeholder: undefined,
|
|
|
|
|
|
|
|
clearable: false,
|
|
|
|
|
|
|
|
filterable: false,
|
|
|
|
size: 'medium',
|
|
|
|
size: 'medium',
|
|
|
|
disabled: false,
|
|
|
|
disabled: false,
|
|
|
|
required: false,
|
|
|
|
required: false,
|
|
|
|
@ -42,10 +47,18 @@ const props = withDefaults(
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
const emit = defineEmits<{
|
|
|
|
'update:modelValue': [value: CSelectValue | null]
|
|
|
|
'update:modelValue': [value: CSelectValue | null]
|
|
|
|
|
|
|
|
clear: []
|
|
|
|
}>()
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const { controlId, describedBy, invalid, required } = useFormControl(props)
|
|
|
|
const { controlId, describedBy, invalid, required } = useFormControl(props)
|
|
|
|
const objectKeys = new WeakMap<object, number>()
|
|
|
|
const objectKeys = new WeakMap<object, number>()
|
|
|
|
|
|
|
|
const listboxId = `c-select-listbox-${useId()}`
|
|
|
|
|
|
|
|
const inputElement = ref<HTMLInputElement | null>(null)
|
|
|
|
|
|
|
|
const listElement = ref<HTMLUListElement | null>(null)
|
|
|
|
|
|
|
|
const isOpen = ref(false)
|
|
|
|
|
|
|
|
const isFiltering = ref(false)
|
|
|
|
|
|
|
|
const query = ref('')
|
|
|
|
|
|
|
|
const highlightedKey = ref<string | number | null>(null)
|
|
|
|
let nextObjectKey = 0
|
|
|
|
let nextObjectKey = 0
|
|
|
|
|
|
|
|
|
|
|
|
const selection = computed({
|
|
|
|
const selection = computed({
|
|
|
|
@ -91,7 +104,6 @@ const visibleOptions = computed(() =>
|
|
|
|
const record = source as Record<string, unknown>
|
|
|
|
const record = source as Record<string, unknown>
|
|
|
|
if (record.hidden) return []
|
|
|
|
if (record.hidden) return []
|
|
|
|
const value = resolveValue(source, props.optionValue)
|
|
|
|
const value = resolveValue(source, props.optionValue)
|
|
|
|
|
|
|
|
|
|
|
|
const explicitKey = props.optionKey
|
|
|
|
const explicitKey = props.optionKey
|
|
|
|
? typeof props.optionKey === 'function'
|
|
|
|
? typeof props.optionKey === 'function'
|
|
|
|
? props.optionKey(source)
|
|
|
|
? props.optionKey(source)
|
|
|
|
@ -115,19 +127,240 @@ const visibleOptions = computed(() =>
|
|
|
|
|
|
|
|
|
|
|
|
const option = source as CSelectOption
|
|
|
|
const option = source as CSelectOption
|
|
|
|
if (option.hidden) return []
|
|
|
|
if (option.hidden) return []
|
|
|
|
|
|
|
|
|
|
|
|
const valueKey =
|
|
|
|
const valueKey =
|
|
|
|
typeof option.value === 'object'
|
|
|
|
typeof option.value === 'object'
|
|
|
|
? `object:${objectKey(option.value)}`
|
|
|
|
? `object:${objectKey(option.value)}`
|
|
|
|
: `${typeof option.value}:${String(option.value)}:${index}`
|
|
|
|
: `${typeof option.value}:${String(option.value)}:${index}`
|
|
|
|
|
|
|
|
|
|
|
|
return [{ ...option, key: valueKey }]
|
|
|
|
return [{ ...option, key: valueKey }]
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const selectedOption = computed(() =>
|
|
|
|
|
|
|
|
visibleOptions.value.find((option) => Object.is(option.value, selection.value)),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
const selectedLabel = computed(() => selectedOption.value?.label ?? '')
|
|
|
|
|
|
|
|
const filterText = computed(() => (isFiltering.value ? query.value.trim() : ''))
|
|
|
|
|
|
|
|
const filteredOptions = computed(() => {
|
|
|
|
|
|
|
|
const search = filterText.value.toLocaleLowerCase()
|
|
|
|
|
|
|
|
return search
|
|
|
|
|
|
|
|
? visibleOptions.value.filter((option) => option.label.toLocaleLowerCase().includes(search))
|
|
|
|
|
|
|
|
: visibleOptions.value
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
const highlightedIndex = computed(() =>
|
|
|
|
|
|
|
|
filteredOptions.value.findIndex((option) => option.key === highlightedKey.value),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
const activeDescendant = computed(() =>
|
|
|
|
|
|
|
|
highlightedIndex.value >= 0 ? `${listboxId}-option-${highlightedIndex.value}` : undefined,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
|
|
selectedLabel,
|
|
|
|
|
|
|
|
(label) => {
|
|
|
|
|
|
|
|
if (!isOpen.value) query.value = label
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(filteredOptions, () => {
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
|
|
isOpen.value &&
|
|
|
|
|
|
|
|
(highlightedIndex.value < 0 || filteredOptions.value[highlightedIndex.value]?.disabled)
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
highlightFirstEnabled()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(highlightedIndex, (index) => {
|
|
|
|
|
|
|
|
if (index < 0) return
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
|
|
const option = listElement.value?.children[index]
|
|
|
|
|
|
|
|
if (option instanceof HTMLElement) option.scrollIntoView({ block: 'nearest' })
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
|
|
[required, selection, () => props.filterable],
|
|
|
|
|
|
|
|
() => {
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
|
|
inputElement.value?.setCustomValidity(
|
|
|
|
|
|
|
|
props.filterable && required.value && selection.value === null
|
|
|
|
|
|
|
|
? 'Please select an option.'
|
|
|
|
|
|
|
|
: '',
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function highlightFirstEnabled() {
|
|
|
|
|
|
|
|
highlightedKey.value = filteredOptions.value.find((option) => !option.disabled)?.key ?? null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function openList() {
|
|
|
|
|
|
|
|
if (props.disabled) return
|
|
|
|
|
|
|
|
isOpen.value = true
|
|
|
|
|
|
|
|
const selected = selectedOption.value
|
|
|
|
|
|
|
|
highlightedKey.value = selected && !selected.disabled ? selected.key : null
|
|
|
|
|
|
|
|
if (highlightedKey.value === null) highlightFirstEnabled()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function closeList(restoreLabel = true) {
|
|
|
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
|
|
|
isFiltering.value = false
|
|
|
|
|
|
|
|
highlightedKey.value = null
|
|
|
|
|
|
|
|
if (restoreLabel) query.value = selectedLabel.value
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleFocus(event: FocusEvent) {
|
|
|
|
|
|
|
|
openList()
|
|
|
|
|
|
|
|
const input = event.currentTarget as HTMLInputElement
|
|
|
|
|
|
|
|
input.select()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleClick(event: MouseEvent) {
|
|
|
|
|
|
|
|
openList()
|
|
|
|
|
|
|
|
if (!isFiltering.value) (event.currentTarget as HTMLInputElement).select()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleInput(event: Event) {
|
|
|
|
|
|
|
|
query.value = (event.target as HTMLInputElement).value
|
|
|
|
|
|
|
|
isFiltering.value = true
|
|
|
|
|
|
|
|
isOpen.value = true
|
|
|
|
|
|
|
|
nextTick(highlightFirstEnabled)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function moveHighlight(direction: 1 | -1) {
|
|
|
|
|
|
|
|
if (!isOpen.value) {
|
|
|
|
|
|
|
|
openList()
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const options = filteredOptions.value
|
|
|
|
|
|
|
|
if (!options.length) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let index = highlightedIndex.value
|
|
|
|
|
|
|
|
for (let attempt = 0; attempt < options.length; attempt += 1) {
|
|
|
|
|
|
|
|
index = (index + direction + options.length) % options.length
|
|
|
|
|
|
|
|
if (!options[index]?.disabled) {
|
|
|
|
|
|
|
|
highlightedKey.value = options[index]?.key ?? null
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function highlightEdge(edge: 'first' | 'last') {
|
|
|
|
|
|
|
|
if (!isOpen.value) openList()
|
|
|
|
|
|
|
|
const options = edge === 'first' ? filteredOptions.value : [...filteredOptions.value].reverse()
|
|
|
|
|
|
|
|
highlightedKey.value = options.find((option) => !option.disabled)?.key ?? null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function chooseOption(option: (typeof visibleOptions.value)[number]) {
|
|
|
|
|
|
|
|
if (option.disabled) return
|
|
|
|
|
|
|
|
emit('update:modelValue', option.value)
|
|
|
|
|
|
|
|
query.value = option.label
|
|
|
|
|
|
|
|
closeList(false)
|
|
|
|
|
|
|
|
inputElement.value?.focus()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
|
|
|
|
|
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
moveHighlight(event.key === 'ArrowDown' ? 1 : -1)
|
|
|
|
|
|
|
|
} else if (event.key === 'Home' && isOpen.value) {
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
highlightEdge('first')
|
|
|
|
|
|
|
|
} else if (event.key === 'End' && isOpen.value) {
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
highlightEdge('last')
|
|
|
|
|
|
|
|
} else if (event.key === 'Enter') {
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
if (!isOpen.value) openList()
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
const option = filteredOptions.value[highlightedIndex.value]
|
|
|
|
|
|
|
|
if (option) chooseOption(option)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if (event.key === 'Escape' && isOpen.value) {
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
closeList()
|
|
|
|
|
|
|
|
} else if (event.key === 'Tab') {
|
|
|
|
|
|
|
|
closeList()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleFocusOut(event: FocusEvent) {
|
|
|
|
|
|
|
|
const next = event.relatedTarget
|
|
|
|
|
|
|
|
if (!(next instanceof Node) || !(event.currentTarget as HTMLElement).contains(next)) closeList()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function clearSelection() {
|
|
|
|
|
|
|
|
emit('update:modelValue', null)
|
|
|
|
|
|
|
|
query.value = ''
|
|
|
|
|
|
|
|
closeList(false)
|
|
|
|
|
|
|
|
emit('clear')
|
|
|
|
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<template>
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
class="c-select-control"
|
|
|
|
|
|
|
|
:class="{ 'is-clearable': clearable, 'is-filterable': filterable }"
|
|
|
|
|
|
|
|
@focusout="filterable ? handleFocusOut($event) : undefined"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<template v-if="filterable">
|
|
|
|
|
|
|
|
<div class="combobox">
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
ref="inputElement"
|
|
|
|
|
|
|
|
v-bind="$attrs"
|
|
|
|
|
|
|
|
class="c-select search"
|
|
|
|
|
|
|
|
:class="[`is-${size}`, { 'is-invalid': invalid, 'has-placeholder': !query }]"
|
|
|
|
|
|
|
|
:id="controlId"
|
|
|
|
|
|
|
|
:value="query"
|
|
|
|
|
|
|
|
:placeholder="placeholder"
|
|
|
|
|
|
|
|
:disabled="disabled"
|
|
|
|
|
|
|
|
:required="required"
|
|
|
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
|
|
|
role="combobox"
|
|
|
|
|
|
|
|
aria-autocomplete="list"
|
|
|
|
|
|
|
|
:aria-expanded="isOpen ? 'true' : 'false'"
|
|
|
|
|
|
|
|
:aria-controls="listboxId"
|
|
|
|
|
|
|
|
:aria-activedescendant="activeDescendant"
|
|
|
|
|
|
|
|
:aria-required="required ? 'true' : undefined"
|
|
|
|
|
|
|
|
:aria-invalid="invalid ? 'true' : undefined"
|
|
|
|
|
|
|
|
:aria-describedby="describedBy"
|
|
|
|
|
|
|
|
@focus="handleFocus"
|
|
|
|
|
|
|
|
@click="handleClick"
|
|
|
|
|
|
|
|
@input="handleInput"
|
|
|
|
|
|
|
|
@keydown="handleKeydown"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<ul ref="listElement" v-show="isOpen" :id="listboxId" class="options" role="listbox">
|
|
|
|
|
|
|
|
<li
|
|
|
|
|
|
|
|
v-for="(option, index) in filteredOptions"
|
|
|
|
|
|
|
|
:id="`${listboxId}-option-${index}`"
|
|
|
|
|
|
|
|
:key="option.key"
|
|
|
|
|
|
|
|
class="option"
|
|
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
|
|
'is-highlighted': option.key === highlightedKey,
|
|
|
|
|
|
|
|
'is-selected': Object.is(option.value, selection),
|
|
|
|
|
|
|
|
'is-disabled': option.disabled,
|
|
|
|
|
|
|
|
}"
|
|
|
|
|
|
|
|
role="option"
|
|
|
|
|
|
|
|
:aria-selected="Object.is(option.value, selection) ? 'true' : 'false'"
|
|
|
|
|
|
|
|
:aria-disabled="option.disabled ? 'true' : undefined"
|
|
|
|
|
|
|
|
@mousemove="!option.disabled && (highlightedKey = option.key)"
|
|
|
|
|
|
|
|
@mousedown.prevent
|
|
|
|
|
|
|
|
@click="chooseOption(option)"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
|
|
<li v-if="!filteredOptions.length" class="empty" role="presentation">
|
|
|
|
|
|
|
|
No matching options
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<select
|
|
|
|
<select
|
|
|
|
|
|
|
|
v-else
|
|
|
|
v-model="selection"
|
|
|
|
v-model="selection"
|
|
|
|
v-bind="$attrs"
|
|
|
|
v-bind="$attrs"
|
|
|
|
class="c-select"
|
|
|
|
class="c-select"
|
|
|
|
@ -155,9 +388,65 @@ const visibleOptions = computed(() =>
|
|
|
|
</option>
|
|
|
|
</option>
|
|
|
|
<slot />
|
|
|
|
<slot />
|
|
|
|
</select>
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<CButton
|
|
|
|
|
|
|
|
v-if="clearable"
|
|
|
|
|
|
|
|
class="clear"
|
|
|
|
|
|
|
|
icon="×"
|
|
|
|
|
|
|
|
:size="size"
|
|
|
|
|
|
|
|
:disabled="disabled || (modelValue ?? null) === null"
|
|
|
|
|
|
|
|
:aria-controls="controlId"
|
|
|
|
|
|
|
|
aria-label="Clear selection"
|
|
|
|
|
|
|
|
@click="clearSelection"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
|
|
.c-select-control {
|
|
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.c-select,
|
|
|
|
|
|
|
|
.combobox {
|
|
|
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.is-clearable {
|
|
|
|
|
|
|
|
.c-select {
|
|
|
|
|
|
|
|
border-start-end-radius: 0;
|
|
|
|
|
|
|
|
border-end-end-radius: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.clear {
|
|
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
|
|
margin-inline-start: -1px;
|
|
|
|
|
|
|
|
border-start-start-radius: 0;
|
|
|
|
|
|
|
|
border-end-start-radius: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.combobox {
|
|
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&::after {
|
|
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
|
|
inset-inline-end: 9px;
|
|
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
|
|
color: var(--c-muted-text-color, #68717d);
|
|
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
|
|
content: '▼';
|
|
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.c-select {
|
|
|
|
.c-select {
|
|
|
|
box-sizing: border-box;
|
|
|
|
box-sizing: border-box;
|
|
|
|
width: 100%;
|
|
|
|
width: 100%;
|
|
|
|
@ -172,6 +461,10 @@ const visibleOptions = computed(() =>
|
|
|
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
|
|
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
|
|
|
border-radius: var(--c-border-radius, 3px);
|
|
|
|
border-radius: var(--c-border-radius, 3px);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.search {
|
|
|
|
|
|
|
|
cursor: text;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
|
&:hover:not(:disabled) {
|
|
|
|
border-color: var(--c-control-hover-border-color, #929aa6);
|
|
|
|
border-color: var(--c-control-hover-border-color, #929aa6);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -214,4 +507,49 @@ const visibleOptions = computed(() =>
|
|
|
|
font-size: 14px;
|
|
|
|
font-size: 14px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.options {
|
|
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
|
|
z-index: 20;
|
|
|
|
|
|
|
|
inset-inline: 0;
|
|
|
|
|
|
|
|
top: calc(100% + 2px);
|
|
|
|
|
|
|
|
max-height: 210px;
|
|
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
color: var(--c-text-color, #20242a);
|
|
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
|
|
background: var(--c-surface-color, #fff);
|
|
|
|
|
|
|
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
|
|
|
|
|
|
|
border-radius: var(--c-border-radius, 3px);
|
|
|
|
|
|
|
|
box-shadow: 0 2px 6px rgb(0 0 0 / 14%);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.option,
|
|
|
|
|
|
|
|
.empty {
|
|
|
|
|
|
|
|
min-height: 26px;
|
|
|
|
|
|
|
|
padding: 4px 7px;
|
|
|
|
|
|
|
|
line-height: 18px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.option {
|
|
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.is-highlighted {
|
|
|
|
|
|
|
|
background: var(--c-hover-color, #eef1f5);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.is-selected {
|
|
|
|
|
|
|
|
color: var(--c-primary-text-color, #fff);
|
|
|
|
|
|
|
|
background: var(--c-primary-color, #286aa6);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.is-disabled {
|
|
|
|
|
|
|
|
color: var(--c-disabled-text-color, #8a9099);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.empty {
|
|
|
|
|
|
|
|
color: var(--c-muted-text-color, #68717d);
|
|
|
|
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</style>
|
|
|
|
|