Skip to content

Commit

Permalink
fix(Combobox): initial search not working and virtualizer
Browse files Browse the repository at this point in the history
  • Loading branch information
zernonia committed Sep 24, 2024
1 parent d94969e commit ca90450
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 54 deletions.
12 changes: 10 additions & 2 deletions packages/core/src/Combobox/ComboboxInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ComboboxInputProps extends ListboxFilterProps {

<script setup lang="ts">
import { injectComboboxRootContext } from './ComboboxRoot.vue'
import { injectListboxRootContext } from '@/Listbox/ListboxRoot.vue'
import { ListboxFilter } from '@/Listbox'
const props = withDefaults(defineProps<ComboboxInputProps>(), {
Expand All @@ -21,6 +22,7 @@ const props = withDefaults(defineProps<ComboboxInputProps>(), {
const emits = defineEmits<ComboboxInputEmits>()
const rootContext = injectComboboxRootContext()
const listboxContext = injectListboxRootContext()
const { primitiveElement, currentElement } = usePrimitiveElement()
const modelValue = useVModel(props, 'modelValue', emits, {
Expand All @@ -37,12 +39,18 @@ function handleKeyDown(ev: KeyboardEvent) {
rootContext.onOpenChange(true)
}
function handleInput(event: Event) {
function handleInput(event: InputEvent) {
const target = event.target as HTMLInputElement
if (!rootContext.open.value) {
rootContext.onOpenChange(true)
nextTick(() => {
if (target.value) {
rootContext.filterState.search = target.value
listboxContext.highlightFirstItem(event)
}
})
}
else {
const target = event.target as HTMLInputElement
rootContext.filterState.search = target.value
}
}
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/Combobox/ComboboxItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ if (!props.value) {
)
}
const isRender = computed(() => rootContext.ignoreFilter.value ? true : !rootContext.filterState.search ? true : rootContext.filterState.filtered.items.get(id)! > 0)
const isRender = computed(() => {
if (rootContext.isVirtual.value || rootContext.ignoreFilter.value || !rootContext.filterState.search)
return true
else
return rootContext.filterState.filtered.items.get(id)! > 0
})
onMounted(() => {
// textValue to perform filter
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/Combobox/ComboboxViewport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { PrimitiveProps } from '@/Primitive'
import { useForwardExpose } from '@/shared'
import { useNonce } from '@/shared/useNonce'
import { toRefs } from 'vue'
import { injectComboboxRootContext } from './ComboboxRoot.vue'
export interface ComboboxViewportProps extends PrimitiveProps {
/**
Expand All @@ -20,6 +21,8 @@ const { forwardRef } = useForwardExpose()
const { nonce: propNonce } = toRefs(props)
const nonce = useNonce(propNonce)
const rootContext = injectComboboxRootContext()
</script>

<template>
Expand All @@ -33,7 +36,7 @@ const nonce = useNonce(propNonce)
// `selectedItem.offsetTop` in calculations, the offset is relative to the viewport
// (independent of the scrollUpButton).
position: 'relative',
flex: 1,
flex: rootContext.isVirtual.value ? undefined : 1,
overflow: 'auto',
}"
>
Expand Down
11 changes: 4 additions & 7 deletions packages/core/src/Combobox/ComboboxVirtualizer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ export interface ComboboxVirtualizerProps<T extends AcceptableValue = Acceptable
</script>

<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue">
import type { ListboxVirtualizerProps } from '@/Listbox'
import { ListboxVirtualizer } from '@/Listbox'
import ListboxVirtualizer, { type ListboxVirtualizerProps, type ListboxVirtualizerSlots } from '@/Listbox/ListboxVirtualizer.vue'
import type { AcceptableValue } from '@/shared/types'
import { injectComboboxRootContext } from './ComboboxRoot.vue'
const props = defineProps<ComboboxVirtualizerProps<T>>()
defineSlots<{
default: (props: {
option: T
}) => any
default: (props: ListboxVirtualizerSlots<T>) => any
}>()
const rootContext = injectComboboxRootContext()
Expand All @@ -23,9 +20,9 @@ rootContext.isVirtual.value = true

<template>
<ListboxVirtualizer
v-slot="{ option }"
v-slot="slotProps"
v-bind="props"
>
<slot :option="option" />
<slot v-bind="slotProps" />
</ListboxVirtualizer>
</template>
48 changes: 25 additions & 23 deletions packages/core/src/Combobox/story/ComboboxVirtual.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import { computed, ref } from 'vue'
import { ComboboxAnchor, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxItemIndicator, ComboboxRoot, ComboboxTrigger, ComboboxViewport, ComboboxVirtualizer } from '..'
import { Icon } from '@iconify/vue'
import { countryList } from '@/shared/constant'
import { useFilter } from '@/shared'
const { startsWith } = useFilter({ sensitivity: 'base' })
const filterText = ref('')
const filteredOptions = computed(() => {
const options = countryList.map(a => ({ label: a, value: a }))
return filterText.value ? options.filter(item => item.label.toLowerCase().includes(filterText.value.toLowerCase())) : options
return filterText.value ? options.filter(opt => startsWith(opt.label, filterText.value)) : options
})
</script>

<template>
<Story
title="Combobox/Virtual"
:layout="{ type: 'single', iframe: 'false' }"
:layout="{ type: 'single', iframe: false }"
>
<Variant
title="default"
Expand All @@ -38,29 +41,28 @@ const filteredOptions = computed(() => {
/>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent class="absolute mt-2 p-[5px] max-h-[500px] w-[200px] bg-white rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade">
<ComboboxViewport>
<div>
<ComboboxVirtualizer
v-slot="{ option }"
:options="filteredOptions"
:estimate-size="25"
<ComboboxContent class="absolute mt-2 p-[5px] w-[200px] bg-white rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade">
<ComboboxViewport class="max-h-80 overflow-y-auto">
<ComboboxVirtualizer
v-slot="{ option }"
:options="filteredOptions"
:estimate-size="25"
:text-content="(opt) => opt.label"
>
<ComboboxItem
class="text-[13px] w-full leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-grass9 data-[highlighted]:text-grass1"
:value="option"
>
<ComboboxItem
class="text-[13px] w-full leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-grass9 data-[highlighted]:text-grass1"
:value="option"
<ComboboxItemIndicator
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
>
<ComboboxItemIndicator
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
>
<Icon icon="radix-icons:check" />
</ComboboxItemIndicator>
<span>
{{ option.label }}
</span>
</ComboboxItem>
</ComboboxVirtualizer>
</div>
<Icon icon="radix-icons:check" />
</ComboboxItemIndicator>
<span class="truncate">
{{ option.label }}
</span>
</ComboboxItem>
</ComboboxVirtualizer>
</ComboboxViewport>
</ComboboxContent>
</ComboboxRoot>
Expand Down
14 changes: 8 additions & 6 deletions packages/core/src/Listbox/ListboxRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,12 @@ function highlightItem(value: T) {
}
function onKeydownEnter(event: KeyboardEvent) {
if (highlightedElement.value)
if (highlightedElement.value) {
event.preventDefault()
event.stopPropagation()
highlightedElement.value.click()
}
}
function onKeydownTypeAhead(event: KeyboardEvent) {
Expand Down Expand Up @@ -293,14 +297,12 @@ function handleMultipleReplace(event: KeyboardEvent, targetEl: HTMLElement) {
}
async function highlightSelected(event?: Event) {
await nextTick()
if (isVirtual.value) {
nextTick(() => {
// Trigger on nextTick for Virtualizer to be mounted
virtualFocusHook.trigger(event)
})
// Trigger on nextTick for Virtualizer to be mounted
virtualFocusHook.trigger(event)
}
else {
await nextTick()
const collection = getCollectionItem()
const item = collection.find(i => i.dataset.state === 'checked')
if (item)
Expand Down
37 changes: 25 additions & 12 deletions packages/core/src/Listbox/ListboxVirtualizer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ export interface ListboxVirtualizerProps<T extends AcceptableValue = AcceptableV
/** text content for each item to achieve type-ahead feature */
textContent?: (option: T) => string
}
export interface ListboxVirtualizerSlots<T> {
option: T
virtualizer: Virtualizer<Element | Window, Element>
virtualItem: VirtualItem<Element>
}
</script>

<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue">
import { type VirtualItem, type Virtualizer, useVirtualizer } from '@tanstack/vue-virtual'
import { type Ref, cloneVNode, computed, useSlots } from 'vue'
import { Fragment, type Ref, type VNode, cloneVNode, computed, useSlots } from 'vue'
import { injectListboxRootContext } from './ListboxRoot.vue'
import { compare, queryCheckedElement } from './utils'
import { MAP_KEY_TO_FOCUS_INTENT } from '@/RovingFocus/utils'
Expand All @@ -25,11 +32,7 @@ import type { AcceptableValue } from '@/shared/types'
const props = defineProps<ListboxVirtualizerProps<T>>()
defineSlots<{
default: (props: {
option: T
virtualizer: Virtualizer<Element | Window, Element>
virtualItem: VirtualItem<Element>
}) => any
default: (props: ListboxVirtualizerSlots<T>) => any
}>()
const slots = useSlots()
Expand Down Expand Up @@ -69,13 +72,19 @@ const virtualizer = useVirtualizer(
)
const virtualizedItems = computed(() => virtualizer.value.getVirtualItems().map((item) => {
const defaultNode = slots.default!({
option: props.options[item.index],
virtualizer: virtualizer.value,
virtualItem: item,
})[0]
const targetNode = defaultNode.type === Fragment && Array.isArray(defaultNode.children)
? defaultNode.children[0] as VNode
: defaultNode
return {
item,
is: cloneVNode(slots.default!({
option: props.options[item.index],
virtualizer: virtualizer.value,
virtualItem: item,
})![0], {
is: cloneVNode(targetNode, {
'key': `${item.key}`,
'data-index': item.index,
'aria-setsize': props.options.length,
Expand Down Expand Up @@ -111,6 +120,9 @@ rootContext.virtualFocusHook.on((event) => {
}
})
}
else {
rootContext.highlightFirstItem(event as InputEvent)
}
})
rootContext.virtualHighlightHook.on((value) => {
Expand Down Expand Up @@ -196,7 +208,8 @@ rootContext.virtualKeydownHook.on((event) => {
requestAnimationFrame(() => {
const items = getItems()
const item = intent === 'first' ? items[0] : items[items.length - 1]
rootContext.changeHighlight(item.ref)
if (item)
rootContext.changeHighlight(item.ref)
})
}
else if (!intent && !isMetaKey) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/Listbox/story/ListboxVirtual.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const filteredOptions = computed(() => {
:layout="{ type: 'grid', iframe: false, width: '50%' }"
>
<Variant title="Basic">
<ListboxRoot class="w-48 h-72 overflow-auto p-1 rounded-lg border bg-white text-green9 mx-auto">
<ListboxContent>
<ListboxRoot class="w-48 p-1 rounded-lg border bg-white text-green9 mx-auto">
<ListboxContent class="h-72 overflow-auto">
<ListboxVirtualizer
v-slot="{ option }"
:options="filteredOptions"
Expand Down

0 comments on commit ca90450

Please sign in to comment.