refactor: rewrite SelectTrigger/SelectItem without reka-ui
role="combobox" trigger and role="option" items driven by the shared Select context. SelectContent (next commit) is needed before the Select family works end-to-end again.
This commit is contained in:
parent
f69a3b0c65
commit
1cb1486fbd
2 changed files with 76 additions and 39 deletions
|
|
@ -1,45 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import { CheckIcon } from '@lucide/vue';
|
||||
import { CheckIcon } from '@lucide/vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { computed, inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SelectContextKey } from './context'
|
||||
import SelectItemText from './SelectItemText.vue'
|
||||
|
||||
import type { SelectItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
useForwardProps,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
const props = defineProps<{
|
||||
value: string
|
||||
disabled?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const props = defineProps<SelectItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
const context = inject(SelectContextKey)!
|
||||
const itemRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
const isSelected = computed(() => context.modelValue.value === props.value)
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
function select() {
|
||||
if (props.disabled) return
|
||||
context.modelValue.value = props.value
|
||||
context.open.value = false
|
||||
}
|
||||
|
||||
function onPointerMove() {
|
||||
if (props.disabled) return
|
||||
itemRef.value?.focus()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
context.registerLabel(props.value, itemRef.value?.textContent?.trim() ?? '')
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
context.unregisterLabel(props.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItem
|
||||
<div
|
||||
ref="itemRef"
|
||||
data-slot="select-item"
|
||||
v-bind="forwardedProps"
|
||||
role="option"
|
||||
:data-value="value"
|
||||
:aria-selected="isSelected"
|
||||
:data-disabled="disabled ? '' : undefined"
|
||||
tabindex="-1"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*=size-])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
@click="select"
|
||||
@pointermove="onPointerMove"
|
||||
>
|
||||
<span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
||||
<SelectItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<CheckIcon class="pointer-events-none" />
|
||||
</slot>
|
||||
</SelectItemIndicator>
|
||||
<slot v-if="isSelected" name="indicator-icon">
|
||||
<CheckIcon class="pointer-events-none" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<SelectItemText>
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { ChevronDownIcon } from '@lucide/vue';
|
||||
|
||||
import type { SelectTriggerProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDownIcon } from '@lucide/vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { inject } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SelectContextKey } from './context'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<SelectTriggerProps & { class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
|
||||
{ size: "default" },
|
||||
defineProps<{ class?: HTMLAttributes['class'], size?: 'sm' | 'default', disabled?: boolean }>(),
|
||||
{ size: 'default' },
|
||||
)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
const context = inject(SelectContextKey)!
|
||||
|
||||
function onClick() {
|
||||
if (props.disabled || context.disabled.value) return
|
||||
context.open.value = !context.open.value
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
context.open.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectTrigger
|
||||
<button
|
||||
:ref="(el) => { context.triggerRef.value = el as HTMLElement | null }"
|
||||
type="button"
|
||||
data-slot="select-trigger"
|
||||
:data-size="size"
|
||||
v-bind="forwardedProps"
|
||||
role="combobox"
|
||||
:aria-controls="context.contentId"
|
||||
:aria-expanded="context.open.value"
|
||||
aria-autocomplete="none"
|
||||
:disabled="disabled || context.disabled.value"
|
||||
:class="cn(
|
||||
'border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm transition-colors select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*=size-])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
props.class,
|
||||
)"
|
||||
@click="onClick"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<slot />
|
||||
<SelectIcon as-child>
|
||||
<ChevronDownIcon class="text-muted-foreground size-4 pointer-events-none" />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
<ChevronDownIcon class="text-muted-foreground size-4 pointer-events-none" />
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in a new issue