Skip to content

Commit b8c214e

Browse files
Make React types more compatible with other libraries (#2282)
* Export explicit props types * wip * wip * wip * wip dialog types * wip * Fix build * Upgrade esbuild * Add aliased types for ComponentLabel and ComponentDescription * Update lockfile * Update changelog * Update exported prop type names * Make onChange optional * Update tests * Use `never` in CleanProps Using a branded type doesn’t work properly with unions * Fix types * wip * work on types * wip * wip * Tweak types in render helpers * Fix CS * Fix changelog * Tweak render prop types for combobox * Update hidden props type name * remove unused type * Tweak types * Update TypeScript version
1 parent c7f6bc6 commit b8c214e

File tree

26 files changed

+1226
-560
lines changed

26 files changed

+1226
-560
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"@swc/jest": "^0.2.17",
4242
"@testing-library/jest-dom": "^5.16.4",
4343
"@types/node": "^14.14.22",
44-
"esbuild": "^0.14.11",
44+
"esbuild": "^0.17.8",
4545
"fast-glob": "^3.2.11",
4646
"husky": "^4.3.8",
4747
"jest": "26",
@@ -51,6 +51,6 @@
5151
"prettier-plugin-tailwindcss": "^0.1.4",
5252
"rimraf": "^3.0.2",
5353
"tslib": "^2.3.1",
54-
"typescript": "^4.5.4"
54+
"typescript": "^4.9.5"
5555
}
5656
}

packages/@headlessui-react/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add explicit props types for every component ([#2282](https://github.com/tailwindlabs/headlessui/pull/2282))
13+
1014
### Fixed
1115

1216
- Ensure the main tree and parent `Dialog` components are marked as `inert` ([#2290](https://github.com/tailwindlabs/headlessui/pull/2290))
1317
- Fix nested `Popover` components not opening ([#2293](https://github.com/tailwindlabs/headlessui/pull/2293))
18+
- Make React types more compatible with other libraries ([#2282](https://github.com/tailwindlabs/headlessui/pull/2282))
1419

1520
## [1.7.11] - 2023-02-15
1621

packages/@headlessui-react/src/components/combobox/combobox.test.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,10 @@ describe('Rendering', () => {
424424

425425
render(
426426
<Combobox name="assignee" by="id">
427-
<Combobox.Input displayValue={(value: { name: string }) => value.name} />
427+
<Combobox.Input
428+
displayValue={(value: { name: string }) => value.name}
429+
onChange={NOOP}
430+
/>
428431
<Combobox.Options>
429432
{data.map((person) => (
430433
<Combobox.Option key={person.id} value={person}>

packages/@headlessui-react/src/components/combobox/combobox.tsx

+133-42
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@ import { useTreeWalker } from '../../hooks/use-tree-walker'
3131

3232
import { calculateActiveIndex, Focus } from '../../utils/calculate-active-index'
3333
import { disposables } from '../../utils/disposables'
34-
import { forwardRefWithAs, render, compact, PropsForFeatures, Features } from '../../utils/render'
34+
import {
35+
forwardRefWithAs,
36+
render,
37+
compact,
38+
PropsForFeatures,
39+
Features,
40+
HasDisplayName,
41+
RefProp,
42+
} from '../../utils/render'
3543
import { isDisabledReactIssue7711 } from '../../utils/bugs'
3644
import { match } from '../../utils/match'
3745
import { objectToFormEntries } from '../../utils/form'
@@ -313,12 +321,12 @@ function stateReducer<T>(state: StateDefinition<T>, action: Actions<T>) {
313321
// ---
314322

315323
let DEFAULT_COMBOBOX_TAG = Fragment
316-
interface ComboboxRenderPropArg<T> {
324+
interface ComboboxRenderPropArg<TValue, TActive = TValue> {
317325
open: boolean
318326
disabled: boolean
319327
activeIndex: number | null
320-
activeOption: T | null
321-
value: T
328+
activeOption: TActive | null
329+
value: TValue
322330
}
323331

324332
type O = 'value' | 'defaultValue' | 'nullable' | 'multiple' | 'onChange' | 'by'
@@ -336,7 +344,7 @@ type ComboboxValueProps<
336344
multiple: true
337345
onChange?(value: EnsureArray<TValue>): void
338346
by?: ByComparator<TValue>
339-
} & Props<TTag, ComboboxRenderPropArg<EnsureArray<TValue>>, O>)
347+
} & Props<TTag, ComboboxRenderPropArg<EnsureArray<TValue>, TValue>, O>)
340348
| ({
341349
value?: TValue | null
342350
defaultValue?: TValue | null
@@ -352,7 +360,7 @@ type ComboboxValueProps<
352360
multiple: true
353361
onChange?(value: EnsureArray<TValue>): void
354362
by?: ByComparator<TValue extends Array<infer U> ? U : TValue>
355-
} & Expand<Props<TTag, ComboboxRenderPropArg<EnsureArray<TValue>>, O>>)
363+
} & Expand<Props<TTag, ComboboxRenderPropArg<EnsureArray<TValue>, TValue>, O>>)
356364
| ({
357365
value?: TValue
358366
nullable?: false
@@ -364,7 +372,7 @@ type ComboboxValueProps<
364372
{ nullable?: TNullable; multiple?: TMultiple }
365373
>
366374

367-
type ComboboxProps<
375+
export type ComboboxProps<
368376
TValue,
369377
TNullable extends boolean | undefined,
370378
TMultiple extends boolean | undefined,
@@ -678,7 +686,6 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
678686
</ComboboxActionsContext.Provider>
679687
)
680688
}
681-
let ComboboxRoot = forwardRefWithAs(ComboboxFn)
682689

683690
// ---
684691

@@ -697,23 +704,27 @@ type InputPropsWeControl =
697704
| 'onChange'
698705
| 'displayValue'
699706

700-
let Input = forwardRefWithAs(function Input<
707+
export type ComboboxInputProps<TTag extends ElementType, TType> = Props<
708+
TTag,
709+
InputRenderPropArg,
710+
InputPropsWeControl
711+
> & {
712+
displayValue?(item: TType): string
713+
onChange?(event: React.ChangeEvent<HTMLInputElement>): void
714+
}
715+
716+
function InputFn<
701717
TTag extends ElementType = typeof DEFAULT_INPUT_TAG,
702718
// TODO: One day we will be able to infer this type from the generic in Combobox itself.
703719
// But today is not that day..
704720
TType = Parameters<typeof ComboboxRoot>[0]['value']
705-
>(
706-
props: Props<TTag, InputRenderPropArg, InputPropsWeControl> & {
707-
displayValue?(item: TType): string
708-
onChange(event: React.ChangeEvent<HTMLInputElement>): void
709-
},
710-
ref: Ref<HTMLInputElement>
711-
) {
721+
>(props: ComboboxInputProps<TTag, TType>, ref: Ref<HTMLInputElement>) {
712722
let internalId = useId()
713723
let {
714724
id = `headlessui-combobox-input-${internalId}`,
715725
onChange,
716726
displayValue,
727+
// @ts-ignore: We know this MAY NOT exist for a given tag but we only care when it _does_ exist.
717728
type = 'text',
718729
...theirProps
719730
} = props
@@ -988,7 +999,7 @@ let Input = forwardRefWithAs(function Input<
988999
defaultTag: DEFAULT_INPUT_TAG,
9891000
name: 'Combobox.Input',
9901001
})
991-
})
1002+
}
9921003

9931004
// ---
9941005

@@ -999,7 +1010,7 @@ interface ButtonRenderPropArg {
9991010
value: any
10001011
}
10011012
type ButtonPropsWeControl =
1002-
| 'type'
1013+
// | 'type' // While we do "control" this prop we allow it to be overridden
10031014
| 'tabIndex'
10041015
| 'aria-haspopup'
10051016
| 'aria-controls'
@@ -1009,8 +1020,14 @@ type ButtonPropsWeControl =
10091020
| 'onClick'
10101021
| 'onKeyDown'
10111022

1012-
let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
1013-
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
1023+
export type ComboboxButtonProps<TTag extends ElementType> = Props<
1024+
TTag,
1025+
ButtonRenderPropArg,
1026+
ButtonPropsWeControl
1027+
>
1028+
1029+
function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
1030+
props: ComboboxButtonProps<TTag>,
10141031
ref: Ref<HTMLButtonElement>
10151032
) {
10161033
let data = useData('Combobox.Button')
@@ -1105,7 +1122,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
11051122
defaultTag: DEFAULT_BUTTON_TAG,
11061123
name: 'Combobox.Button',
11071124
})
1108-
})
1125+
}
11091126

11101127
// ---
11111128

@@ -1116,8 +1133,14 @@ interface LabelRenderPropArg {
11161133
}
11171134
type LabelPropsWeControl = 'ref' | 'onClick'
11181135

1119-
let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
1120-
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>,
1136+
export type ComboboxLabelProps<TTag extends ElementType> = Props<
1137+
TTag,
1138+
LabelRenderPropArg,
1139+
LabelPropsWeControl
1140+
>
1141+
1142+
function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
1143+
props: ComboboxLabelProps<TTag>,
11211144
ref: Ref<HTMLLabelElement>
11221145
) {
11231146
let internalId = useId()
@@ -1144,7 +1167,7 @@ let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DE
11441167
defaultTag: DEFAULT_LABEL_TAG,
11451168
name: 'Combobox.Label',
11461169
})
1147-
})
1170+
}
11481171

