Skip to content

Commit

Permalink
fix(Select): render selected textValue
Browse files Browse the repository at this point in the history
  • Loading branch information
zernonia committed Nov 7, 2024
1 parent e29f3ac commit 1e17cc4
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 60 deletions.
19 changes: 9 additions & 10 deletions packages/core/src/Select/SelectItemText.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import { useForwardExpose } from '@/shared'
import { injectSelectRootContext } from './SelectRoot.vue'
export interface SelectItemTextProps extends PrimitiveProps {}
</script>

<script setup lang="ts">
import { computed, h, onBeforeUnmount, onMounted } from 'vue'
import { injectSelectNativeOptionsContext } from './SelectRoot.vue'
import { computed, onBeforeUnmount, onMounted } from 'vue'
import { injectSelectContentContext } from './SelectContentImpl.vue'
import { injectSelectItemContext } from './SelectItem.vue'
import { Primitive } from '@/Primitive'
Expand All @@ -20,19 +20,18 @@ const props = withDefaults(defineProps<SelectItemTextProps>(), {
as: 'span',
})
const rootContext = injectSelectRootContext()
const contentContext = injectSelectContentContext()
const nativeOptionContext = injectSelectNativeOptionsContext()
const itemContext = injectSelectItemContext()
const { forwardRef, currentElement: itemTextElement } = useForwardExpose()
const nativeOption = computed(() => {
return h('option', {
key: itemContext.value.toString(),
const optionProps = computed(() => {
return {
value: itemContext.value,
disabled: itemContext.disabled.value,
textContent: itemTextElement.value?.textContent,
})
textContent: itemTextElement.value?.textContent ?? itemContext.value.toString(),
}
})
onMounted(() => {
Expand All @@ -44,11 +43,11 @@ onMounted(() => {
itemContext.value,
itemContext.disabled.value,
)
nativeOptionContext.onNativeOptionAdd(nativeOption.value)
rootContext.onOptionAdd(optionProps.value)
})
onBeforeUnmount(() => {
nativeOptionContext.onNativeOptionRemove(nativeOption.value)
rootContext.onOptionRemove(optionProps.value)
})
</script>

Expand Down
87 changes: 41 additions & 46 deletions packages/core/src/Select/SelectRoot.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Ref, VNode } from 'vue'
import type { Ref } from 'vue'
import type { AcceptableValue, Direction, FormFieldProps } from '@/shared/types'
import { createContext, isNullish, useDirection, useFormControl } from '@/shared'
import { compare } from './utils'
Expand Down Expand Up @@ -50,18 +50,16 @@ export interface SelectRootContext<T> {
triggerPointerDownPosRef: Ref<{ x: number, y: number } | null>
isEmptyModelValue: Ref<boolean>
disabled?: Ref<boolean>
optionsSet: Ref<Set<SelectOption>>
onOptionAdd: (option: SelectOption) => void
onOptionRemove: (option: SelectOption) => void
}
export const [injectSelectRootContext, provideSelectRootContext]
= createContext<SelectRootContext<AcceptableValue>>('SelectRoot')
export interface SelectNativeOptionsContext {
onNativeOptionAdd: (option: VNode) => void
onNativeOptionRemove: (option: VNode) => void
}
export const [injectSelectNativeOptionsContext, provideSelectNativeOptionsContext]
= createContext<SelectNativeOptionsContext>('SelectRoot')
interface SelectOption { value: any, disabled?: boolean, textContent: string }
</script>

<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue">
Expand Down Expand Up @@ -95,7 +93,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue ?? (multiple.value ? [] : undefined),
passive: (props.modelValue === undefined) as false,
deep: true,
}) as Ref<T | Array<T> | undefined>
}) as Ref<T | T[] | undefined>
const open = useVModel(props, 'open', emits, {
defaultValue: props.defaultOpen,
Expand All @@ -119,6 +117,30 @@ const isEmptyModelValue = computed(() => {
useCollection({ isProvider: true })
const dir = useDirection(propDir)
const isFormControl = useFormControl(triggerElement)
const optionsSet = ref<Set<SelectOption>>(new Set())
// The native `select` only associates the correct default value if the corresponding
// `option` is rendered as a child **at the same time** as itself.
// Because it might take a few renders for our items to gather the information to build
// the native `option`(s), we generate a key on the `select` to make sure Vue re-builds it
// each time the options change.
const nativeSelectKey = computed(() => {
return Array.from(optionsSet.value)
.map(option => option.value)
.join(';')
})
function handleValueChange(value: T) {
if (multiple.value && Array.isArray(modelValue.value)) {
const index = modelValue.value.findIndex(i => compare(i, value, props.by))
index === -1 ? modelValue.value.push(value) : modelValue.value.splice(index, 1)
}
else {
modelValue.value = value
}
}
provideSelectRootContext({
triggerElement,
onTriggerChange: (node) => {
Expand All @@ -130,15 +152,8 @@ provideSelectRootContext({
},
contentId: '',
modelValue,
onValueChange: (value: any) => {
if (multiple.value && Array.isArray(modelValue.value)) {
const index = modelValue.value.findIndex(i => compare(i, value, props.by))
index === -1 ? modelValue.value.push(value) : modelValue.value.splice(index, 1)
}
else {
modelValue.value = value
}
},
// @ts-expect-error Missing infer for AcceptableValue
onValueChange: handleValueChange,
by: props.by,
open,
multiple,
Expand All @@ -150,29 +165,10 @@ provideSelectRootContext({
triggerPointerDownPosRef,
disabled,
isEmptyModelValue,
})
const isFormControl = useFormControl(triggerElement)
const nativeOptionsSet = ref<Set<VNode>>(new Set())
// The native `select` only associates the correct default value if the corresponding
// `option` is rendered as a child **at the same time** as itself.
// Because it might take a few renders for our items to gather the information to build
// the native `option`(s), we generate a key on the `select` to make sure Vue re-builds it
// each time the options change.
const nativeSelectKey = computed(() => {
return Array.from(nativeOptionsSet.value)
.map(option => option.props?.value)
.join(';')
})
provideSelectNativeOptionsContext({
onNativeOptionAdd: (option) => {
nativeOptionsSet.value.add(option)
},
onNativeOptionRemove: (option) => {
nativeOptionsSet.value.delete(option)
},
optionsSet,
onOptionAdd: option => optionsSet.value.add(option),
onOptionRemove: option => optionsSet.value.delete(option),
})
</script>

Expand All @@ -194,17 +190,16 @@ provideSelectNativeOptionsContext({
:autocomplete="autocomplete"
:disabled="disabled"
:value="modelValue"
@change="modelValue = $event.target.value"
@change="handleValueChange($event.target.value)"
>
<option
v-if="modelValue === undefined"
value=""
/>
<component
v-bind="option.props"
:is="option"
v-for="option in Array.from(nativeOptionsSet)"
:key="option.key ?? ''"
<option
v-for="option in Array.from(optionsSet)"
:key="option.value ?? ''"
v-bind="option"
/>
</BubbleSelect>
</PopperRoot>
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/Select/SelectValue.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import type { AcceptableValue } from '@/shared/types'
export interface SelectValueProps extends PrimitiveProps {
/** The content that will be rendered inside the `SelectValue` when no `value` or `defaultValue` is set. */
Expand Down Expand Up @@ -27,10 +28,14 @@ onMounted(() => {
})
const slotText = computed(() => {
if (Array.isArray(rootContext.modelValue.value))
return rootContext.modelValue.value.length === 0 ? props.placeholder : rootContext.modelValue.value.join(', ')
else
return rootContext.modelValue.value || props.placeholder
const options = Array.from(rootContext.optionsSet.value)
const getOption = (value?: AcceptableValue) => options.find(option => option.value === value)
if (Array.isArray(rootContext.modelValue.value)) {
return rootContext.modelValue.value.length === 0 ? props.placeholder : rootContext.modelValue.value.map(value => getOption(value)?.textContent).join(', ')
}
else {
return getOption(rootContext.modelValue.value)?.textContent || props.placeholder
}
})
</script>

Expand Down

0 comments on commit 1e17cc4

Please # to comment.