FieldAutoCompleteInput.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { useLazyQuery } from '@vue/apollo-composable'
  4. import {
  5. refDebounced,
  6. useDebounceFn,
  7. useElementBounding,
  8. useElementVisibility,
  9. useWindowSize,
  10. watchOnce,
  11. } from '@vueuse/core'
  12. import gql from 'graphql-tag'
  13. import { cloneDeep, escapeRegExp, isEqual, uniqBy } from 'lodash-es'
  14. import { useTemplateRef } from 'vue'
  15. import {
  16. computed,
  17. markRaw,
  18. nextTick,
  19. ref,
  20. toRef,
  21. watch,
  22. type ConcreteComponent,
  23. } from 'vue'
  24. import type { SelectOption } from '#shared/components/CommonSelect/types'
  25. import useValue from '#shared/components/Form/composables/useValue.ts'
  26. import type {
  27. AutoCompleteOption,
  28. AutocompleteSelectValue,
  29. } from '#shared/components/Form/fields/FieldAutocomplete/types.ts'
  30. import type { FormFieldContext } from '#shared/components/Form/types/field.ts'
  31. import useSelectOptions from '#shared/composables/useSelectOptions.ts'
  32. import { useTrapTab } from '#shared/composables/useTrapTab.ts'
  33. import { useFormBlock } from '#shared/form/useFormBlock.ts'
  34. import { i18n } from '#shared/i18n.ts'
  35. import { QueryHandler } from '#shared/server/apollo/handler/index.ts'
  36. import type { ObjectLike } from '#shared/types/utils.ts'
  37. import stopEvent from '#shared/utils/events.ts'
  38. import CommonInputSearch from '#desktop/components/CommonInputSearch/CommonInputSearch.vue'
  39. import CommonSelect from '#desktop/components/CommonSelect/CommonSelect.vue'
  40. import FieldAutoCompleteOptionIcon from './FieldAutoCompleteOptionIcon.vue'
  41. import type {
  42. AutoCompleteProps,
  43. SelectOptionFunction,
  44. ClearFilterInputFunction,
  45. AutoCompleteOptionValueDictionary,
  46. } from './types.ts'
  47. import type { FormKitNode } from '@formkit/core'
  48. import type { NameNode, OperationDefinitionNode, SelectionNode } from 'graphql'
  49. interface Props {
  50. context: FormFieldContext<AutoCompleteProps>
  51. }
  52. const emit = defineEmits<{
  53. 'search-interaction-update': [
  54. filter: string,
  55. optionValues: AutoCompleteOptionValueDictionary,
  56. selectOption: SelectOptionFunction,
  57. clearFilter: ClearFilterInputFunction,
  58. ]
  59. 'keydown-filter-input': [
  60. event: KeyboardEvent,
  61. filter: string,
  62. optionValues: AutoCompleteOptionValueDictionary,
  63. selectOption: SelectOptionFunction,
  64. clearFilter: ClearFilterInputFunction,
  65. ]
  66. 'close-select-dropdown': []
  67. }>()
  68. const props = defineProps<Props>()
  69. const contextReactive = toRef(props, 'context')
  70. const { hasValue, valueContainer, currentValue, isCurrentValue, clearValue } =
  71. useValue<AutocompleteSelectValue>(contextReactive)
  72. // TODO: I think clearValue needs to wrapper for the full clear of the field (to remove some of the remembered stuff).
  73. const localOptions = ref(props.context.options || [])
  74. const {
  75. sortedOptions,
  76. appendedOptions,
  77. optionValueLookup,
  78. getSelectedOption,
  79. getSelectedOptionLabel,
  80. } = useSelectOptions<AutoCompleteOption[]>(localOptions, contextReactive)
  81. watch(
  82. () => props.context.options,
  83. (options) => {
  84. if (!options) return
  85. localOptions.value = options || []
  86. },
  87. )
  88. // Remember current optionValueLookup in node context.
  89. contextReactive.value.optionValueLookup = optionValueLookup
  90. // Initial options prefill for non-multiple fields (multiple fields needs to be handled in
  91. // the form updater or via options prop).
  92. let rememberedInitialOptionFromBuilder: AutoCompleteOption | undefined
  93. const initialOptionBuilderHandler = (rootNode: FormKitNode) => {
  94. if (
  95. hasValue.value &&
  96. props.context.initialOptionBuilder &&
  97. !getSelectedOptionLabel(currentValue.value)
  98. ) {
  99. const initialOption = props.context.initialOptionBuilder(
  100. rootNode?.context?.initialEntityObject as ObjectLike,
  101. currentValue.value,
  102. props.context,
  103. )
  104. if (initialOption) {
  105. localOptions.value.push(initialOption)
  106. if (rememberedInitialOptionFromBuilder) {
  107. const rememberedOptionValue = rememberedInitialOptionFromBuilder.value
  108. localOptions.value = localOptions.value.filter(
  109. (option) => option.value !== rememberedOptionValue,
  110. )
  111. }
  112. rememberedInitialOptionFromBuilder = initialOption
  113. }
  114. }
  115. }
  116. if (!props.context.multiple && props.context.initialOptionBuilder) {
  117. const rootNode = props.context.node.at('$root')
  118. if (rootNode) {
  119. initialOptionBuilderHandler(rootNode)
  120. rootNode?.on('reset', ({ origin }) => {
  121. initialOptionBuilderHandler(origin)
  122. })
  123. }
  124. }
  125. const input = useTemplateRef('input')
  126. const outputElement = useTemplateRef('output')
  127. const filterInput = useTemplateRef('filter-input')
  128. const select = useTemplateRef('select')
  129. const filter = ref('')
  130. const { activateTabTrap, deactivateTabTrap } = useTrapTab(input, true)
  131. const clearFilter = () => {
  132. filter.value = ''
  133. }
  134. const trimmedFilter = computed(() => {
  135. if (!props.context.stripFilter) return filter.value.trim()
  136. return props.context.stripFilter(filter.value.trim())
  137. })
  138. const debouncedFilter = refDebounced(
  139. trimmedFilter,
  140. props.context.debounceInterval ?? 500,
  141. )
  142. const AutocompleteSearchDocument = gql`
  143. ${props.context.gqlQuery}
  144. `
  145. const additionalQueryParams = () => {
  146. if (typeof props.context.additionalQueryParams === 'function') {
  147. return props.context.additionalQueryParams()
  148. }
  149. return props.context.additionalQueryParams || {}
  150. }
  151. const defaultFilter = computed(() => {
  152. if (props.context.alwaysApplyDefaultFilter) return props.context.defaultFilter
  153. if (hasValue.value) return ''
  154. return props.context.defaultFilter
  155. })
  156. const autocompleteQueryHandler = new QueryHandler(
  157. useLazyQuery(
  158. AutocompleteSearchDocument,
  159. () => ({
  160. input: {
  161. query: debouncedFilter.value || defaultFilter.value || '',
  162. limit: props.context.limit,
  163. ...(additionalQueryParams() || {}),
  164. },
  165. }),
  166. () => ({
  167. enabled: !!(debouncedFilter.value || defaultFilter.value),
  168. cachePolicy: 'no-cache', // Do not use cache, because we want always up-to-date results.
  169. }),
  170. ),
  171. )
  172. if (defaultFilter.value) {
  173. autocompleteQueryHandler.load()
  174. } else {
  175. watchOnce(
  176. () => debouncedFilter.value,
  177. (newValue) => {
  178. if (!newValue.length) return
  179. autocompleteQueryHandler.load()
  180. },
  181. )
  182. }
  183. const autocompleteQueryResultKey = (
  184. (AutocompleteSearchDocument.definitions[0] as OperationDefinitionNode)
  185. .selectionSet.selections[0] as SelectionNode & { name: NameNode }
  186. ).name.value
  187. const autocompleteQueryResultOptions = computed<AutoCompleteOption[]>(
  188. (oldValue) => {
  189. const resultOptions =
  190. autocompleteQueryHandler.result().value?.[autocompleteQueryResultKey] ||
  191. []
  192. if (oldValue && isEqual(oldValue, resultOptions)) return oldValue
  193. return resultOptions
  194. },
  195. )
  196. const autocompleteOptions = computed(
  197. () => cloneDeep(autocompleteQueryResultOptions.value) || [],
  198. )
  199. const {
  200. sortedOptions: sortedAutocompleteOptions,
  201. selectOption: selectAutocompleteOption,
  202. getSelectedOption: getSelectedAutocompleteOption,
  203. getSelectedOptionIcon: getSelectedAutocompleteOptionIcon,
  204. optionValueLookup: autocompleteOptionValueLookup,
  205. } = useSelectOptions<AutoCompleteOption[]>(
  206. autocompleteOptions,
  207. toRef(props, 'context'),
  208. )
  209. const preprocessedAutocompleteOptions = computed(() => {
  210. if (!props.context.autocompleteOptionsPreprocessor)
  211. return sortedAutocompleteOptions.value
  212. return props.context.autocompleteOptionsPreprocessor(
  213. sortedAutocompleteOptions.value,
  214. )
  215. })
  216. const selectOption = (option: SelectOption, focus = false) => {
  217. selectAutocompleteOption(option as AutoCompleteOption)
  218. if (!props.context.multiple) {
  219. localOptions.value = [option as AutoCompleteOption]
  220. select.value?.closeDropdown()
  221. return
  222. }
  223. if (!sortedOptions.value.some((elem) => elem.value === option.value)) {
  224. appendedOptions.value.push(option as AutoCompleteOption)
  225. }
  226. appendedOptions.value = appendedOptions.value.filter((elem) =>
  227. isCurrentValue(elem.value),
  228. )
  229. if (!focus) return
  230. filterInput.value?.focus()
  231. }
  232. const isLoading = autocompleteQueryHandler.loading()
  233. const isUserTyping = ref(false)
  234. const selectNewOption = (option: SelectOption, focus = false) => {
  235. if (isCurrentValue(option.value)) return
  236. selectOption(option, focus)
  237. }
  238. const availableOptions = computed<AutoCompleteOption[]>((oldValue) => {
  239. const currentOptions =
  240. filter.value || defaultFilter.value
  241. ? preprocessedAutocompleteOptions.value
  242. : sortedOptions.value
  243. if (oldValue && isEqual(oldValue, currentOptions)) return oldValue
  244. // :TODO check why bug occurs when selecting by keyboard
  245. // Remove duplicates. Sometimes option appears twice in the list.
  246. // ... The problem is that it sometimes it can not be unique related to same id from different objects in one field.
  247. return uniqBy(currentOptions, 'value')
  248. // return currentOptions
  249. })
  250. const emitResultUpdated = () => {
  251. nextTick(() => {
  252. emit(
  253. 'search-interaction-update',
  254. debouncedFilter.value,
  255. { ...autocompleteOptionValueLookup.value, ...optionValueLookup.value },
  256. selectNewOption,
  257. clearFilter,
  258. )
  259. })
  260. }
  261. // Controls state to avoid user showing no results while typing and previous search was no results.
  262. const debouncedSetTypingFalse = useDebounceFn(() => {
  263. isUserTyping.value = false
  264. }, 500)
  265. watch(debouncedFilter, (newValue) => {
  266. if (newValue !== '' || defaultFilter.value) return
  267. emitResultUpdated()
  268. })
  269. watch(isLoading, (newValue, oldValue) => {
  270. // We need not to trigger when query was started.
  271. if (newValue && !oldValue) return
  272. emitResultUpdated()
  273. })
  274. const onKeydownFilterInput = (event: KeyboardEvent) => {
  275. nextTick(() => {
  276. emit(
  277. 'keydown-filter-input',
  278. event,
  279. filter.value,
  280. { ...autocompleteOptionValueLookup.value, ...optionValueLookup.value },
  281. selectNewOption,
  282. clearFilter,
  283. )
  284. })
  285. }
  286. const deaccent = (s: string) =>
  287. s.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
  288. const availableOptionsWithMatches = computed(() => {
  289. // Trim and de-accent search keywords and compile them as a case-insensitive regex.
  290. // Make sure to escape special regex characters!
  291. const filterRegex = new RegExp(
  292. escapeRegExp(deaccent(filter.value.trim())),
  293. 'i',
  294. )
  295. return availableOptions.value.map(
  296. (option) =>
  297. ({
  298. ...option,
  299. // Match options via their de-accented labels.
  300. match: filterRegex.exec(deaccent(option.label || String(option.value))),
  301. }) as AutoCompleteOption,
  302. )
  303. })
  304. const childOptions = ref<AutoCompleteOption[]>([])
  305. const showChildOptions = (option: AutoCompleteOption) => {
  306. if (!option.children) return
  307. childOptions.value = option.children
  308. }
  309. const clearChildOptions = () => {
  310. if (!childOptions.value.length) return
  311. childOptions.value = []
  312. }
  313. const displayOptions = computed(() => {
  314. if (childOptions.value.length) return childOptions.value
  315. return availableOptionsWithMatches.value
  316. })
  317. const suggestedOptionLabel = computed(() => {
  318. if (!filter.value || !availableOptionsWithMatches.value.length)
  319. return undefined
  320. const exactMatches = availableOptionsWithMatches.value.filter(
  321. (option) =>
  322. (option.label || option.value.toString())
  323. .toLowerCase()
  324. .indexOf(filter.value.toLowerCase()) === 0 &&
  325. (option.label || option.value.toString()).length > filter.value.length,
  326. )
  327. if (!exactMatches.length) return undefined
  328. return exactMatches[0].label || exactMatches[0].value.toString()
  329. })
  330. const inputElementBounds = useElementBounding(input)
  331. const isInputVisible = !!VITE_TEST_MODE || useElementVisibility(input)
  332. const windowSize = useWindowSize()
  333. const isBelowHalfScreen = computed(() => {
  334. return inputElementBounds.y.value > windowSize.height.value / 2
  335. })
  336. const onCloseDropdown = () => {
  337. clearChildOptions()
  338. clearFilter()
  339. deactivateTabTrap()
  340. emit('close-select-dropdown')
  341. }
  342. const foldDropdown = (event?: MouseEvent) => {
  343. if ((event?.target as HTMLElement)?.tagName !== 'INPUT' && select.value) {
  344. select.value.closeDropdown()
  345. return onCloseDropdown()
  346. }
  347. }
  348. const openSelectDropdown = () => {
  349. if (props.context.disabled) return
  350. select.value?.openDropdown(inputElementBounds, windowSize.height)
  351. requestAnimationFrame(() => {
  352. activateTabTrap()
  353. if (props.context.noFiltering) outputElement.value?.focus()
  354. else filterInput.value?.focus()
  355. })
  356. }
  357. defineExpose({ openSelectDropdown })
  358. const openOrMoveFocusToDropdown = (lastOption = false) => {
  359. if (!select.value?.isOpen) {
  360. return openSelectDropdown()
  361. }
  362. deactivateTabTrap()
  363. nextTick(() => {
  364. requestAnimationFrame(() => {
  365. select.value?.moveFocusToDropdown(lastOption)
  366. })
  367. })
  368. }
  369. const onFocusFilterInput = () => {
  370. filterInput.value?.focus()
  371. }
  372. const handleToggleDropdown = (event: MouseEvent) => {
  373. if (select.value?.isOpen) return foldDropdown(event)
  374. openSelectDropdown()
  375. }
  376. const OptionIconComponent =
  377. props.context.optionIconComponent ??
  378. (FieldAutoCompleteOptionIcon as ConcreteComponent)
  379. const handleCloseDropdown = (
  380. event: KeyboardEvent,
  381. expanded: boolean,
  382. closeDropdown: () => void,
  383. ) => {
  384. if (expanded) {
  385. stopEvent(event)
  386. closeDropdown()
  387. }
  388. }
  389. watch(filter, (newValue, oldValue) => {
  390. if (newValue !== oldValue) {
  391. isUserTyping.value = true
  392. if (newValue === '') {
  393. // Instant update if filter is empty.
  394. isUserTyping.value = false
  395. }
  396. debouncedSetTypingFalse()
  397. }
  398. })
  399. useFormBlock(
  400. contextReactive,
  401. useDebounceFn((event) => {
  402. if (select.value?.isOpen) foldDropdown(event)
  403. openSelectDropdown()
  404. }, 500),
  405. )
  406. </script>
  407. <template>
  408. <div
  409. ref="input"
  410. class="flex h-auto min-h-10 hover:outline hover:outline-1 hover:outline-offset-1 hover:outline-blue-600 has-[output:focus,input:focus]:outline has-[output:focus,input:focus]:outline-1 has-[output:focus,input:focus]:outline-offset-1 has-[output:focus,input:focus]:outline-blue-800 dark:hover:outline-blue-900 dark:has-[output:focus,input:focus]:outline-blue-800"
  411. :class="[
  412. context.classes.input,
  413. {
  414. 'rounded-lg': !select?.isOpen,
  415. 'rounded-t-lg': select?.isOpen && !isBelowHalfScreen,
  416. 'rounded-b-lg': select?.isOpen && isBelowHalfScreen,
  417. 'bg-blue-200 dark:bg-gray-700': !context.alternativeBackground,
  418. 'bg-neutral-50 dark:bg-gray-500': context.alternativeBackground,
  419. },
  420. ]"
  421. data-test-id="field-autocomplete"
  422. >
  423. <CommonSelect
  424. ref="select"
  425. #default="{ state: expanded, close: closeDropdown }"
  426. :model-value="currentValue"
  427. :options="displayOptions"
  428. :multiple="context.multiple"
  429. :owner="context.id"
  430. :filter="filter"
  431. :option-icon-component="markRaw(OptionIconComponent)"
  432. :empty-initial-label-text="contextReactive.emptyInitialLabelText"
  433. :actions="context.actions"
  434. :is-child-page="childOptions.length > 0"
  435. no-options-label-translation
  436. :is-loading="isLoading || isUserTyping"
  437. :is-target-visible="isInputVisible"
  438. no-close
  439. passive
  440. initially-empty
  441. @select="selectOption"
  442. @push="showChildOptions"
  443. @pop="clearChildOptions"
  444. @close="onCloseDropdown"
  445. @focus-filter-input="onFocusFilterInput"
  446. >
  447. <output
  448. :id="context.id"
  449. ref="output"
  450. role="combobox"
  451. aria-controls="common-select"
  452. aria-owns="common-select"
  453. aria-haspopup="menu"
  454. :aria-expanded="expanded"
  455. :name="context.node.name"
  456. class="formkit-disabled:pointer-events-none flex grow items-center gap-2.5 px-2.5 py-2 text-black focus:outline-none dark:text-white"
  457. :aria-labelledby="`label-${context.id}`"
  458. :aria-disabled="context.disabled"
  459. :aria-describedby="context.describedBy"
  460. aria-autocomplete="none"
  461. :data-multiple="context.multiple"
  462. tabindex="0"
  463. v-bind="context.attrs"
  464. @keydown.escape="handleCloseDropdown($event, expanded, closeDropdown)"
  465. @keypress.enter.prevent="openSelectDropdown()"
  466. @keydown.down.prevent="openOrMoveFocusToDropdown()"
  467. @keydown.up.prevent="openOrMoveFocusToDropdown(true)"
  468. @keypress.space.prevent="openSelectDropdown()"
  469. @blur="context.handlers.blur"
  470. @click.stop="handleToggleDropdown"
  471. >
  472. <div
  473. v-if="hasValue && context.multiple"
  474. class="flex flex-wrap gap-1.5"
  475. role="list"
  476. >
  477. <div
  478. v-for="selectedValue in valueContainer"
  479. :key="selectedValue.toString()"
  480. class="flex items-center gap-1.5"
  481. role="listitem"
  482. >
  483. <div
  484. class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs text-black dark:text-white"
  485. :class="{
  486. 'bg-white dark:bg-gray-200': !context.alternativeBackground,
  487. 'bg-neutral-100 dark:bg-gray-200':
  488. context.alternativeBackground,
  489. }"
  490. >
  491. <CommonIcon
  492. v-if="getSelectedAutocompleteOptionIcon(selectedValue)"
  493. :name="getSelectedAutocompleteOptionIcon(selectedValue)"
  494. class="shrink-0 fill-gray-100 dark:fill-neutral-400"
  495. size="xs"
  496. decorative
  497. />
  498. <span
  499. v-tooltip="
  500. getSelectedOptionLabel(selectedValue) ||
  501. i18n.t('%s (unknown)', selectedValue.toString())
  502. "
  503. class="line-clamp-3 whitespace-pre-wrap break-words"
  504. >
  505. {{
  506. getSelectedOptionLabel(selectedValue) ||
  507. i18n.t('%s (unknown)', selectedValue.toString())
  508. }}
  509. </span>
  510. <CommonIcon
  511. :aria-label="i18n.t('Unselect Option')"
  512. class="shrink-0 fill-stone-200 hover:fill-black focus:outline-none focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:fill-neutral-500 dark:hover:fill-white"
  513. name="x-lg"
  514. size="xs"
  515. role="button"
  516. tabindex="0"
  517. @click.stop="
  518. selectOption(
  519. getSelectedAutocompleteOption(selectedValue) ||
  520. getSelectedOption(selectedValue),
  521. )
  522. "
  523. @keypress.enter.prevent.stop="
  524. selectOption(
  525. getSelectedAutocompleteOption(selectedValue) ||
  526. getSelectedOption(selectedValue),
  527. )
  528. "
  529. @keypress.space.prevent.stop="
  530. selectOption(
  531. getSelectedAutocompleteOption(selectedValue) ||
  532. getSelectedOption(selectedValue),
  533. )
  534. "
  535. />
  536. </div>
  537. </div>
  538. </div>
  539. <CommonInputSearch
  540. v-if="expanded || !hasValue"
  541. ref="filter-input"
  542. v-model="filter"
  543. :class="{ 'pointer-events-none': !expanded }"
  544. :tabindex="!expanded ? '-1' : undefined"
  545. :suggestion="suggestedOptionLabel"
  546. :alternative-background="context.alternativeBackground"
  547. :placeholder="context.filterInputPlaceholder"
  548. @keypress.space.stop
  549. @keydown="onKeydownFilterInput"
  550. />
  551. <div
  552. v-if="!expanded"
  553. class="flex grow flex-wrap gap-1"
  554. :class="{ grow: hasValue && !context.multiple }"
  555. role="list"
  556. >
  557. <div
  558. v-if="hasValue && !context.multiple"
  559. class="flex items-center gap-1.5 text-sm"
  560. role="listitem"
  561. >
  562. <CommonIcon
  563. v-if="getSelectedAutocompleteOptionIcon(currentValue)"
  564. :name="getSelectedAutocompleteOptionIcon(currentValue)"
  565. class="shrink-0 fill-gray-100 dark:fill-neutral-400"
  566. size="tiny"
  567. decorative
  568. />
  569. <span
  570. v-tooltip="
  571. getSelectedOptionLabel(currentValue) ||
  572. i18n.t('%s (unknown)', currentValue.toString())
  573. "
  574. class="line-clamp-3 whitespace-pre-wrap break-words"
  575. >
  576. {{
  577. getSelectedOptionLabel(currentValue) ||
  578. i18n.t('%s (unknown)', currentValue.toString())
  579. }}
  580. </span>
  581. </div>
  582. </div>
  583. <CommonIcon
  584. v-if="context.clearable && hasValue && !context.disabled"
  585. :aria-label="i18n.t('Clear Selection')"
  586. class="shrink-0 fill-stone-200 hover:fill-black focus:outline-none focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:fill-neutral-500 dark:hover:fill-white"
  587. name="x-lg"
  588. size="xs"
  589. role="button"
  590. tabindex="0"
  591. @click.stop="clearValue()"
  592. @keypress.enter.prevent.stop="clearValue()"
  593. @keypress.space.prevent.stop="clearValue()"
  594. />
  595. <CommonIcon
  596. class="shrink-0 fill-stone-200 dark:fill-neutral-500"
  597. name="chevron-down"
  598. size="xs"
  599. decorative
  600. />
  601. </output>
  602. </CommonSelect>
  603. </div>
  604. </template>