Skip to content

Commit

Permalink
Make React types more compatible with other libraries (#2282)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
thecrypticace authored Feb 20, 2023
1 parent c7f6bc6 commit b8c214e
Show file tree
Hide file tree
Showing 26 changed files with 1,226 additions and 560 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@swc/jest": "^0.2.17",
"@testing-library/jest-dom": "^5.16.4",
"@types/node": "^14.14.22",
"esbuild": "^0.14.11",
"esbuild": "^0.17.8",
"fast-glob": "^3.2.11",
"husky": "^4.3.8",
"jest": "26",
Expand All @@ -51,6 +51,6 @@
"prettier-plugin-tailwindcss": "^0.1.4",
"rimraf": "^3.0.2",
"tslib": "^2.3.1",
"typescript": "^4.5.4"
"typescript": "^4.9.5"
}
}
5 changes: 5 additions & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add explicit props types for every component ([#2282](https://github.com/tailwindlabs/headlessui/pull/2282))

### Fixed

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

## [1.7.11] - 2023-02-15

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,10 @@ describe('Rendering', () => {

render(
<Combobox name="assignee" by="id">
<Combobox.Input displayValue={(value: { name: string }) => value.name} />
<Combobox.Input
displayValue={(value: { name: string }) => value.name}
onChange={NOOP}
/>
<Combobox.Options>
{data.map((person) => (
<Combobox.Option key={person.id} value={person}>
Expand Down
175 changes: 133 additions & 42 deletions packages/@headlessui-react/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ import { useTreeWalker } from '../../hooks/use-tree-walker'

import { calculateActiveIndex, Focus } from '../../utils/calculate-active-index'
import { disposables } from '../../utils/disposables'
import { forwardRefWithAs, render, compact, PropsForFeatures, Features } from '../../utils/render'
import {
forwardRefWithAs,
render,
compact,
PropsForFeatures,
Features,
HasDisplayName,
RefProp,
} from '../../utils/render'
import { isDisabledReactIssue7711 } from '../../utils/bugs'
import { match } from '../../utils/match'
import { objectToFormEntries } from '../../utils/form'
Expand Down Expand Up @@ -313,12 +321,12 @@ function stateReducer<T>(state: StateDefinition<T>, action: Actions<T>) {
// ---

let DEFAULT_COMBOBOX_TAG = Fragment
interface ComboboxRenderPropArg<T> {
interface ComboboxRenderPropArg<TValue, TActive = TValue> {
open: boolean
disabled: boolean
activeIndex: number | null
activeOption: T | null
value: T
activeOption: TActive | null
value: TValue
}

type O = 'value' | 'defaultValue' | 'nullable' | 'multiple' | 'onChange' | 'by'
Expand All @@ -336,7 +344,7 @@ type ComboboxValueProps<
multiple: true
onChange?(value: EnsureArray<TValue>): void
by?: ByComparator<TValue>
} & Props<TTag, ComboboxRenderPropArg<EnsureArray<TValue>>, O>)
} & Props<TTag, ComboboxRenderPropArg<EnsureArray<TValue>, TValue>, O>)
| ({
value?: TValue | null
defaultValue?: TValue | null
Expand All @@ -352,7 +360,7 @@ type ComboboxValueProps<
multiple: true
onChange?(value: EnsureArray<TValue>): void
by?: ByComparator<TValue extends Array<infer U> ? U : TValue>
} & Expand<Props<TTag, ComboboxRenderPropArg<EnsureArray<TValue>>, O>>)
} & Expand<Props<TTag, ComboboxRenderPropArg<EnsureArray<TValue>, TValue>, O>>)
| ({
value?: TValue
nullable?: false
Expand All @@ -364,7 +372,7 @@ type ComboboxValueProps<
{ nullable?: TNullable; multiple?: TMultiple }
>

type ComboboxProps<
export type ComboboxProps<
TValue,
TNullable extends boolean | undefined,
TMultiple extends boolean | undefined,
Expand Down Expand Up @@ -678,7 +686,6 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
</ComboboxActionsContext.Provider>
)
}
let ComboboxRoot = forwardRefWithAs(ComboboxFn)

// ---

Expand All @@ -697,23 +704,27 @@ type InputPropsWeControl =
| 'onChange'
| 'displayValue'

let Input = forwardRefWithAs(function Input<
export type ComboboxInputProps<TTag extends ElementType, TType> = Props<
TTag,
InputRenderPropArg,
InputPropsWeControl
> & {
displayValue?(item: TType): string
onChange?(event: React.ChangeEvent<HTMLInputElement>): void
}

function InputFn<
TTag extends ElementType = typeof DEFAULT_INPUT_TAG,
// TODO: One day we will be able to infer this type from the generic in Combobox itself.
// But today is not that day..
TType = Parameters<typeof ComboboxRoot>[0]['value']
>(
props: Props<TTag, InputRenderPropArg, InputPropsWeControl> & {
displayValue?(item: TType): string
onChange(event: React.ChangeEvent<HTMLInputElement>): void
},
ref: Ref<HTMLInputElement>
) {
>(props: ComboboxInputProps<TTag, TType>, ref: Ref<HTMLInputElement>) {
let internalId = useId()
let {
id = `headlessui-combobox-input-${internalId}`,
onChange,
displayValue,
// @ts-ignore: We know this MAY NOT exist for a given tag but we only care when it _does_ exist.
type = 'text',
...theirProps
} = props
Expand Down Expand Up @@ -988,7 +999,7 @@ let Input = forwardRefWithAs(function Input<
defaultTag: DEFAULT_INPUT_TAG,
name: 'Combobox.Input',
})
})
}

// ---

Expand All @@ -999,7 +1010,7 @@ interface ButtonRenderPropArg {
value: any
}
type ButtonPropsWeControl =
| 'type'
// | 'type' // While we do "control" this prop we allow it to be overridden
| 'tabIndex'
| 'aria-haspopup'
| 'aria-controls'
Expand All @@ -1009,8 +1020,14 @@ type ButtonPropsWeControl =
| 'onClick'
| 'onKeyDown'

let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
props: Props<TTag, ButtonRenderPropArg, ButtonPropsWeControl>,
export type ComboboxButtonProps<TTag extends ElementType> = Props<
TTag,
ButtonRenderPropArg,
ButtonPropsWeControl
>

function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
props: ComboboxButtonProps<TTag>,
ref: Ref<HTMLButtonElement>
) {
let data = useData('Combobox.Button')
Expand Down Expand Up @@ -1105,7 +1122,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
defaultTag: DEFAULT_BUTTON_TAG,
name: 'Combobox.Button',
})
})
}

// ---

Expand All @@ -1116,8 +1133,14 @@ interface LabelRenderPropArg {
}
type LabelPropsWeControl = 'ref' | 'onClick'

let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>,
export type ComboboxLabelProps<TTag extends ElementType> = Props<
TTag,
LabelRenderPropArg,
LabelPropsWeControl
>

function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: ComboboxLabelProps<TTag>,
ref: Ref<HTMLLabelElement>
) {
let internalId = useId()
Expand All @@ -1144,7 +1167,7 @@ let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DE
defaultTag: DEFAULT_LABEL_TAG,
name: 'Combobox.Label',
})
})
}

// ---

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

let OptionsRenderFeatures = Features.RenderStrategy | Features.Static

let Options = forwardRefWithAs(function Options<
TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG
>(
props: Props<TTag, OptionsRenderPropArg, OptionsPropsWeControl> &
PropsForFeatures<typeof OptionsRenderFeatures> & {
hold?: boolean
},
export type ComboboxOptionsProps<TTag extends ElementType> = Props<
TTag,
OptionsRenderPropArg,
OptionsPropsWeControl
> &
PropsForFeatures<typeof OptionsRenderFeatures> & {
hold?: boolean
}

function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
props: ComboboxOptionsProps<TTag>,
ref: Ref<HTMLUListElement>
) {
let internalId = useId()
Expand Down Expand Up @@ -1226,7 +1253,7 @@ let Options = forwardRefWithAs(function Options<
visible,
name: 'Combobox.Options',
})
})
}

