fix(SelectMenu): wrap content with FocusScope

Resolves #2657
This commit is contained in:
Benjamin Canac
2025-02-11 15:14:06 +01:00
parent 80e6c1d264
commit e7e75858d7
4 changed files with 2400 additions and 2229 deletions

View File

@@ -134,7 +134,7 @@ extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3']
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false"> <script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
import { computed, toRef, toRaw } from 'vue' import { computed, toRef, toRaw } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits, useFilter } from 'reka-ui' import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui'
import { defu } from 'defu' import { defu } from 'defu'
import { reactivePick, createReusableTemplate } from '@vueuse/core' import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
@@ -337,68 +337,70 @@ function onUpdateOpen(value: boolean) {
<ComboboxPortal :disabled="!portal"> <ComboboxPortal :disabled="!portal">
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps"> <ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
<ComboboxInput v-if="!!searchInput" v-model="searchTerm" :display-value="() => searchTerm" as-child> <FocusScope trapped :class="ui.focusScope({ class: props.ui?.focusScope })">
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" /> <ComboboxInput v-if="!!searchInput" v-model="searchTerm" :display-value="() => searchTerm" as-child>
</ComboboxInput> <UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
</ComboboxInput>
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })"> <ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm"> <slot name="empty" :search-term="searchTerm">
{{ searchTerm ? t('selectMenu.noMatch', { searchTerm }) : t('selectMenu.noData') }} {{ searchTerm ? t('selectMenu.noMatch', { searchTerm }) : t('selectMenu.noData') }}
</slot> </slot>
</ComboboxEmpty> </ComboboxEmpty>
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })"> <ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" /> <ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" />
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })"> <ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`"> <template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })"> <ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
{{ get(item, props.labelKey as string) }} {{ get(item, props.labelKey as string) }}
</ComboboxLabel> </ComboboxLabel>
<ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" /> <ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
<ComboboxItem <ComboboxItem
v-else v-else
:class="ui.item({ class: props.ui?.item })" :class="ui.item({ class: props.ui?.item })"
:disabled="item.disabled" :disabled="item.disabled"
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item" :value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
@select="item.onSelect" @select="item.onSelect"
> >
<slot name="item" :item="(item as T)" :index="index"> <slot name="item" :item="(item as T)" :index="index">
<slot name="item-leading" :item="(item as T)" :index="index"> <slot name="item-leading" :item="(item as T)" :index="index">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" /> <UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" /> <UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
<UChip <UChip
v-else-if="item.chip" v-else-if="item.chip"
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])" :size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
inset inset
standalone standalone
v-bind="item.chip" v-bind="item.chip"
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip })" :class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip })"
/> />
</slot>
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<slot name="item-label" :item="(item as T)" :index="index">
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
</slot> </slot>
</span>
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })"> <span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<slot name="item-trailing" :item="(item as T)" :index="index" /> <slot name="item-label" :item="(item as T)" :index="index">
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
</slot>
</span>
<ComboboxItemIndicator as-child> <span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" /> <slot name="item-trailing" :item="(item as T)" :index="index" />
</ComboboxItemIndicator>
</span>
</slot>
</ComboboxItem>
</template>
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'bottom'" /> <ComboboxItemIndicator as-child>
</ComboboxViewport> <UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
</ComboboxItemIndicator>
</span>
</slot>
</ComboboxItem>
</template>
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'bottom'" />
</ComboboxViewport>
</FocusScope>
<ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" /> <ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
</ComboboxContent> </ComboboxContent>

View File

@@ -5,7 +5,8 @@ import select from './select'
export default (options: Required<ModuleOptions>) => { export default (options: Required<ModuleOptions>) => {
return defu({ return defu({
slots: { slots: {
input: 'border-b border-(--ui-border)' input: 'border-b border-(--ui-border)',
focusScope: 'flex flex-col min-h-0'
} }
}, select(options)) }, select(options))
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff