Tofu/app/components/molecules/DishNameField.js
2026-09-18 01:41:02 +02:00

82 lines
2.4 KiB
JavaScript

import { useState } from 'preact/hooks'
import { html } from 'htm/preact'
import { TextField } from '#tofu/components/atoms/TextField.js'
import { SuggestionList } from '#tofu/components/atoms/SuggestionList.js'
// Combobox over the dish name. Suggestions arrive as a prop: this molecule never
// reads a store, a service or an index. It owns UI state only (open, activeIndex).
// Nothing is ever blocked: an empty suggestion list simply means free text.
export function DishNameField({ id, label, value, suggestions = [], onInput, onSelect }) {
const [open, setOpen] = useState(false)
const [activeIndex, setActiveIndex] = useState(-1)
const listId = `${id}-suggestions`
const expanded = open && suggestions.length > 0
const activeId = expanded && activeIndex >= 0 ? `${listId}-${activeIndex}` : null
function close() {
setOpen(false)
setActiveIndex(-1)
}
function handleInput(nextValue) {
setOpen(true)
setActiveIndex(-1)
onInput(nextValue)
}
function selectSuggestion(suggestion) {
close()
onSelect(suggestion)
}
function handleKeyDown(event) {
if (event.key === 'Escape') {
close()
return
}
if (event.key === 'Enter') {
// Only intercept Enter when an item is highlighted; otherwise the form submits.
if (!expanded || activeIndex < 0) return
event.preventDefault()
selectSuggestion(suggestions[activeIndex])
return
}
if (suggestions.length === 0) return
if (event.key === 'ArrowDown') {
event.preventDefault()
setOpen(true)
setActiveIndex(expanded ? (activeIndex + 1) % suggestions.length : 0)
return
}
if (event.key === 'ArrowUp') {
event.preventDefault()
setOpen(true)
setActiveIndex(expanded && activeIndex > 0 ? activeIndex - 1 : suggestions.length - 1)
}
}
return html`
<${TextField}
id=${id}
label=${label}
value=${value}
onInput=${handleInput}
autocomplete="off"
role="combobox"
aria-expanded=${expanded ? 'true' : 'false'}
aria-controls=${listId}
aria-autocomplete="list"
aria-activedescendant=${activeId}
onKeyDown=${handleKeyDown}
onBlur=${() => close()}
>
<${SuggestionList}
id=${listId}
items=${expanded ? suggestions : []}
activeIndex=${activeIndex}
onSelect=${selectSuggestion}
/>
<//>
`
}