mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 02:23:07 +00:00
- 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)
96 lines
2.7 KiB
TypeScript
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>
|
|
)
|
|
}
|