11491172
// ---
11501173

@@ -1156,13 +1179,17 @@ type OptionsPropsWeControl = 'aria-labelledby' | 'hold' | 'onKeyDown' | 'role' |
11561179

11571180
let OptionsRenderFeatures = Features.RenderStrategy | Features.Static
11581181

1159-
let Options = forwardRefWithAs(function Options<
1160-
TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG
1161-
>(
1162-
props: Props<TTag, OptionsRenderPropArg, OptionsPropsWeControl> &
1163-
PropsForFeatures<typeof OptionsRenderFeatures> & {
1164-
hold?: boolean
1165-
},
1182+
export type ComboboxOptionsProps<TTag extends ElementType> = Props<
1183+
TTag,
1184+
OptionsRenderPropArg,
1185+
OptionsPropsWeControl
1186+
> &
1187+
PropsForFeatures<typeof OptionsRenderFeatures> & {
1188+
hold?: boolean
1189+
}
1190+
1191+
function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
1192+
props: ComboboxOptionsProps<TTag>,
11661193
ref: Ref<HTMLUListElement>
11671194
) {
11681195
let internalId = useId()
@@ -1226,7 +1253,7 @@ let Options = forwardRefWithAs(function Options<
12261253
visible,
12271254
name: 'Combobox.Options',
12281255
})
1229-
})
1256+
}
12301257

12311258
// ---
12321259

@@ -1238,18 +1265,21 @@ interface OptionRenderPropArg {
12381265
}
12391266
type ComboboxOptionPropsWeControl = 'role' | 'tabIndex' | 'aria-disabled' | 'aria-selected'
12401267

