mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
refactor(planner): share the suggestion dropdown between the airport, location and address fields
AirportSelect, LocationSelect and AddressInput each carried their own copy of the open state, the keyboard highlight, the outside click that closes the list, the floating list with its loading line and the suggestion row. useSuggestionDropdown now holds the open state, the highlight, the outside click and the arrow, Enter and Escape handling. SuggestionDropdown.tsx holds the boxed input with its icon and clear button (airport and location), the floating list (all three), the row button and the place row content (location and address). What still differs between the fields stays with them or is a parameter: the icon, the placeholder, what typing does (LocationSelect also resets the highlight), the rows the keys walk (LocationSelect walks the trip's places below three characters), whether the loading line may show (not while LocationSelect offers the trip's places) and how a row lines up its content (centred for airports). Each field keeps its own query, search and pick.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { MapPin } from 'lucide-react'
|
||||
import { mapsApi } from '../../api/client'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useLocationBias } from '../../hooks/useLocationBias'
|
||||
import { usePlaceLanguage } from '../../hooks/usePlaceLanguage'
|
||||
import { useSuggestionDropdown } from './useSuggestionDropdown'
|
||||
import { PlaceSuggestion, SuggestionList, SuggestionRow } from './SuggestionDropdown'
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
@@ -17,28 +18,18 @@ interface Props {
|
||||
// authoritative: every keystroke reaches the parent so a hand-written address
|
||||
// is never lost, and picking a suggestion just replaces the text (#1496).
|
||||
export default function AddressInput({ value, onChange, placeholder, className }: Props) {
|
||||
const { t, locale } = useTranslation()
|
||||
const { locale } = useTranslation()
|
||||
const placeLang = usePlaceLanguage()
|
||||
// Ohne Reisekontext ist der Hinweis leer, und die Suche laeuft wie bisher.
|
||||
const { point: locationBias } = useLocationBias()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { open, setOpen, highlight, setHighlight, wrapRef, handleKey } = useSuggestionDropdown()
|
||||
const [results, setResults] = useState<any[]>([])
|
||||
const [highlight, setHighlight] = useState(-1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const wrapRef = useRef<HTMLDivElement>(null)
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
// Clearing the timer does nothing to a request that is already out, and
|
||||
// mapsApi.search takes no signal, so results are matched by ticket instead.
|
||||
const reqIdRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (!wrapRef.current?.contains(e.target as Node)) setOpen(false)
|
||||
}
|
||||
if (open) document.addEventListener('mousedown', handler)
|
||||
return () => document.removeEventListener('mousedown', handler)
|
||||
}, [open])
|
||||
|
||||
useEffect(() => () => { if (debounceRef.current) clearTimeout(debounceRef.current) }, [])
|
||||
|
||||
// Search on typing only (not on focus or external value changes), so opening
|
||||
@@ -74,14 +65,6 @@ export default function AddressInput({ value, onChange, placeholder, className }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const onKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (!open || results.length === 0) return
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setHighlight(h => Math.min(h + 1, results.length - 1)) }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight(h => Math.max(h - 1, 0)) }
|
||||
else if (e.key === 'Enter' && highlight >= 0) { e.preventDefault(); pick(results[highlight]) }
|
||||
else if (e.key === 'Escape') setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={wrapRef} style={{ position: 'relative' }}>
|
||||
<input
|
||||
@@ -92,37 +75,23 @@ export default function AddressInput({ value, onChange, placeholder, className }
|
||||
// Opens its list on focus, so a dialog must not focus it by itself (#1302).
|
||||
data-no-autofocus
|
||||
onFocus={() => setOpen(true)}
|
||||
onKeyDown={onKey}
|
||||
onKeyDown={(e) => handleKey(e, results, pick)}
|
||||
className={className}
|
||||
/>
|
||||
{open && (loading || results.length > 0) && (
|
||||
<div className="bg-surface-card" style={{ position: 'absolute', top: 'calc(100% + 4px)', insetInline: 0, border: '1px solid var(--border-primary)', borderRadius: 10, boxShadow: '0 8px 24px rgba(0,0,0,0.18)', maxHeight: 260, overflowY: 'auto', zIndex: 1000 }}>
|
||||
{loading && results.length === 0 && (
|
||||
<div className="text-content-faint" style={{ padding: 10, fontSize: 'calc(12px * var(--fs-scale-body, 1))' }}>{t('common.loading')}</div>
|
||||
)}
|
||||
{open && (
|
||||
<SuggestionList loading={loading} rowCount={results.length}>
|
||||
{results.map((r, i) => (
|
||||
<button
|
||||
<SuggestionRow
|
||||
key={`${r.osm_id || r.google_place_id || i}`}
|
||||
type="button"
|
||||
onClick={() => pick(r)}
|
||||
onMouseEnter={() => setHighlight(i)}
|
||||
className={`text-content ${i === highlight ? 'bg-surface-hover' : 'bg-transparent'}`}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 8, width: '100%',
|
||||
padding: '8px 12px', border: 'none', cursor: 'pointer', textAlign: 'start',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
active={i === highlight}
|
||||
onPick={() => pick(r)}
|
||||
onHover={() => setHighlight(i)}
|
||||
align="flex-start"
|
||||
>
|
||||
<MapPin size={12} className="text-content-faint" style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.name || r.address}</div>
|
||||
{r.address && r.name && r.name !== r.address && (
|
||||
<div className="text-content-faint" style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.address}</div>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<PlaceSuggestion name={r.name} address={r.address} />
|
||||
</SuggestionRow>
|
||||
))}
|
||||
</div>
|
||||
</SuggestionList>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user