Skip to content

Make React types more compatible with other libraries #2282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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..
Comment on lines 1279 to 1280
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random off-topic side note: I think one way we can solve this is by introduce a different API using some factory like functions. Example:

let Combobox = createCombobox<TypeYouWant>()

Which could create the Combobox, Combobox.Input, Combobox.Option, ... components already scoped to the TypeYouWant 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah that's pretty good idea 🤔

Would be a good thing to explore for future improvements

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