mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
feat(planner): a floating variant of the location search in the map controls' frosted glass
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import { MapPin } from 'lucide-react'
|
||||
import { MapPin, Search } from 'lucide-react'
|
||||
import { mapsApi } from '../../api/client'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useLocationBias } from '../../hooks/useLocationBias'
|
||||
@@ -21,6 +21,8 @@ interface Props {
|
||||
ariaLabel?: string
|
||||
/** Announces the field as a combobox and says "nothing found" or an error in its list. */
|
||||
showSearchStatus?: boolean
|
||||
/** Floats over a map as a frosted pill, like the map controls. */
|
||||
floating?: boolean
|
||||
style?: React.CSSProperties
|
||||
/**
|
||||
* The trip's own places, offered while the field is empty or holds fewer than three
|
||||
@@ -30,7 +32,7 @@ interface Props {
|
||||
places?: LocationPoint[]
|
||||
}
|
||||
|
||||
export default function LocationSelect({ value, onChange, placeholder, ariaLabel, showSearchStatus = false, style, places }: Props) {
|
||||
export default function LocationSelect({ value, onChange, placeholder, ariaLabel, showSearchStatus = false, floating = false, style, places }: Props) {
|
||||
const { t, locale } = useTranslation()
|
||||
const placeLang = usePlaceLanguage()
|
||||
// Ohne Reisekontext ist der Hinweis leer, und die Suche laeuft wie bisher.
|
||||
@@ -137,7 +139,10 @@ export default function LocationSelect({ value, onChange, placeholder, ariaLabel
|
||||
return (
|
||||
<div ref={wrapRef} style={{ position: 'relative', ...style }}>
|
||||
<SuggestionInputBox
|
||||
icon={<MapPin size={14} className="text-content-faint" style={{ flexShrink: 0 }} />}
|
||||
icon={floating
|
||||
? <Search size={16} strokeWidth={2} className="text-content-muted" style={{ flexShrink: 0 }} />
|
||||
: <MapPin size={14} className="text-content-faint" style={{ flexShrink: 0 }} />}
|
||||
floating={floating}
|
||||
query={query}
|
||||
placeholder={placeholder ?? t('reservations.searchLocation')}
|
||||
onType={(text) => { setQuery(text); setOpen(true); setHighlight(-1); setSearchError(false); setSearchedQuery(null); if (value) onChange(null) }}
|
||||
@@ -153,6 +158,7 @@ export default function LocationSelect({ value, onChange, placeholder, ariaLabel
|
||||
rowCount={rows.length}
|
||||
status={statusLine}
|
||||
listbox={showSearchStatus ? { id: listId, label: accessibleLabel } : undefined}
|
||||
floating={floating}
|
||||
>
|
||||
{rows.map((r, i) => (
|
||||
<SuggestionRow
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { MapPin, X } from 'lucide-react';
|
||||
import type React from 'react';
|
||||
import { useTranslation } from '../../i18n';
|
||||
import { MAP_CONTROL_SHADOW } from '../Map/mapControlShadow';
|
||||
|
||||
// Over a map the field and its list take the map controls' frosted glass instead of a form box.
|
||||
const FLOATING_GLASS: React.CSSProperties = {
|
||||
background: 'var(--sidebar-bg)',
|
||||
backdropFilter: 'blur(20px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
||||
boxShadow: MAP_CONTROL_SHADOW,
|
||||
border: 'none',
|
||||
};
|
||||
|
||||
// The markup the planner's search fields share: the boxed input with its icon and
|
||||
// clear button (AirportSelect, LocationSelect), the floating list (all three) and
|
||||
@@ -15,6 +25,8 @@ interface SuggestionInputBoxProps {
|
||||
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
/** Shown as the clear button while a value is picked. */
|
||||
onClear?: () => void;
|
||||
/** Floats over a map: a frosted pill like the map controls. */
|
||||
floating?: boolean;
|
||||
/** Combobox wiring for a field that announces its list (role, aria-expanded, ...). */
|
||||
inputAria?: React.AriaAttributes & { role?: React.AriaRole };
|
||||
}
|
||||
@@ -28,18 +40,16 @@ export function SuggestionInputBox({
|
||||
onKeyDown,
|
||||
onClear,
|
||||
inputAria,
|
||||
floating = false,
|
||||
}: SuggestionInputBoxProps) {
|
||||
return (
|
||||
<div
|
||||
className="bg-surface-tertiary"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '8px 10px',
|
||||
borderRadius: 10,
|
||||
border: '1px solid var(--border-primary)',
|
||||
}}
|
||||
className={floating ? 'focus-within:ring-2 focus-within:ring-accent' : 'bg-surface-tertiary'}
|
||||
style={
|
||||
floating
|
||||
? { ...FLOATING_GLASS, display: 'flex', alignItems: 'center', gap: 8, height: 42, padding: '0 8px 0 14px', borderRadius: 999 }
|
||||
: { display: 'flex', alignItems: 'center', gap: 6, padding: '8px 10px', borderRadius: 10, border: '1px solid var(--border-primary)' }
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
<input
|
||||
@@ -84,19 +94,21 @@ interface SuggestionListProps {
|
||||
status?: React.ReactNode;
|
||||
/** Wraps the rows in a listbox when the field is a combobox. */
|
||||
listbox?: { id: string; label: string };
|
||||
/** Floats over a map: the same frosted glass as the field above it. */
|
||||
floating?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** The floating list under the field. Rendered only while it is loading, has rows or a status; the caller checks `open`. */
|
||||
export function SuggestionList({ loading, rowCount, status, listbox, children }: SuggestionListProps) {
|
||||
export function SuggestionList({ loading, rowCount, status, listbox, floating = false, children }: SuggestionListProps) {
|
||||
const { t } = useTranslation();
|
||||
if (!loading && rowCount === 0 && !status) return null;
|
||||
return (
|
||||
<div
|
||||
className="bg-surface-card"
|
||||
className={floating ? undefined : 'bg-surface-card'}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 'calc(100% + 4px)',
|
||||
top: floating ? 'calc(100% + 6px)' : 'calc(100% + 4px)',
|
||||
insetInline: 0,
|
||||
border: '1px solid var(--border-primary)',
|
||||
borderRadius: 10,
|
||||
@@ -104,6 +116,7 @@ export function SuggestionList({ loading, rowCount, status, listbox, children }:
|
||||
maxHeight: 260,
|
||||
overflowY: 'auto',
|
||||
zIndex: 1000,
|
||||
...(floating ? { ...FLOATING_GLASS, borderRadius: 16, padding: 4 } : {}),
|
||||
}}
|
||||
>
|
||||
{loading && rowCount === 0 && (
|
||||
|
||||
Reference in New Issue
Block a user