1241-
let Option = forwardRefWithAs(function Option<
1268+
export type ComboboxOptionProps<TTag extends ElementType, TType> = Props<
1269+
TTag,
1270+
OptionRenderPropArg,
1271+
ComboboxOptionPropsWeControl | 'value'
1272+
> & {
1273+
disabled?: boolean
1274+
value: TType
1275+
}
1276+
1277+
function OptionFn<
12421278
TTag extends ElementType = typeof DEFAULT_OPTION_TAG,
12431279
// TODO: One day we will be able to infer this type from the generic in Combobox itself.
12441280
// But today is not that day..
12451281
TType = Parameters<typeof ComboboxRoot>[0]['value']
1246-
>(
1247-
props: Props<TTag, OptionRenderPropArg, ComboboxOptionPropsWeControl | 'value'> & {
1248-
disabled?: boolean
1249-
value: TType
1250-
},
1251-
ref: Ref<HTMLLIElement>
1252-
) {
1282+
>(props: ComboboxOptionProps<TTag, TType>, ref: Ref<HTMLLIElement>) {
12531283
let internalId = useId()
12541284
let {
12551285
id = `headlessui-combobox-option-${internalId}`,
@@ -1296,7 +1326,13 @@ let Option = forwardRefWithAs(function Option<
12961326
internalOptionRef.current?.scrollIntoView?.({ block: 'nearest' })
12971327
})
12981328
return d.dispose
1299-
}, [internalOptionRef, active, data.comboboxState, data.activationTrigger, /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ data.activeOptionIndex])
1329+
}, [
1330+
internalOptionRef,
1331+
active,
1332+
data.comboboxState,
1333+
data.activationTrigger,
1334+
/* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ data.activeOptionIndex,
1335+
])
13001336

13011337
let handleClick = useEvent((event: { preventDefault: Function }) => {
13021338
if (disabled) return event.preventDefault()
@@ -1379,8 +1415,63 @@ let Option = forwardRefWithAs(function Option<
13791415
defaultTag: DEFAULT_OPTION_TAG,
13801416
name: 'Combobox.Option',
13811417
})
1382-
})
1418+
}
13831419

13841420
// ---
13851421

1422+
interface ComponentCombobox extends HasDisplayName {
1423+
<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
1424+
props: ComboboxProps<TValue, true, true, TTag> & RefProp<typeof ComboboxFn>
1425+
): JSX.Element
1426+
<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
1427+
props: ComboboxProps<TValue, true, false, TTag> & RefProp<typeof ComboboxFn>
1428+
): JSX.Element
1429+
<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
1430+
props: ComboboxProps<TValue, false, false, TTag> & RefProp<typeof ComboboxFn>
1431+
): JSX.Element
1432+
<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
1433+
props: ComboboxProps<TValue, false, true, TTag> & RefProp<typeof ComboboxFn>
1434+
): JSX.Element
1435+
}
1436+
1437+
interface ComponentComboboxButton extends HasDisplayName {
1438+
<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
1439+
props: ComboboxButtonProps<TTag> & RefProp<typeof ButtonFn>
1440+
): JSX.Element
1441+
}
1442+
1443+
interface ComponentComboboxInput extends HasDisplayName {
1444+
<TType, TTag extends ElementType = typeof DEFAULT_INPUT_TAG>(
1445+
props: ComboboxInputProps<TTag, TType> & RefProp<typeof InputFn>
1446+
): JSX.Element
1447+
}
1448+
1449+
interface ComponentComboboxLabel extends HasDisplayName {
1450+
<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
1451+
props: ComboboxLabelProps<TTag> & RefProp<typeof LabelFn>
1452+
): JSX.Element
1453+
}
1454+
1455+
interface ComponentComboboxOptions extends HasDisplayName {
1456+
<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
1457+
props: ComboboxOptionsProps<TTag> & RefProp<typeof OptionsFn>
1458+
): JSX.Element
1459+
}
1460+
1461+
interface ComponentComboboxOption extends HasDisplayName {
1462+
<
1463+
TTag extends ElementType = typeof DEFAULT_OPTION_TAG,
1464+
TType = Parameters<typeof ComboboxRoot>[0]['value']
1465+
>(
1466+
props: ComboboxOptionProps<TTag, TType> & RefProp<typeof OptionFn>
1467+
): JSX.Element
1468+
}
1469+
1470+
let ComboboxRoot = forwardRefWithAs(ComboboxFn) as unknown as ComponentCombobox
1471+
let Button = forwardRefWithAs(ButtonFn) as unknown as ComponentComboboxButton
1472+
let Input = forwardRefWithAs(InputFn) as unknown as ComponentComboboxInput
1473+
let Label = forwardRefWithAs(LabelFn) as unknown as ComponentComboboxLabel
1474+
let Options = forwardRefWithAs(OptionsFn) as unknown as ComponentComboboxOptions
1475+
let Option = forwardRefWithAs(OptionFn) as unknown as ComponentComboboxOption
1476+
13861477
export let Combobox = Object.assign(ComboboxRoot, { Input, Button, Label, Options, Option })

0 commit comments

Comments
 (0)