Skip to content

Commit cea5ac2

Browse files
committed
type changes
1 parent 5c7bc89 commit cea5ac2

File tree

5 files changed

+75
-51
lines changed

5 files changed

+75
-51
lines changed

packages/leva/src/plugins/Select/select-plugin.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { SelectInput, InternalSelectSettings } from './select-types'
22
import { z } from 'zod'
33

4-
const zValidPrimitive = z.union([z.string(), z.number(), z.boolean()])
4+
const zValidPrimitive = z.union([z.string(), z.number(), z.boolean(), z.function()])
55

66
/**
77
* Schema for the usecase
@@ -28,14 +28,18 @@ const keyAsLabelObjectSchema = z.record(z.string(), zValidPrimitive)
2828
* [{ value: 'x', label: 'X' }, { value: 'y', label: 'Y' }]
2929
* ```
3030
*/
31-
const valueLabelObjectSchema = z.object({
31+
export const valueLabelObjectSchema = z.object({
3232
value: zValidPrimitive,
3333
label: z.string().optional(),
3434
})
3535

3636
const arrayOfValueLabelObjectsSchema = z.array(valueLabelObjectSchema)
3737

38-
const allUsecases = z.union([arrayOfPrimitivesSchema, keyAsLabelObjectSchema, arrayOfValueLabelObjectsSchema])
38+
export const selectOptionsSchema = z.union([
39+
arrayOfPrimitivesSchema,
40+
keyAsLabelObjectSchema,
41+
arrayOfValueLabelObjectsSchema,
42+
])
3943

4044
/**
4145
* Schema for the settings object - checks if it has an 'options' key
@@ -44,11 +48,11 @@ const allUsecases = z.union([arrayOfPrimitivesSchema, keyAsLabelObjectSchema, ar
4448
* 2. Array of {value, label} objects: [{ value: 'x', label: 'X' }]
4549
* 3. Object with key-value pairs: { x: 1, y: 2 }
4650
*
47-
* Note: We use allUsecases which handles detailed validation, so invalid formats
51+
* Note: We use selectOptionsSchema which handles detailed validation, so invalid formats
4852
* will be caught and warned about in normalize()
4953
*/
5054
const selectInputSchema = z.object({
51-
options: allUsecases,
55+
options: selectOptionsSchema,
5256
})
5357

5458
// the options attribute is either a key value object, an array, or an array of {value, label} objects
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { LevaInputProps } from '../../types'
2-
import type { SelectOption } from '../../types/public'
2+
import type { z } from 'zod'
3+
import type { selectOptionsSchema } from './select-plugin'
34

4-
export type SelectSettings<U = unknown> = { options: Record<string, U> | U[] | SelectOption<U>[] }
5+
export type SelectSettings = { options: z.infer<typeof selectOptionsSchema> }
56
export type InternalSelectSettings = { keys: string[]; values: any[] }
67

7-
export type SelectInput<P = unknown, U = unknown> = { value?: P } & SelectSettings<U>
8+
export type SelectInput<P = unknown> = { value?: P } & SelectSettings
89

910
export type SelectProps = LevaInputProps<any, InternalSelectSettings, number>

packages/leva/src/types/public.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ expectType<{ a: string }>(useControls({ a: 'some string' }))
5959
*/
6060
expectType<{ a: string }>(useControls({ a: { options: ['foo', 'bar'] } }))
6161
expectType<{ a: number | string }>(useControls({ a: { options: [1, 'bar'] } }))
62-
expectType<{ a: string | number | Array<string | number> }>(useControls({ a: { options: ['foo', 1, ['foo', 'bar']] } }))
6362
expectType<{ a: boolean | number }>(useControls({ a: { options: { foo: 1, bar: true } } }))
64-
expectType<{ a: number | string | string[] }>(useControls({ a: { value: 3, options: ['foo', ['foo', 'bar']] } }))
6563
expectType<{ a: string }>(
6664
useControls({
6765
a: {

packages/leva/src/types/public.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import type { VectorSettings } from '../plugins/Vector/vector-types'
55
import { StoreType, Data, DataInput } from './internal'
66
import type { BeautifyUnionType, UnionToIntersection } from './utils'
7+
import type { z } from 'zod'
8+
import type { valueLabelObjectSchema, selectOptionsSchema } from '../plugins/Select/select-plugin'
79

810
export type RenderFn = (get: (key: string) => any) => boolean
911

@@ -104,12 +106,12 @@ export type IntervalInput = { value: [number, number]; min: number; max: number
104106

105107
export type ImageInput = { image: undefined | string }
106108

107-
export type SelectOption<T = unknown> = { value: T; label?: string }
109+
// Infer valid select types from Zod schemas to ensure runtime validation and types stay in sync
110+
export type SelectOption<T = unknown> = z.infer<typeof valueLabelObjectSchema> & { value: T }
111+
export type SelectOptionsType = z.infer<typeof selectOptionsSchema>
112+
export type SelectInput = { options: SelectOptionsType; value?: any }
108113

109-
type SelectInput = { options: any[] | Record<string, any> | SelectOption[]; value?: any }
110-
111-
// Union branches prevent SelectOption objects from appearing in inferred value types.
112-
// SelectOption<T>[] branch extracts T; Exclude<T, SelectOption> branch handles primitives.
114+
// Type inference helpers that extract value types from different option formats
113115
type SelectWithValueInput<T, K> =
114116
| { options: SelectOption<T>[]; value: K }
115117
| { options: Exclude<T, SelectOption<any>>[] | Record<string, Exclude<T, SelectOption<any>>>; value: K }
Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import React from 'react'
2-
import { StoryFn, Meta } from '@storybook/react'
2+
import type { Meta, StoryObj } from '@storybook/react'
33

44
import Reset from '../components/decorator-reset'
55

66
import { useControls } from '../../src'
77

8-
export default {
8+
const meta = {
99
title: 'Inputs/Select',
1010
decorators: [Reset],
11-
} as Meta
11+
} satisfies Meta
1212

13-
const Template: StoryFn = (args) => {
13+
export default meta
14+
type Story = StoryObj<typeof meta>
15+
16+
const Render = (args: any) => {
1417
const values = useControls({
1518
foo: args,
1619
})
@@ -25,50 +28,60 @@ const Template: StoryFn = (args) => {
2528
/**
2629
* Passes a list of values. The value will be used as both value AND label.
2730
*/
28-
export const Simple = Template.bind({})
29-
Simple.args = {
30-
value: 'x',
31-
options: ['x', 'y'],
32-
}
31+
export const Simple: Story = {
32+
args: {
33+
value: 'x',
34+
options: ['x', 'y'],
35+
},
36+
render: Render,
37+
} as Story
3338

3439
/**
3540
* No value is passed, so the first option will be selected as the default.
3641
*/
37-
export const NoValue = Template.bind({})
38-
NoValue.args = {
39-
options: ['x', 'y'],
40-
}
42+
export const NoValue: Story = {
43+
args: {
44+
options: ['x', 'y'],
45+
},
46+
render: Render,
47+
} as Story
4148

4249
/**
4350
* Passes an object of values. The key will be used as label and the value will be used as value.
4451
*/
45-
export const CustomLabels = Template.bind({})
46-
CustomLabels.args = {
47-
value: 'helloWorld',
48-
options: {
49-
'Hello World': 'helloWorld',
50-
'Leva is awesome!': 'leva',
52+
export const CustomLabels: Story = {
53+
args: {
54+
value: 'helloWorld',
55+
options: {
56+
'Hello World': 'helloWorld',
57+
'Leva is awesome!': 'leva',
58+
},
5159
},
52-
}
60+
render: Render,
61+
} as Story
5362

54-
export const InferredValueAsOption = Template.bind({})
55-
InferredValueAsOption.args = {
56-
value: true,
57-
options: [false],
58-
}
63+
export const InferredValueAsOption: Story = {
64+
args: {
65+
value: true,
66+
options: [false],
67+
},
68+
render: Render,
69+
} as Story
5970

6071
const ComponentA = () => <span>Component A</span>
6172
const ComponentB = () => <span>Component B</span>
6273

6374
/**
6475
* Shows passing functions as the option values.
6576
*/
66-
export const FunctionAsOptions = () => {
77+
const FunctionAsOptionsRender = () => {
6778
const values = useControls({
6879
foo: { options: { none: '', ComponentA, ComponentB } },
6980
})
7081

71-
if (!values.foo) return null
82+
if (!values.foo) {
83+
return <div>No component selected</div>
84+
}
7285

7386
// render value.foo as a react component
7487
const Component = values.foo as React.ComponentType
@@ -80,15 +93,21 @@ export const FunctionAsOptions = () => {
8093
)
8194
}
8295

96+
export const FunctionAsOptions: Story = {
97+
render: FunctionAsOptionsRender,
98+
} as Story
99+
83100
/**
84101
* Shows passing a value/label records array.
85102
*/
86-
export const ValueLabelObjects = Template.bind({})
87-
ValueLabelObjects.args = {
88-
value: '#f00',
89-
options: [
90-
{ value: '#f00', label: 'red' },
91-
{ value: '#0f0', label: 'green' },
92-
{ value: '#00f', label: 'blue' },
93-
],
94-
}
103+
export const ValueLabelObjects: Story = {
104+
args: {
105+
value: '#f00',
106+
options: [
107+
{ value: '#f00', label: 'red' },
108+
{ value: '#0f0', label: 'green' },
109+
{ value: '#00f', label: 'blue' },
110+
],
111+
},
112+
render: Render,
113+
} as Story

0 commit comments

Comments
 (0)