refactor: rewrite the Select root family without reka-ui
Root/Value/Label/Group/Separator/ItemText now share a small provide/inject context. SelectValue's label lookup is backed by an itemLabels map that SelectItem populates on mount (next commit) -- matching the existing behavior where the trigger's displayed label only resolves once the dropdown has opened at least once.
This commit is contained in:
parent
e7d96cc3ba
commit
f69a3b0c65
7 changed files with 76 additions and 69 deletions
|
|
@ -1,19 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from "reka-ui"
|
||||
import { SelectRoot, useForwardPropsEmits } from "reka-ui"
|
||||
import { computed, provide, reactive, ref, useId } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { SelectContextKey } from './context'
|
||||
|
||||
const props = defineProps<SelectRootProps>()
|
||||
const emits = defineEmits<SelectRootEmits>()
|
||||
const props = defineProps<{
|
||||
modelValue?: string
|
||||
defaultValue?: string
|
||||
disabled?: boolean
|
||||
open?: boolean
|
||||
defaultOpen?: boolean
|
||||
}>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'update:open': [value: boolean]
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emit, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue,
|
||||
})
|
||||
const open = useVModel(props, 'open', emit, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultOpen ?? false,
|
||||
})
|
||||
|
||||
const disabled = computed(() => props.disabled ?? false)
|
||||
const triggerRef = ref<HTMLElement | null>(null)
|
||||
const itemLabels = reactive(new Map<string, string>())
|
||||
|
||||
provide(SelectContextKey, {
|
||||
open,
|
||||
modelValue,
|
||||
disabled,
|
||||
triggerRef,
|
||||
contentId: `select-content-${useId()}`,
|
||||
itemLabels,
|
||||
registerLabel: (value: string, label: string) => { itemLabels.set(value, label) },
|
||||
unregisterLabel: (value: string) => { itemLabels.delete(value) },
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="select"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</SelectRoot>
|
||||
<slot />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { SelectGroupProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { SelectGroup } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectGroup
|
||||
data-slot="select-group"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('scroll-my-1 p-1', props.class)"
|
||||
>
|
||||
<div data-slot="select-group" role="group" :class="cn('scroll-my-1 p-1', props.class)">
|
||||
<slot />
|
||||
</SelectGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
<script setup lang="ts">
|
||||
import type { SelectItemTextProps } from "reka-ui"
|
||||
import { SelectItemText } from "reka-ui"
|
||||
|
||||
const props = defineProps<SelectItemTextProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItemText
|
||||
data-slot="select-item-text"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
<span data-slot="select-item-text"><slot /></span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { SelectLabelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { SelectLabel } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes["class"] }>()
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectLabel
|
||||
data-slot="select-label"
|
||||
:class="cn('text-muted-foreground px-1.5 py-1 text-xs', props.class)"
|
||||
>
|
||||
<div data-slot="select-label" :class="cn('text-muted-foreground px-1.5 py-1 text-xs', props.class)">
|
||||
<slot />
|
||||
</SelectLabel>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import type { SelectSeparatorProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { SelectSeparator } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectSeparator
|
||||
data-slot="select-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 my-1 h-px pointer-events-none', props.class)"
|
||||
/>
|
||||
<div data-slot="select-separator" role="separator" :class="cn('bg-border -mx-1 my-1 h-px pointer-events-none', props.class)" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import type { SelectValueProps } from "reka-ui"
|
||||
import { SelectValue } from "reka-ui"
|
||||
import { inject } from 'vue'
|
||||
import { SelectContextKey } from './context'
|
||||
|
||||
const props = defineProps<SelectValueProps>()
|
||||
const props = defineProps<{ placeholder?: string }>()
|
||||
const context = inject(SelectContextKey)!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectValue
|
||||
<span
|
||||
data-slot="select-value"
|
||||
v-bind="props"
|
||||
:data-placeholder="context.modelValue.value ? undefined : placeholder"
|
||||
>
|
||||
<slot />
|
||||
</SelectValue>
|
||||
<slot>{{ context.modelValue.value !== undefined ? context.itemLabels.get(context.modelValue.value) : placeholder }}</slot>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
|||
14
packages/library/src/components/ui/select/context.ts
Normal file
14
packages/library/src/components/ui/select/context.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
export interface SelectContext {
|
||||
open: Ref<boolean>
|
||||
modelValue: Ref<string | undefined>
|
||||
disabled: Ref<boolean>
|
||||
triggerRef: Ref<HTMLElement | null>
|
||||
contentId: string
|
||||
itemLabels: Map<string, string>
|
||||
registerLabel: (value: string, label: string) => void
|
||||
unregisterLabel: (value: string) => void
|
||||
}
|
||||
|
||||
export const SelectContextKey: InjectionKey<SelectContext> = Symbol('SelectContext')
|
||||
Loading…
Reference in a new issue