-
Notifications
You must be signed in to change notification settings - Fork 872
feat(SelectMenu): handle virtualizer #4572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<script setup lang="ts"> | ||
import type { SelectMenuItem } from '@nuxt/ui' | ||
|
||
const items: SelectMenuItem[][] = [Array(5000).fill(0).map((_, i) => ({ label: `item-${i}`, value: i }))] | ||
</script> | ||
|
||
<template> | ||
<div class="flex flex-col items-center gap-4"> | ||
<div class="flex flex-col gap-4 w-48"> | ||
<USelectMenu | ||
:items="items" | ||
placeholder="Search..." | ||
/> | ||
</div> | ||
</div> | ||
</template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -117,6 +117,17 @@ export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = Array | |
ignoreFilter?: boolean | ||
autofocus?: boolean | ||
autofocusDelay?: number | ||
/** | ||
* When `true` the items in the SelectMenu are virtualized. Keep in mind that this only works with a single group due to a limitation of RekaUI (https://github.com/unovue/reka-ui/issues/1885). | ||
* @defaultValue false | ||
*/ | ||
virtualize?: boolean | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rather have a single |
||
/** | ||
* The height of items to be used by the virtualizer to determine the amount of items to render. | ||
* Keep in mind that virtualization only works when you have no groups due to a limitation of RekaUI (https://github.com/unovue/reka-ui/issues/1885). | ||
* @defaultValue 20 | ||
*/ | ||
virtualItemEstimateSize?: number | ||
class?: any | ||
ui?: SelectMenu['slots'] | ||
} | ||
|
@@ -168,7 +179,7 @@ export interface SelectMenuSlots< | |
|
||
<script setup lang="ts" generic="T extends ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false"> | ||
import { ref, computed, onMounted, toRef, toRaw } from 'vue' | ||
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui' | ||
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, ComboboxVirtualizer, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui' | ||
import { defu } from 'defu' | ||
import { reactivePick, createReusableTemplate } from '@vueuse/core' | ||
import { useAppConfig } from '#imports' | ||
|
@@ -192,7 +203,8 @@ const props = withDefaults(defineProps<SelectMenuProps<T, VK, M>>(), { | |
labelKey: 'label' as never, | ||
resetSearchTermOnBlur: true, | ||
resetSearchTermOnSelect: true, | ||
autofocusDelay: 0 | ||
autofocusDelay: 0, | ||
virtualItemEstimateSize: 20 | ||
}) | ||
const emits = defineEmits<SelectMenuEmits<T, VK, M>>() | ||
const slots = defineSlots<SelectMenuSlots<T, VK, M>>() | ||
|
@@ -447,7 +459,50 @@ defineExpose({ | |
<div role="presentation" :class="ui.viewport({ class: props.ui?.viewport })"> | ||
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" /> | ||
|
||
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })"> | ||
<ComboboxVirtualizer | ||
v-if="virtualize" | ||
v-slot="{ option: item, virtualItem: { index } }" | ||
:options="filteredGroups[0]!.map(item => item as AcceptableValue)" | ||
:estimate-size="virtualItemEstimateSize" | ||
> | ||
<ComboboxItem | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be better to move the whole |
||
:class="ui.item({ class: [props.ui?.item, isSelectItem(item) && item.ui?.item, isSelectItem(item) && item.class] })" | ||
:disabled="isSelectItem(item) && item.disabled" | ||
:value="props.valueKey && isSelectItem(item) ? get(item, props.valueKey as string) : item" | ||
@select="onSelect($event, item)" | ||
> | ||
<slot name="item" :item="(item as NestedItem<T>)" :index="index"> | ||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index"> | ||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon] })" /> | ||
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar] })" /> | ||
<UChip | ||
v-else-if="isSelectItem(item) && item.chip" | ||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])" | ||
inset | ||
standalone | ||
v-bind="item.chip" | ||
:class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip] })" | ||
/> | ||
</slot> | ||
|
||
<span :class="ui.itemLabel({ class: [props.ui?.itemLabel, isSelectItem(item) && item.ui?.itemLabel] })"> | ||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index"> | ||
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }} | ||
</slot> | ||
</span> | ||
|
||
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, isSelectItem(item) && item.ui?.itemTrailing] })"> | ||
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" /> | ||
|
||
<ComboboxItemIndicator as-child> | ||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, isSelectItem(item) && item.ui?.itemTrailingIcon] })" /> | ||
</ComboboxItemIndicator> | ||
</span> | ||
</slot> | ||
</ComboboxItem> | ||
</ComboboxVirtualizer> | ||
|
||
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" v-else :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`"> | ||
<ComboboxLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })"> | ||
{{ get(item, props.labelKey as string) }} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be done inside the
select-menu
page?