Skip to content

Commit

Permalink
refactor: support type inference based on single or multiple item
Browse files Browse the repository at this point in the history
  • Loading branch information
zernonia committed Jan 14, 2025
1 parent 121e305 commit 913a7d4
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 106 deletions.
16 changes: 8 additions & 8 deletions packages/core/src/Accordion/AccordionRoot.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script lang="ts">
import type { ComputedRef, Ref } from 'vue'
import type { PrimitiveProps } from '@/Primitive'
import type { AcceptableValue, DataOrientation, Direction, SingleOrMultipleProps, SingleOrMultipleType } from '@/shared/types'
import type { AcceptableValue, DataOrientation, Direction, SingleOrMultipleType, SingleOrMultipleTypeProps } from '@/shared/types'
import { createContext, useDirection, useForwardExpose } from '@/shared'
export interface AccordionRootProps<T = string | string[]>
extends PrimitiveProps, SingleOrMultipleProps<T> {
export interface AccordionRootProps<S extends SingleOrMultipleType = SingleOrMultipleType, T = string>
extends PrimitiveProps, SingleOrMultipleTypeProps<S, T> {
/**
* When type is "single", allows closing content when clicking trigger for an open item.
* When type is "multiple", this prop has no effect.
Expand Down Expand Up @@ -43,11 +43,11 @@ export interface AccordionRootProps<T = string | string[]>
unmountOnHide?: boolean
}
export type AccordionRootEmits<T extends SingleOrMultipleType = SingleOrMultipleType> = {
export type AccordionRootEmits<S extends SingleOrMultipleType = SingleOrMultipleType> = {
/**
* Event handler called when the expanded state of an item changes
*/
'update:modelValue': [value: (T extends 'single' ? string : string[]) | undefined]
'update:modelValue': [value: (S extends 'single' ? string : string[]) | undefined]
}
export type AccordionRootContext<P extends AccordionRootProps> = {
Expand All @@ -66,19 +66,19 @@ export const [injectAccordionRootContext, provideAccordionRootContext]
= createContext<AccordionRootContext<AccordionRootProps>>('AccordionRoot')
</script>

<script setup lang="ts" generic="T extends (string | string[]), ExplicitType extends SingleOrMultipleType">
<script setup lang="ts" generic="S extends SingleOrMultipleType = SingleOrMultipleType, T extends string = string">
import { Primitive } from '@/Primitive'
import { useSingleOrMultipleValue } from '@/shared/useSingleOrMultipleValue'
import { toRefs } from 'vue'
const props = withDefaults(defineProps<AccordionRootProps<T>>(), {
const props = withDefaults(defineProps<AccordionRootProps<S, T>>(), {
disabled: false,
orientation: 'vertical',
collapsible: false,
unmountOnHide: true,
})
const emits = defineEmits<AccordionRootEmits<ExplicitType>>()
const emits = defineEmits<AccordionRootEmits<S>>()
defineSlots<{
default: (props: {
Expand Down
38 changes: 10 additions & 28 deletions packages/core/src/Calendar/CalendarRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { type Formatter, createContext, useDirection, useLocale } from '@/shared
import { useCalendar, useCalendarState } from './useCalendar'
import { getDefaultDate, handleCalendarInitialFocus } from '@/shared/date'
import type { Grid, Matcher, WeekDayFormat } from '@/date'
import type { Direction } from '@/shared/types'
import type { Direction, SingleOrMultipleProps } from '@/shared/types'
type CalendarRootContext = {
locale: Ref<string>
Expand Down Expand Up @@ -42,9 +42,7 @@ type CalendarRootContext = {
dir: Ref<Direction>
}
interface BaseCalendarRootProps extends PrimitiveProps {
/** The default value for the calendar */
defaultValue?: DateValue
export interface CalendarRootProps<S extends boolean = false> extends PrimitiveProps, SingleOrMultipleProps<S, DateValue> {
/** The default placeholder date */
defaultPlaceholder?: DateValue
/** The placeholder date, which is used to determine what month to display when no date is selected. This updates as the user navigates the calendar and can be used to programmatically control the calendar view */
Expand Down Expand Up @@ -87,25 +85,9 @@ interface BaseCalendarRootProps extends PrimitiveProps {
prevPage?: (placeholder: DateValue) => DateValue
}
export interface MultipleCalendarRootProps extends BaseCalendarRootProps {
/** The controlled checked state of the calendar. Can be bound as `v-model`. */
modelValue?: DateValue[] | undefined
/** Whether or not multiple dates can be selected */
multiple: true
}
export interface SingleCalendarRootProps extends BaseCalendarRootProps {
/** The controlled checked state of the calendar. Can be bound as `v-model`. */
modelValue?: DateValue | undefined
/** Whether or not multiple dates can be selected */
multiple?: false
}
export type CalendarRootProps = MultipleCalendarRootProps | SingleCalendarRootProps
export type CalendarRootEmits = {
export type CalendarRootEmits<S extends boolean = false> = {
/** Event handler called whenever the model value changes */
'update:modelValue': [date: DateValue | undefined]
'update:modelValue': [date: S extends false ? DateValue | undefined : DateValue[] | undefined]
/** Event handler called whenever the placeholder value changes */
'update:placeholder': [date: DateValue]
}
Expand All @@ -114,20 +96,19 @@ export const [injectCalendarRootContext, provideCalendarRootContext]
= createContext<CalendarRootContext>('CalendarRoot')
</script>

<script setup lang="ts">
<script setup lang="ts" generic="S extends boolean = false">
import { onMounted, toRefs, watch } from 'vue'
import { Primitive, usePrimitiveElement } from '@/Primitive'
import { useVModel } from '@vueuse/core'
const props = withDefaults(defineProps<CalendarRootProps>(), {
const props = withDefaults(defineProps<CalendarRootProps<S>>(), {
defaultValue: undefined,
as: 'div',
pagedNavigation: false,
preventDeselect: false,
weekStartsOn: 0,
weekdayFormat: 'narrow',
fixedWeeks: false,
multiple: false,
numberOfMonths: 1,
disabled: false,
readonly: false,
Expand All @@ -136,7 +117,7 @@ const props = withDefaults(defineProps<CalendarRootProps>(), {
isDateDisabled: undefined,
isDateUnavailable: undefined,
})
const emits = defineEmits<CalendarRootEmits>()
const emits = defineEmits<CalendarRootEmits<S>>()
defineSlots<{
default: (props: {
/** The current date of the placeholder */
Expand All @@ -152,7 +133,7 @@ defineSlots<{
/** Whether or not to always display 6 weeks in the calendar */
fixedWeeks: boolean
/** The current date of the calendar */
modelValue: DateValue | undefined
modelValue: CalendarRootProps<S>['modelValue']
}) => any
}>()
Expand Down Expand Up @@ -196,6 +177,7 @@ const defaultDate = getDefaultDate({
})
const placeholder = useVModel(props, 'placeholder', emits, {
// @ts-expect-error ignoring DateValue types
defaultValue: props.defaultPlaceholder ?? defaultDate.copy(),
passive: (props.placeholder === undefined) as false,
}) as Ref<DateValue>
Expand Down Expand Up @@ -345,7 +327,7 @@ provideCalendarRootContext({
:week-starts-on="weekStartsOn"
:locale="locale"
:fixed-weeks="fixedWeeks"
:model-value="modelValue"
:model-value="modelValue as any"
/>
<div
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; white-space: nowrap; width: 1px;"
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/Combobox/ComboboxRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ type ComboboxRootContext<T> = {
export const [injectComboboxRootContext, provideComboboxRootContext]
= createContext<ComboboxRootContext<AcceptableValue>>('ComboboxRoot')
export type ComboboxRootEmits<T = AcceptableValue> = {
export type ComboboxRootEmits<S extends boolean = false, T = AcceptableValue> = {
/** Event handler called when the value changes. */
'update:modelValue': [value: T]
'update:modelValue': [value: S extends false ? T : T[]]
/** Event handler when highlighted element changes. */
'highlight': [payload: { ref: HTMLElement, value: T } | undefined]
/** Event handler called when the open state of the combobox changes. */
'update:open': [value: boolean]
}
export interface ComboboxRootProps<T = AcceptableValue> extends Omit<ListboxRootProps<T>, 'orientation' | 'selectionBehavior' > {
export interface ComboboxRootProps<S extends boolean = false, T = AcceptableValue> extends Omit<ListboxRootProps<S, T>, 'orientation' | 'selectionBehavior' > {
/** The controlled open state of the Combobox. Can be binded with with `v-model:open`. */
open?: boolean
/** The open state of the combobox when it is initially rendered. <br> Use when you do not need to control its open state. */
Expand All @@ -59,17 +59,17 @@ export interface ComboboxRootProps<T = AcceptableValue> extends Omit<ListboxRoot
}
</script>

<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue">
<script setup lang="ts" generic="S extends boolean = false, T extends AcceptableValue = AcceptableValue">
import { computed, nextTick, reactive, ref, toRefs, watch } from 'vue'
import { PopperRoot } from '@/Popper'
import { type EventHookOn, createEventHook, useVModel } from '@vueuse/core'
import { ListboxRoot } from '@/Listbox'
const props = withDefaults(defineProps<ComboboxRootProps<T>>(), {
const props = withDefaults(defineProps<ComboboxRootProps<S, T>>(), {
open: undefined,
resetSearchTermOnBlur: true,
})
const emits = defineEmits<ComboboxRootEmits<T>>()
const emits = defineEmits<ComboboxRootEmits<S, T>>()
defineSlots<{
default: (props: {
Expand Down Expand Up @@ -222,7 +222,7 @@ provideComboboxRootContext({
<ListboxRoot
ref="primitiveElement"
v-bind="$attrs"
v-model="modelValue"
v-model="modelValue as any"
:style="{
pointerEvents: open ? 'auto' : undefined,
}"
Expand Down
25 changes: 10 additions & 15 deletions packages/core/src/Listbox/ListboxRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { createContext, findValuesBetween, useDirection, useFormControl, useKbd, useTypeahead } from '@/shared'
import { Primitive } from '..'
import { type PrimitiveProps, usePrimitiveElement } from '@/Primitive'
import type { AcceptableValue, DataOrientation, Direction, FormFieldProps } from '@/shared/types'
import type { AcceptableValue, DataOrientation, Direction, FormFieldProps, SingleOrMultipleProps } from '@/shared/types'
import { getFocusIntent } from '@/RovingFocus/utils'
type ListboxRootContext<T> = {
Expand Down Expand Up @@ -38,13 +38,7 @@ type ListboxRootContext<T> = {
export const [injectListboxRootContext, provideListboxRootContext]
= createContext<ListboxRootContext<AcceptableValue>>('ListboxRoot')
export interface ListboxRootProps<T = AcceptableValue> extends PrimitiveProps, FormFieldProps {
/** The controlled value of the listbox. Can be binded with with `v-model`. */
modelValue?: T | Array<T>
/** The value of the listbox when initially rendered. Use when you do not need to control the state of the Listbox */
defaultValue?: T | Array<T>
/** Whether multiple options can be selected or not. */
multiple?: boolean
export interface ListboxRootProps<S extends boolean = false, T = AcceptableValue> extends PrimitiveProps, FormFieldProps, SingleOrMultipleProps<S, T> {
/** The orientation of the listbox. <br>Mainly so arrow navigation is done accordingly (left & right vs. up & down) */
orientation?: DataOrientation
/** The reading direction of the listbox when applicable. <br> If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. */
Expand All @@ -62,9 +56,9 @@ export interface ListboxRootProps<T = AcceptableValue> extends PrimitiveProps, F
by?: string | ((a: T, b: T) => boolean)
}
export type ListboxRootEmits<T = AcceptableValue> = {
export type ListboxRootEmits<S extends boolean = false, T = AcceptableValue> = {
/** Event handler called when the value changes. */
'update:modelValue': [value: T]
'update:modelValue': [value: S extends true ? T[] : T]
/** Event handler when highlighted element changes. */
'highlight': [payload: { ref: HTMLElement, value: T } | undefined]
/** Event handler called when container is being focused. Can be prevented. */
Expand All @@ -74,18 +68,18 @@ export type ListboxRootEmits<T = AcceptableValue> = {
}
</script>
<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue">
<script setup lang="ts" generic="S extends boolean = false, T extends AcceptableValue = AcceptableValue">
import { type EventHook, createEventHook, useVModel } from '@vueuse/core'
import { type Ref, nextTick, ref, toRefs, watch } from 'vue'
import { compare } from './utils'
import { useCollection } from '@/Collection'
import { VisuallyHiddenInput } from '@/VisuallyHidden'
const props = withDefaults(defineProps<ListboxRootProps>(), {
const props = withDefaults(defineProps<ListboxRootProps<S, T>>(), {
selectionBehavior: 'toggle',
orientation: 'vertical',
})
const emits = defineEmits<ListboxRootEmits>()
const emits = defineEmits<ListboxRootEmits<S>>()
defineSlots<{
default: (props: {
Expand All @@ -107,7 +101,7 @@ const firstValue = ref<T>()
const isUserAction = ref(false)
const focusable = ref(true)
const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue ?? (multiple.value ? [] : undefined),
defaultValue: props.defaultValue ?? (multiple.value ? ([] as any) : undefined),
passive: (props.modelValue === undefined) as false,
deep: true,
}) as Ref<T | T[] | undefined>
Expand All @@ -128,7 +122,7 @@ function onValueChange(val: T) {
}
else {
if (props.selectionBehavior === 'toggle') {
if (compare(modelValue.value, val, props.by))
if (compare(modelValue.value as T, val, props.by))
modelValue.value = undefined
else
modelValue.value = val
Expand Down Expand Up @@ -364,6 +358,7 @@ provideListboxRootContext({
virtualFocusHook,
virtualKeydownHook,
virtualHighlightHook,
// @ts-expect-error ignoring
by: props.by,
firstValue,
selectionBehavior,
Expand Down
23 changes: 9 additions & 14 deletions packages/core/src/Select/SelectRoot.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
<script lang="ts">
import type { Ref } from 'vue'
import type { AcceptableValue, Direction, FormFieldProps } from '@/shared/types'
import type { AcceptableValue, Direction, FormFieldProps, SingleOrMultipleProps } from '@/shared/types'
import { createContext, isNullish, useDirection, useFormControl } from '@/shared'
import { compare } from './utils'
import { useCollection } from '@/Collection'
export interface SelectRootProps<T = AcceptableValue> extends FormFieldProps {
export interface SelectRootProps<S extends boolean = false, T = AcceptableValue> extends FormFieldProps, SingleOrMultipleProps<S, T> {
/** The controlled open state of the Select. Can be bind as `v-model:open`. */
open?: boolean
/** The open state of the select when it is initially rendered. Use when you do not need to control its open state. */
defaultOpen?: boolean
/** The value of the select when initially rendered. Use when you do not need to control the state of the Select */
defaultValue?: T | Array<T>
/** The controlled value of the Select. Can be bind as `v-model`. */
modelValue?: T | Array<T>
/** Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared. */
by?: string | ((a: T, b: T) => boolean)
/** The reading direction of the combobox when applicable. <br> If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. */
dir?: Direction
/** Whether multiple options can be selected or not. */
multiple?: boolean
/** Native html input `autocomplete` attribute. */
autocomplete?: string
/** When `true`, prevents the user from interacting with Select */
disabled?: boolean
}
export type SelectRootEmits<T = AcceptableValue> = {
export type SelectRootEmits<S extends boolean = false, T = AcceptableValue> = {
/** Event handler called when the value changes. */
'update:modelValue': [value: T]
'update:modelValue': [value: S extends false ? T : T[]]
/** Event handler called when the open state of the context menu changes. */
'update:open': [value: boolean]
}
Expand Down Expand Up @@ -62,7 +56,7 @@ export const [injectSelectRootContext, provideSelectRootContext]
interface SelectOption { value: any, disabled?: boolean, textContent: string }
</script>

<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue">
<script setup lang="ts" generic="S extends boolean =false, T extends AcceptableValue = AcceptableValue">
import { computed, ref, toRefs } from 'vue'
import BubbleSelect from './BubbleSelect.vue'
import { PopperRoot } from '@/Popper'
Expand All @@ -72,11 +66,11 @@ defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SelectRootProps>(), {
const props = withDefaults(defineProps<SelectRootProps<S, T>>(), {
modelValue: undefined,
open: undefined,
})
const emits = defineEmits<SelectRootEmits>()
const emits = defineEmits<SelectRootEmits<S, T>>()
defineSlots<{
default: (props: {
Expand All @@ -90,7 +84,7 @@ defineSlots<{
const { required, disabled, multiple, dir: propDir } = toRefs(props)
const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue ?? (multiple.value ? [] : undefined),
defaultValue: props.defaultValue ?? (multiple.value ? ([] as any) : undefined),
passive: (props.modelValue === undefined) as false,
deep: true,
}) as Ref<T | T[] | undefined>
Expand Down Expand Up @@ -156,6 +150,7 @@ provideSelectRootContext({
modelValue,
// @ts-expect-error Missing infer for AcceptableValue
onValueChange: handleValueChange,
// @ts-expect-error Missing infer for AcceptableValue
by: props.by,
open,
multiple,
Expand Down
Loading

0 comments on commit 913a7d4

Please sign in to comment.