Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active April 6, 2026 15:25
Show Gist options
  • Select an option

  • Save ryanflorence/646f9ad1aa86344a170643800ce5767f to your computer and use it in GitHub Desktop.

Select an option

Save ryanflorence/646f9ad1aa86344a170643800ce5767f to your computer and use it in GitHub Desktop.
select.tsx
import { on, ref, type Handle, type Props } from 'remix/component'
import { Glyph } from '../glyph/glyph.tsx'
import { popover } from '../popover/popover.ts'
import { listbox, type ListboxValue, type ListboxOption } from '../listbox/listbox.ts'
import { ui } from '../theme/theme.ts'
import { onKeyDown } from '../keydown/keydown.ts'
import { waitForCssTransition } from '../utils/wait-for-css-transition.ts'
import { wait } from '../utils/wait.ts'
interface SelectProps extends Props<'button'> {
initialLabel: string
defaultValue: string
}
enum State {
Initializing = 'initializing',
Closed = 'closed',
Open = 'open',
Selecting = 'selecting',
}
export function Select(handle: Handle) {
let state: State = State.Initializing
let value: ListboxValue = null
let activeValue: ListboxValue = null
let selectedId: string | null = null
let defaultLabel = ''
let label = ''
let buttonRef: HTMLElement
let surfaceRef: HTMLElement
function openPopover() {
if (state !== State.Closed) return
state = State.Open
activeValue = value
handle.update()
}
function syncPopoverMinWidth() {
if (state !== State.Open) return
surfaceRef.style.minWidth = `${buttonRef.offsetWidth}px`
}
function closePopover() {
if (state !== State.Open) return
state = State.Closed
handle.update()
}
async function selectOption(nextValue: ListboxValue, option: ListboxOption | undefined) {
if (state !== State.Open) return
state = State.Selecting
value = nextValue
activeValue = nextValue
selectedId = option ? option.id : null
// wait for the popover transition
await Promise.all([handle.update(), waitForCssTransition(surfaceRef, handle.signal)])
// delay the label update
await wait(75)
if (handle.signal.aborted) return
state = State.Closed
label = option ? option.label : defaultLabel
handle.update()
}
function highlightOption(nextActiveValue: ListboxValue) {
if (state !== State.Open) return
activeValue = nextActiveValue
handle.update()
}
return (props: SelectProps) => {
let { initialLabel, defaultValue, mix, ...buttonProps } = props
defaultLabel = initialLabel
if (state === State.Initializing) {
label = defaultLabel
value = defaultValue ?? null
activeValue = value
state = State.Closed
}
return (
<popover.context>
<button
{...buttonProps}
mix={[
ui.button.select,
ref((node) => (buttonRef = node)),
popover.focusOnHide(),
popover.anchor({
placement: 'left',
inset: true,
relativeTo: selectedId ? `#${selectedId}` : '[role="option"]',
}),
on('pointerdown', openPopover),
on('click', openPopover), // AT activation
onKeyDown('ArrowDown', openPopover),
onKeyDown('ArrowUp', openPopover),
mix,
]}
>
<span mix={ui.button.label}>{label}</span>
<Glyph mix={ui.button.icon} name="chevronDown" />
</button>
<div
mix={[
ui.popover.surface,
ref((node) => (surfaceRef = node)),
popover.surface({
open: state === State.Open,
hasHideTransition: true,
onHide: closePopover,
}),
on('beforetoggle', syncPopoverMinWidth),
]}
>
<listbox.context
flashSelection={true}
value={value}
activeValue={activeValue}
onSelect={selectOption}
onHighlight={highlightOption}
>
<div mix={[ui.listbox.surface, popover.focusOnShow(), listbox.list()]}>
{props.children}
</div>
</listbox.context>
</div>
</popover.context>
)
}
}
type OptionProps = Props<'div'> & Omit<ListboxOption, 'id'>
export function Option() {
return (props: OptionProps) => {
let { label, value, disabled, textValue, children, mix, ...divProps } = props
return (
<div
{...divProps}
mix={[ui.listbox.option, listbox.option({ value, label, disabled, textValue }), mix]}
>
<Glyph mix={ui.listbox.glyph} name="check" />
<span mix={ui.listbox.label}>{children ?? props.label}</span>
</div>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment