Skip to main content

createBaseSelectSearchInput

Bind a custom text input to BaseSelect with accessible keyboard and IME behavior.

View as Markdown

createBaseSelectSearchInput connects a caller-owned <input> to a BaseSelect. Call BaseSelect.useContext() inside a BaseSelect child component and pass that state to the hook. It supplies input attributes, keyboard handling, focus registration, and IME-safe query commits.

Import#

import { createBaseSelectSearchInput } from 'moraine/utils'

Options and result#

interface BaseSelectSearchInputOptions {
  state: Pick<
    BaseSelectT.Context,
    | 'id'
    | 'listboxId'
    | 'open'
    | 'setOpen'
    | 'highlightedValue'
    | 'itemId'
    | 'locked'
    | 'disabled'
    | 'readOnly'
    | 'focusOwner'
    | 'setFocusOwner'
    | 'registerCompositionDiscarder'
    | 'keyDown'
    | 'focus'
    | 'blur'
  >
  searchValue: Accessor<string>
  setSearchValue: (value: string) => string
  enabled?: Accessor<boolean>
  displayValue?: Accessor<string>
  transformInput?: (value: string) => string
  maxLength?: number
  autocomplete?: JSX.InputHTMLAttributes<HTMLInputElement>['autocomplete']
}

const { inputProps, isComposing, discardComposition } = createBaseSelectSearchInput(options)

Pass a Solid signal as searchValue and its setter as setSearchValue, or provide equivalent functions. setSearchValue must return the text it applies. Spread inputProps directly on an <input>. The other result methods let a custom control defer its own keyboard work during composition or discard a draft before clearing the selection.

enabled defaults to true. When false, the input is read-only and BaseSelect handles keys as a non-editable control. displayValue defaults to searchValue; a Combobox can supply the selected label while closed. transformInput runs before a completed input is committed. maxLength sets the native input attribute; clamp the value in your setter too when programmatic updates must obey the same limit. autocomplete defaults to off.

Example#

src/CustomSearchSelect.tsx
import { createSignal, For } from 'solid-js'
import { BaseSelect } from 'moraine'
import { createBaseSelectSearchInput } from 'moraine/utils'
import type { BaseSelectSearchInputOptions } from 'moraine/utils'

const fruits = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
]

function SearchControl(
  props: Pick<BaseSelectSearchInputOptions, 'searchValue' | 'setSearchValue'>,
) {
  const state = BaseSelect.useContext()
  const { inputProps } = createBaseSelectSearchInput({
    state,
    searchValue: () => props.searchValue(),
    setSearchValue: (value) => props.setSearchValue(value),
  })
  return (
    <BaseSelect.Control>
      <input {...inputProps} placeholder="Search fruits" />
    </BaseSelect.Control>
  )
}

export function CustomSearchSelect() {
  const [searchValue, setSearchValue] = createSignal('')
  const matches = () =>
    fruits.filter((fruit) => fruit.label.toLowerCase().includes(searchValue().toLowerCase()))

  return (
    <BaseSelect items={matches()}>
      <SearchControl searchValue={searchValue} setSearchValue={setSearchValue} />
      <BaseSelect.Content>
        <BaseSelect.Listbox>
          <For each={matches()}>
            {(fruit) => <BaseSelect.Item item={fruit}>{fruit.label}</BaseSelect.Item>}
          </For>
        </BaseSelect.Listbox>
      </BaseSelect.Content>
    </BaseSelect>
  )
}