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">
|
<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"
|
const props = defineProps<{
|
||||||
import type { HTMLAttributes } from "vue"
|
value: string
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
disabled?: boolean
|
||||||
import {
|
class?: HTMLAttributes['class']
|
||||||
SelectItem,
|
}>()
|
||||||
SelectItemIndicator,
|
|
||||||
SelectItemText,
|
|
||||||
useForwardProps,
|
|
||||||
} from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectItem
|
<div
|
||||||
|
ref="itemRef"
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
v-bind="forwardedProps"
|
role="option"
|
||||||
|
:data-value="value"
|
||||||
|
:aria-selected="isSelected"
|
||||||
|
:data-disabled="disabled ? '' : undefined"
|
||||||
|
tabindex="-1"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
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',
|
'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,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
@click="select"
|
||||||
|
@pointermove="onPointerMove"
|
||||||
>
|
>
|
||||||
<span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
<span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
||||||
<SelectItemIndicator>
|
<slot v-if="isSelected" name="indicator-icon">
|
||||||
<slot name="indicator-icon">
|
|
||||||
<CheckIcon class="pointer-events-none" />
|
<CheckIcon class="pointer-events-none" />
|
||||||
</slot>
|
</slot>
|
||||||
</SelectItemIndicator>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<SelectItemText>
|
<SelectItemText>
|
||||||
<slot />
|
<slot />
|
||||||
</SelectItemText>
|
</SelectItemText>
|
||||||
</SelectItem>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,49 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronDownIcon } from '@lucide/vue';
|
import { ChevronDownIcon } from '@lucide/vue'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
import type { SelectTriggerProps } from "reka-ui"
|
import { inject } from 'vue'
|
||||||
import type { HTMLAttributes } from "vue"
|
import { cn } from '@/lib/utils'
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
import { SelectContextKey } from './context'
|
||||||
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<SelectTriggerProps & { class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
|
defineProps<{ class?: HTMLAttributes['class'], size?: 'sm' | 'default', disabled?: boolean }>(),
|
||||||
{ size: "default" },
|
{ size: 'default' },
|
||||||
)
|
)
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class", "size")
|
const context = inject(SelectContextKey)!
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectTrigger
|
<button
|
||||||
|
:ref="(el) => { context.triggerRef.value = el as HTMLElement | null }"
|
||||||
|
type="button"
|
||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
:data-size="size"
|
: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(
|
: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',
|
'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,
|
props.class,
|
||||||
)"
|
)"
|
||||||
|
@click="onClick"
|
||||||
|
@keydown="onKeydown"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<SelectIcon as-child>
|
|
||||||
<ChevronDownIcon class="text-muted-foreground size-4 pointer-events-none" />
|
<ChevronDownIcon class="text-muted-foreground size-4 pointer-events-none" />
|
||||||
</SelectIcon>
|
</button>
|
||||||
</SelectTrigger>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue