reactjstypescriptcomboboxheadless-ui

How do I extend Headless UI's Combobox component using Typescipt?


HeadlessUI has an awesome Combobox component. Check out this example that uses the active (highlighted) option. I've successfully converted this example to typescript:

Headless UI Combobox with activeOption

import * as Headless from '@headlessui/react'
import { useState } from 'react'

interface Person {
  id: number
  name: string
}

const people: Person[] = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState<Person | null>(people[0])
  const [query, setQuery] = useState<string>('')

  const filteredPeople: Person[] =
    query === ''
      ? people
      : people.filter((person) => {
          return person.name.toLowerCase().includes(query.toLowerCase())
        })

  return (
    <Headless.Combobox
      value={selectedPerson}
      onChange={setSelectedPerson}
      onClose={() => setQuery('')}
    >
      {({ activeOption }) => (
        <>
          <Headless.ComboboxInput
            aria-label="Assignee"
            displayValue={(person: Person | null) => person?.name ?? ''}
            onChange={(event) => setQuery(event.target.value)}
          />
          <Headless.ComboboxOptions
            anchor="bottom"
            className="border empty:invisible"
          >
            {filteredPeople.map((person) => (
              <Headless.ComboboxOption
                key={person.id}
                value={person}
                className="data-[focus]:bg-blue-100"
              >
                {person.name}
              </Headless.ComboboxOption>
            ))}
          </Headless.ComboboxOptions>
          {activeOption && <div>The currently focused user is: {activeOption.name}</div>}
        </>
      )}
    </Headless.Combobox>
  )
}

My custom SuperBox component

Now, my goal is to create my own "SuperBox" component that is basically just Combobox with a few additional props. Here's my attempt...

import * as Headless from '@headlessui/react'
import { ComponentProps, ReactNode } from 'react'

interface SuperBoxProps extends ComponentProps<typeof Headless.Combobox> {
  className?: string
  children?: ReactNode
}

export function SuperBox({ className, children, ...props }: SuperBoxProps) {
  return (
    <Headless.Combobox
      data-slot="control"
      className={className}
      {...props}
    >
      <div className="relative">{children}</div>
    </Headless.Combobox>
  )
}

The problem

Unfortunately, my when I replace Headless.Combobox with SuperBox in the example above, I get the following typescript error:

Type '({ activeOption }: { activeOption: any; }) => Element' is not assignable to type 'ReactNode'

I understand that children in this case is a function, not a ReactNode, but I can't figure out how to adjust my type definitions to support this.


Solution

  • i think this is what you have to do.

    import * as Headless from '@headlessui/react';
    import {
      ComponentProps,
      JSXElementConstructor,
      ReactElement,
      ReactNode,
    } from 'react';
    
    
    type ComboboxRenderPropArg = {
      activeOption: unknown; // Update with the correct type if you know it
      open: boolean;
    };
    
    interface SuperBoxProps extends ComponentProps<typeof Headless.Combobox> {
      className?: string;
      children?:
        | ReactNode
        | ((
            bag: ComboboxRenderPropArg
          ) => ReactElement<any, string | JSXElementConstructor<any>>);
    }
    
    export function SuperBox({ className, children, ...props }: SuperBoxProps) {
      return (
        <Headless.Combobox data-slot="control" className={className} {...props}>
          <div className="relative">
            {typeof children === 'function'
              ? children({ activeOption: null, open: false })
              : children}
          </div>
        </Headless.Combobox>
      );
    }
    

    and then, you are good to go.