teqo-backup/src/components/campaign/shared/StrictCombobox.tsx
Francisco Solla 4e920ee064 feat(C107): sheet de criação de atualização usável no mobile — altura auto até o topo, form sem labels com divisórias full-bleed, placeholders, polaridade em toggle, checks à direita
- MunicipalityUpdateFields ganha layout 'labeled'|'list'; polaridade vira ToggleGroup Ruim|Neutra|Boa nas duas resoluções (mesmo dado, hidden input)
- layout 'list': labels sr-only, textarea sem borda, divisórias divide-y full-bleed, Urgente/Sinalizar adversário como linha tocável com check à direita
- StrictCombobox: prop placeholder (backward-compatible)
- Sheet do feed: max-h-dvh (teto no topo, rola por dentro), título sr-only, form lista com placeholders descritivos; desktop preservado
- Quick-edit B42 herda o visual no sheet mobile via layout
- unit/e2e atualizados para o toggle (radio)
2026-08-10 01:11:24 -03:00

96 lines
2.7 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '@/components/ui/combobox'
import { cn } from '@/lib/utils'
import { matchesAtWordStart, normalizeSearchPhrase } from '@/lib/wordStartFilter'
import type { StrictComboboxOption } from '@/utilities/territory/territoryComboboxOptions'
type StrictComboboxProps = {
id: string
options: StrictComboboxOption[]
value: string
onValueChange: (value: string) => void
onInvalid?: () => void
error?: string
placeholder?: string
className?: string
}
export const StrictCombobox = ({
id,
options,
value,
onValueChange,
onInvalid,
error,
placeholder,
className,
}: StrictComboboxProps) => {
const selected = options.find((option) => option.value === value) ?? null
const [inputValue, setInputValue] = useState(selected?.label ?? '')
useEffect(() => {
setInputValue(selected?.label ?? '')
}, [selected?.label])
return (
<Combobox<StrictComboboxOption>
items={options}
value={selected}
inputValue={inputValue}
onInputValueChange={setInputValue}
onValueChange={(option) => {
setInputValue(option?.label ?? '')
onValueChange(option?.value ?? '')
}}
filter={(option, query) => matchesAtWordStart(option.label, query)}
isItemEqualToValue={(left, right) => left?.value === right?.value}
>
<ComboboxInput
id={id}
autoComplete="off"
autoCorrect="off"
spellCheck={false}
placeholder={placeholder}
className={cn('min-h-11 rounded-[6px]', className)}
showClear={Boolean(value)}
aria-invalid={Boolean(error)}
aria-describedby={error ? `${id}-error` : undefined}
onBlur={(event) => {
const normalized = normalizeSearchPhrase(event.currentTarget.value)
const exact = options.find((option) => normalizeSearchPhrase(option.label) === normalized)
if (normalized && !exact) {
setInputValue('')
onValueChange('')
onInvalid?.()
} else if (exact) {
setInputValue(exact.label)
onValueChange(exact.value)
} else {
setInputValue('')
onValueChange('')
}
}}
/>
<ComboboxContent>
<ComboboxEmpty>Nenhuma opção encontrada.</ComboboxEmpty>
<ComboboxList>
{(option: StrictComboboxOption) => (
<ComboboxItem key={option.value} value={option}>
{option.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}