mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-12 10:12:17 +01:00
Add virtualizer to ProjectDropdown, ClientDropdown and Reporting
Comboboxes; Remove redundant focus loop on Project/ClientDropdown
This commit is contained in:
@@ -9,10 +9,16 @@ import {
|
||||
ComboboxItem,
|
||||
ComboboxRoot,
|
||||
ComboboxViewport,
|
||||
} from 'radix-vue';
|
||||
ComboboxVirtualizer,
|
||||
} from 'reka-ui';
|
||||
|
||||
const NONE_ID = 'none';
|
||||
|
||||
// height of one row (px-2 py-1.5 text-sm → 12px padding + 20px line box).
|
||||
// Rows are uniform single-line, so a fixed size is exact enough for the virtualizer and avoids
|
||||
// any per-row DOM measurement.
|
||||
const ROW_HEIGHT = 32;
|
||||
|
||||
const model = defineModel<string[]>({
|
||||
default: [],
|
||||
});
|
||||
@@ -56,6 +62,23 @@ const showNoItem = computed(() => {
|
||||
return props.noItemLabel.toLowerCase().includes(search);
|
||||
});
|
||||
|
||||
// A single flat list for the virtualizer. The optional "no item" entry is folded in as the
|
||||
// first row so the whole list (including it) is virtualized through one ComboboxVirtualizer.
|
||||
type Row = { kind: 'none' } | { kind: 'item'; item: T };
|
||||
|
||||
const rows = computed<Row[]>(() => {
|
||||
const itemRows = filteredItems.value.map((item): Row => ({ kind: 'item', item }));
|
||||
return showNoItem.value ? [{ kind: 'none' }, ...itemRows] : itemRows;
|
||||
});
|
||||
|
||||
function keyForRow(row: Row): string {
|
||||
return row.kind === 'none' ? NONE_ID : props.getKeyFromItem(row.item);
|
||||
}
|
||||
|
||||
function nameForRow(row: Row): string {
|
||||
return row.kind === 'none' ? (props.noItemLabel ?? '') : props.getNameForItem(row.item);
|
||||
}
|
||||
|
||||
function toggleItem(id: string) {
|
||||
if (model.value.includes(id)) {
|
||||
model.value = model.value.filter((itemId) => itemId !== id);
|
||||
@@ -74,46 +97,35 @@ const emit = defineEmits(['update:modelValue', 'changed', 'submit']);
|
||||
<slot name="trigger"></slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<ComboboxRoot
|
||||
v-model:search-term="searchValue"
|
||||
v-model:open="open"
|
||||
class="p-2"
|
||||
:filter-function="(val: string[]) => val">
|
||||
<ComboboxRoot v-model:open="open" class="p-2" :ignore-filter="true">
|
||||
<ComboboxAnchor>
|
||||
<ComboboxInput
|
||||
v-model="searchValue"
|
||||
class="w-full h-8 rounded-md border border-input-border bg-input-background px-3 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none"
|
||||
:placeholder="searchPlaceholder" />
|
||||
</ComboboxAnchor>
|
||||
<ComboboxContent
|
||||
:dismiss-able="false"
|
||||
position="inline"
|
||||
class="mt-2 min-w-60 max-w-80 max-h-60 overflow-y-auto">
|
||||
<ComboboxViewport>
|
||||
<ComboboxItem
|
||||
v-if="showNoItem"
|
||||
:value="NONE_ID"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary data-[highlighted]:bg-card-background-active cursor-default"
|
||||
@select.prevent="toggleItem(NONE_ID)">
|
||||
<Checkbox
|
||||
:checked="model.includes(NONE_ID)"
|
||||
aria-hidden="true"
|
||||
:tabindex="-1"
|
||||
class="pointer-events-none" />
|
||||
<span class="truncate">{{ noItemLabel }}</span>
|
||||
</ComboboxItem>
|
||||
<ComboboxItem
|
||||
v-for="item in filteredItems"
|
||||
:key="getKeyFromItem(item)"
|
||||
:value="getKeyFromItem(item)"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary data-[highlighted]:bg-card-background-active cursor-default"
|
||||
@select.prevent="toggleItem(getKeyFromItem(item))">
|
||||
<Checkbox
|
||||
:checked="model.includes(getKeyFromItem(item))"
|
||||
aria-hidden="true"
|
||||
:tabindex="-1"
|
||||
class="pointer-events-none" />
|
||||
<span class="truncate">{{ getNameForItem(item) }}</span>
|
||||
</ComboboxItem>
|
||||
class="mt-2 min-w-60 max-w-80">
|
||||
<ComboboxViewport class="max-h-60 overflow-y-auto">
|
||||
<ComboboxVirtualizer
|
||||
v-slot="{ option }"
|
||||
:options="rows"
|
||||
:estimate-size="ROW_HEIGHT"
|
||||
:text-content="nameForRow">
|
||||
<ComboboxItem
|
||||
:value="keyForRow(option)"
|
||||
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary data-[highlighted]:bg-card-background-active cursor-default"
|
||||
@select.prevent="toggleItem(keyForRow(option))">
|
||||
<Checkbox
|
||||
:checked="model.includes(keyForRow(option))"
|
||||
aria-hidden="true"
|
||||
:tabindex="-1"
|
||||
class="pointer-events-none" />
|
||||
<span class="truncate">{{ nameForRow(option) }}</span>
|
||||
</ComboboxItem>
|
||||
</ComboboxVirtualizer>
|
||||
</ComboboxViewport>
|
||||
</ComboboxContent>
|
||||
</ComboboxRoot>
|
||||
|
||||
Reference in New Issue
Block a user