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">
|
<script setup lang="ts">
|
||||||
import type { SelectRootEmits, SelectRootProps } from "reka-ui"
|
import { computed, provide, reactive, ref, useId } from 'vue'
|
||||||
import { SelectRoot, useForwardPropsEmits } from "reka-ui"
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { SelectContextKey } from './context'
|
||||||
|
|
||||||
const props = defineProps<SelectRootProps>()
|
const props = defineProps<{
|
||||||
const emits = defineEmits<SelectRootEmits>()
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectRoot
|
<slot />
|
||||||
v-slot="slotProps"
|
|
||||||
data-slot="select"
|
|
||||||
v-bind="forwarded"
|
|
||||||
>
|
|
||||||
<slot v-bind="slotProps" />
|
|
||||||
</SelectRoot>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SelectGroupProps } from "reka-ui"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import type { HTMLAttributes } from "vue"
|
import { cn } from '@/lib/utils'
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { SelectGroup } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes["class"] }>()
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectGroup
|
<div data-slot="select-group" role="group" :class="cn('scroll-my-1 p-1', props.class)">
|
||||||
data-slot="select-group"
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
:class="cn('scroll-my-1 p-1', props.class)"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</SelectGroup>
|
</div>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<SelectItemText
|
<span data-slot="select-item-text"><slot /></span>
|
||||||
data-slot="select-item-text"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</SelectItemText>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SelectLabelProps } from "reka-ui"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import type { HTMLAttributes } from "vue"
|
import { cn } from '@/lib/utils'
|
||||||
import { SelectLabel } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes["class"] }>()
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectLabel
|
<div data-slot="select-label" :class="cn('text-muted-foreground px-1.5 py-1 text-xs', props.class)">
|
||||||
data-slot="select-label"
|
|
||||||
:class="cn('text-muted-foreground px-1.5 py-1 text-xs', props.class)"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</SelectLabel>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SelectSeparatorProps } from "reka-ui"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import type { HTMLAttributes } from "vue"
|
import { cn } from '@/lib/utils'
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { SelectSeparator } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes["class"] }>()
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectSeparator
|
<div data-slot="select-separator" role="separator" :class="cn('bg-border -mx-1 my-1 h-px pointer-events-none', props.class)" />
|
||||||
data-slot="select-separator"
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
:class="cn('bg-border -mx-1 my-1 h-px pointer-events-none', props.class)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SelectValueProps } from "reka-ui"
|
import { inject } from 'vue'
|
||||||
import { SelectValue } from "reka-ui"
|
import { SelectContextKey } from './context'
|
||||||
|
|
||||||
const props = defineProps<SelectValueProps>()
|
const props = defineProps<{ placeholder?: string }>()
|
||||||
|
const context = inject(SelectContextKey)!
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectValue
|
<span
|
||||||
data-slot="select-value"
|
data-slot="select-value"
|
||||||
v-bind="props"
|
:data-placeholder="context.modelValue.value ? undefined : placeholder"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot>{{ context.modelValue.value !== undefined ? context.itemLabels.get(context.modelValue.value) : placeholder }}</slot>
|
||||||
</SelectValue>
|
</span>
|
||||||
</template>
|
</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