// ---

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

let Option = forwardRefWithAs(function Option<
export type ComboboxOptionProps<TTag extends ElementType, TType> = Props<
TTag,
OptionRenderPropArg,
ComboboxOptionPropsWeControl | 'value'
> & {
disabled?: boolean
value: TType
}

function OptionFn<
TTag extends ElementType = typeof DEFAULT_OPTION_TAG,
// TODO: One day we will be able to infer this type from the generic in Combobox itself.
// But today is not that day..
TType = Parameters<typeof ComboboxRoot>[0]['value']
>(
props: Props<TTag, OptionRenderPropArg, ComboboxOptionPropsWeControl | 'value'> & {
disabled?: boolean
value: TType
},
ref: Ref<HTMLLIElement>
) {
>(props: ComboboxOptionProps<TTag, TType>, ref: Ref<HTMLLIElement>) {
let internalId = useId()
let {
id = `headlessui-combobox-option-${internalId}`,
Expand Down Expand Up @@ -1296,7 +1326,13 @@ let Option = forwardRefWithAs(function Option<
internalOptionRef.current?.scrollIntoView?.({ block: 'nearest' })
})
return d.dispose
}, [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])
}, [
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,
])

let handleClick = useEvent((event: { preventDefault: Function }) => {
if (disabled) return event.preventDefault()
Expand Down Expand Up @@ -1379,8 +1415,63 @@ let Option = forwardRefWithAs(function Option<
defaultTag: DEFAULT_OPTION_TAG,
name: 'Combobox.Option',
})
})
}

// ---

interface ComponentCombobox extends HasDisplayName {
<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
props: ComboboxProps<TValue, true, true, TTag> & RefProp<typeof ComboboxFn>
): JSX.Element
<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
props: ComboboxProps<TValue, true, false, TTag> & RefProp<typeof ComboboxFn>
): JSX.Element
<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
props: ComboboxProps<TValue, false, false, TTag> & RefProp<typeof ComboboxFn>
): JSX.Element
<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_TAG>(
props: ComboboxProps<TValue, false, true, TTag> & RefProp<typeof ComboboxFn>
): JSX.Element
}

interface ComponentComboboxButton extends HasDisplayName {
<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
props: ComboboxButtonProps<TTag> & RefProp<typeof ButtonFn>
): JSX.Element
}

interface ComponentComboboxInput extends HasDisplayName {
<TType, TTag extends ElementType = typeof DEFAULT_INPUT_TAG>(
props: ComboboxInputProps<TTag, TType> & RefProp<typeof InputFn>
): JSX.Element
}

interface ComponentComboboxLabel extends HasDisplayName {
<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
props: ComboboxLabelProps<TTag> & RefProp<typeof LabelFn>
): JSX.Element
}

interface ComponentComboboxOptions extends HasDisplayName {
<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
props: ComboboxOptionsProps<TTag> & RefProp<typeof OptionsFn>
): JSX.Element
}

interface ComponentComboboxOption extends HasDisplayName {
<
TTag extends ElementType = typeof DEFAULT_OPTION_TAG,
TType = Parameters<typeof ComboboxRoot>[0]['value']
>(
props: ComboboxOptionProps<TTag, TType> & RefProp<typeof OptionFn>
): JSX.Element
}

let ComboboxRoot = forwardRefWithAs(ComboboxFn) as unknown as ComponentCombobox
let Button = forwardRefWithAs(ButtonFn) as unknown as ComponentComboboxButton
let Input = forwardRefWithAs(InputFn) as unknown as ComponentComboboxInput
let Label = forwardRefWithAs(LabelFn) as unknown as ComponentComboboxLabel
let Options = forwardRefWithAs(OptionsFn) as unknown as ComponentComboboxOptions
let Option = forwardRefWithAs(OptionFn) as unknown as ComponentComboboxOption

export let Combobox = Object.assign(ComboboxRoot, { Input, Button, Label, Options, Option })
Loading

0 comments on commit b8c214e

Please sign in to comment.