Skip to content

Commit 2a0fb94

Browse files
committed
feat(Select): support different modelValue, option types
1 parent 6a62348 commit 2a0fb94

File tree

7 files changed

+75
-40
lines changed

7 files changed

+75
-40
lines changed

packages/radix-vue/src/Select/SelectContent.vue

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export interface SelectContentProps extends SelectContentImplProps {
1616
</script>
1717

1818
<script setup lang="ts">
19-
import { computed, onMounted, ref } from 'vue'
2019
import SelectContentImpl from './SelectContentImpl.vue'
2120
import { injectSelectRootContext } from './SelectRoot.vue'
2221
import { Presence } from '@/Presence'
@@ -32,16 +31,11 @@ const emits = defineEmits<SelectContentEmits>()
3231
const forwarded = useForwardPropsEmits(props, emits)
3332
3433
const rootContext = injectSelectRootContext()
35-
36-
const presenceRef = ref<InstanceType<typeof Presence>>()
37-
const renderPresence = computed(() => props.forceMount || rootContext.open.value)
3834
</script>
3935

4036
<template>
4137
<Presence
42-
v-if="renderPresence"
43-
ref="presenceRef"
44-
:present="true"
38+
:present="props.forceMount || rootContext.open.value"
4539
>
4640
<SelectContentImpl v-bind="{ ...forwarded, ...$attrs }">
4741
<slot />

packages/radix-vue/src/Select/SelectContentImpl.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,23 @@ import {
1414
useHideOthers,
1515
useTypeahead,
1616
} from '@/shared'
17+
import type { AcceptableValue } from '@/shared/types'
18+
import { compare } from './utils'
1719
1820
interface SelectContentContext {
1921
content?: Ref<HTMLElement | undefined>
2022
viewport?: Ref<HTMLElement | undefined>
2123
onViewportChange: (node: HTMLElement | undefined) => void
2224
itemRefCallback: (
2325
node: HTMLElement | undefined,
24-
value: string,
26+
value: AcceptableValue,
2527
disabled: boolean
2628
) => void
2729
selectedItem?: Ref<HTMLElement | undefined>
2830
onItemLeave?: () => void
2931
itemTextRefCallback: (
3032
node: HTMLElement | undefined,
31-
value: string,
33+
value: AcceptableValue,
3234
disabled: boolean
3335
) => void
3436
focusSelectedItem?: () => void
@@ -218,7 +220,7 @@ provideSelectContentContext({
218220
const isFirstValidItem = !firstValidItemFoundRef.value && !disabled
219221
const isSelectedItem
220222
= rootContext.modelValue?.value !== undefined
221-
&& rootContext.modelValue?.value === value
223+
&& compare(rootContext.modelValue.value, value, rootContext.by) // rootContext.modelValue?.value === value
222224
if (isSelectedItem || isFirstValidItem) {
223225
selectedItem.value = node
224226
if (isFirstValidItem)
@@ -234,7 +236,7 @@ provideSelectContentContext({
234236
const isFirstValidItem = !firstValidItemFoundRef.value && !disabled
235237
const isSelectedItem
236238
= rootContext.modelValue?.value !== undefined
237-
&& rootContext.modelValue?.value === value
239+
&& compare(rootContext.modelValue.value, value, rootContext.by) // rootContext.modelValue?.value === value
238240
if (isSelectedItem || isFirstValidItem)
239241
selectedItemText.value = node
240242
},

packages/radix-vue/src/Select/SelectItem.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import type { Ref } from 'vue'
33
import type { PrimitiveProps } from '@/Primitive'
44
import { createContext, useForwardExpose, useId } from '@/shared'
5+
import type { AcceptableValue } from '@/shared/types'
56
6-
interface SelectItemContext {
7-
value: string
7+
interface SelectItemContext<T = AcceptableValue> {
8+
value: T
89
textId: string
910
disabled: Ref<boolean>
1011
isSelected: Ref<boolean>
@@ -14,9 +15,9 @@ interface SelectItemContext {
1415
export const [injectSelectItemContext, provideSelectItemContext]
1516
= createContext<SelectItemContext>('SelectItem')
1617
17-
export interface SelectItemProps extends PrimitiveProps {
18+
export interface SelectItemProps<T = AcceptableValue> extends PrimitiveProps {
1819
/** The value given as data when submitted with a `name`. */
19-
value: string
20+
value: T
2021
/** When `true`, prevents the user from interacting with the item. */
2122
disabled?: boolean
2223
/**
@@ -40,7 +41,7 @@ import {
4041
} from 'vue'
4142
import { injectSelectRootContext } from './SelectRoot.vue'
4243
import { injectSelectContentContext } from './SelectContentImpl.vue'
43-
import { SELECTION_KEYS } from './utils'
44+
import { SELECTION_KEYS, getValue } from './utils'
4445
import { Primitive } from '@/Primitive'
4546
4647
const props = defineProps<SelectItemProps>()

packages/radix-vue/src/Select/SelectItemText.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const { forwardRef, currentElement: itemTextElement } = useForwardExpose()
2828
2929
const nativeOption = computed(() => {
3030
return h('option', {
31-
key: itemContext.value,
31+
key: itemContext.value.toString(),
3232
value: itemContext.value,
3333
disabled: itemContext.disabled.value,
3434
innerHTML: itemTextElement.value?.textContent,

packages/radix-vue/src/Select/SelectRoot.vue

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
<script lang="ts">
22
import type { Ref, VNode } from 'vue'
3-
import type { Direction } from '../shared/types'
3+
import type { AcceptableValue, Direction } from '@/shared/types'
44
import { createContext, useDirection, useFormControl } from '@/shared'
55
6-
export interface SelectRootProps {
6+
export interface SelectRootProps<T = AcceptableValue> {
77
/** The controlled open state of the Select. Can be bind as `v-model:open`. */
88
open?: boolean
99
/** The open state of the select when it is initially rendered. Use when you do not need to control its open state. */
1010
defaultOpen?: boolean
1111
/** The value of the select when initially rendered. Use when you do not need to control the state of the Select */
12-
defaultValue?: string
12+
defaultValue?: T
1313
/** The controlled value of the Select. Can be bind as `v-model`. */
14-
modelValue?: string
14+
modelValue?: T
15+
/** Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared. */
16+
by?: string | ((a: T, b: T) => boolean)
1517
/** The reading direction of the combobox when applicable. <br> If omitted, inherits globally from `DirectionProvider` or assumes LTR (left-to-right) reading mode. */
1618
dir?: Direction
1719
/** The name of the Select. Submitted with its owning form as part of a name/value pair. */
@@ -23,31 +25,33 @@ export interface SelectRootProps {
2325
/** When `true`, indicates that the user must select a value before the owning form can be submitted. */
2426
required?: boolean
2527
}
26-
export type SelectRootEmits = {
28+
29+
export type SelectRootEmits<T = AcceptableValue> = {
2730
/** Event handler called when the value changes. */
28-
'update:modelValue': [value: string]
31+
'update:modelValue': [value: T]
2932
/** Event handler called when the open state of the context menu changes. */
3033
'update:open': [value: boolean]
3134
}
3235
33-
export interface SelectRootContext {
36+
interface SelectRootContext<T> {
3437
triggerElement: Ref<HTMLElement | undefined>
3538
onTriggerChange: (node: HTMLElement | undefined) => void
3639
valueElement: Ref<HTMLElement | undefined>
3740
onValueElementChange: (node: HTMLElement) => void
3841
contentId: string
39-
modelValue?: Ref<string>
40-
onValueChange: (value: string) => void
42+
modelValue?: Ref<T>
43+
onValueChange: (value: T) => void
4144
open: Ref<boolean>
4245
required?: Ref<boolean>
46+
by?: string | ((a: T, b: T) => boolean)
4347
onOpenChange: (open: boolean) => void
4448
dir: Ref<Direction>
4549
triggerPointerDownPosRef: Ref<{ x: number, y: number } | null>
4650
disabled?: Ref<boolean>
4751
}
4852
4953
export const [injectSelectRootContext, provideSelectRootContext]
50-
= createContext<SelectRootContext>('SelectRoot')
54+
= createContext<SelectRootContext<AcceptableValue>>('SelectRoot')
5155
5256
export interface SelectNativeOptionsContext {
5357
onNativeOptionAdd: (option: VNode) => void
@@ -58,18 +62,20 @@ export const [injectSelectNativeOptionsContext, provideSelectNativeOptionsContex
5862
= createContext<SelectNativeOptionsContext>('SelectRoot')
5963
</script>
6064

61-
<script setup lang="ts">
65+
<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue">
6266
import { computed, ref, toRefs } from 'vue'
6367
import BubbleSelect from './BubbleSelect.vue'
6468
import { PopperRoot } from '@/Popper'
6569
import { useVModel } from '@vueuse/core'
6670
71+
defineOptions({
72+
inheritAttrs: false,
73+
})
74+
6775
const props = withDefaults(defineProps<SelectRootProps>(), {
68-
defaultValue: '',
6976
modelValue: undefined,
7077
open: undefined,
7178
})
72-
7379
const emits = defineEmits<SelectRootEmits>()
7480
7581
defineSlots<{
@@ -84,7 +90,7 @@ defineSlots<{
8490
const modelValue = useVModel(props, 'modelValue', emits, {
8591
defaultValue: props.defaultValue,
8692
passive: (props.modelValue === undefined) as false,
87-
}) as Ref<string>
93+
}) as Ref<T>
8894
8995
const open = useVModel(props, 'open', emits, {
9096
defaultValue: props.defaultOpen,
@@ -111,9 +117,10 @@ provideSelectRootContext({
111117
},
112118
contentId: '',
113119
modelValue,
114-
onValueChange: (value) => {
120+
onValueChange: (value: any) => {
115121
modelValue.value = value
116122
},
123+
by: props.by,
117124
open,
118125
required,
119126
onOpenChange: (value) => {
@@ -150,11 +157,6 @@ provideSelectNativeOptionsContext({
150157

151158
<template>
152159
<PopperRoot>
153-
<slot
154-
:model-value="modelValue"
155-
:open="open"
156-
/>
157-
158160
<BubbleSelect
159161
v-if="isFormControl"
160162
:key="nativeSelectKey"

packages/radix-vue/src/Select/story/_SelectTest.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ const options = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
3232
class="min-w-[160px] inline-flex items-center justify-between rounded px-[15px] text-[13px] leading-none h-[35px] gap-[5px] bg-white text-violet11 shadow-[0_2px_10px] shadow-black/10 hover:bg-mauve3 focus:shadow-[0_0_0_2px] focus:shadow-black data-[placeholder]:text-violet9 outline-none"
3333
aria-label="Customise options"
3434
>
35-
<SelectValue placeholder="Please select a fruit">
36-
{{ fruit }}
37-
</SelectValue>
35+
<SelectValue placeholder="Please select a fruit" />
3836
<Icon
3937
icon="radix-icons:chevron-down"
4038
class="h-4 w-4"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
1+
import isEqual from 'fast-deep-equal'
2+
13
export const OPEN_KEYS = [' ', 'Enter', 'ArrowUp', 'ArrowDown']
24
export const SELECTION_KEYS = [' ', 'Enter']
35
export const CONTENT_MARGIN = 10
6+
7+
export function getValue<T>(value: T, by?: string | ((a: T) => boolean)) {
8+
if (typeof value === 'string')
9+
return value
10+
11+
if (typeof by === 'function')
12+
return by(value)
13+
14+
if (typeof by === 'string')
15+
return value?.[by as keyof T]
16+
}
17+
18+
export function valueComparator<T>(value: T | T[] | undefined, currentValue: T, comparator?: string | ((a: T, b: T) => boolean)) {
19+
if (value === undefined)
20+
return false
21+
else if (Array.isArray(value))
22+
return value.some(val => compare(val, currentValue, comparator))
23+
else
24+
return compare(value, currentValue, comparator)
25+
}
26+
27+
export function compare<T>(value?: T, currentValue?: T, comparator?: string | ((a: T, b: T) => boolean)) {
28+
if (value === undefined || currentValue === undefined)
29+
return false
30+
31+
if (typeof value === 'string')
32+
return value === currentValue
33+
34+
if (typeof comparator === 'function')
35+
return comparator(value, currentValue)
36+
37+
if (typeof comparator === 'string')
38+
return value?.[comparator as keyof T] === currentValue?.[comparator as keyof T]
39+
40+
return isEqual(value, currentValue)
41+
}

0 commit comments

Comments
 (0)