ProvidersStep search: tighten icons, commit-to-chip, primary search button
Sticky search now uses Autocomplete (multiple+freeSolo capped to 1) instead of a plain TextField: - Pin icon tightened to the left edge and to the placeholder - Committed location renders inside the input as an FA Chip with an X delete (clears the committed filter) - Primary-coloured magnifying-glass IconButton on the right commits the draft; disabled while the draft is empty - Typing no longer filters live — Enter or the search button promotes the draft to a chip, matching the chip mental model The FilterPanel dialog's Location autocomplete already read from the same searchQuery state, so it continues to display the committed chip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import Menu from '@mui/material/Menu';
|
|||||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||||
import ToggleButton from '@mui/material/ToggleButton';
|
import ToggleButton from '@mui/material/ToggleButton';
|
||||||
import SwapVertIcon from '@mui/icons-material/SwapVert';
|
import SwapVertIcon from '@mui/icons-material/SwapVert';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import ViewListOutlinedIcon from '@mui/icons-material/ViewListOutlined';
|
import ViewListOutlinedIcon from '@mui/icons-material/ViewListOutlined';
|
||||||
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
|
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
|
||||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||||
@@ -19,6 +20,7 @@ import { ProviderCard } from '../../molecules/ProviderCard';
|
|||||||
import { FilterPanel } from '../../molecules/FilterPanel';
|
import { FilterPanel } from '../../molecules/FilterPanel';
|
||||||
import { Button } from '../../atoms/Button';
|
import { Button } from '../../atoms/Button';
|
||||||
import { Chip } from '../../atoms/Chip';
|
import { Chip } from '../../atoms/Chip';
|
||||||
|
import { IconButton } from '../../atoms/IconButton';
|
||||||
import { Switch } from '../../atoms/Switch';
|
import { Switch } from '../../atoms/Switch';
|
||||||
import { Typography } from '../../atoms/Typography';
|
import { Typography } from '../../atoms/Typography';
|
||||||
import { Divider } from '../../atoms/Divider';
|
import { Divider } from '../../atoms/Divider';
|
||||||
@@ -246,6 +248,18 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
|
|
||||||
// ─── Local state ───
|
// ─── Local state ───
|
||||||
const [sortAnchor, setSortAnchor] = React.useState<null | HTMLElement>(null);
|
const [sortAnchor, setSortAnchor] = React.useState<null | HTMLElement>(null);
|
||||||
|
// Draft value for the sticky search input — only committed (promoted to a
|
||||||
|
// chip) on Enter or when the search button is clicked. searchQuery is the
|
||||||
|
// committed filter value; the draft lives here until the user confirms.
|
||||||
|
const [searchDraft, setSearchDraft] = React.useState('');
|
||||||
|
|
||||||
|
const commitSearch = (next: string) => {
|
||||||
|
const trimmed = next.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
onSearchChange(trimmed);
|
||||||
|
onSearch?.(trimmed);
|
||||||
|
setSearchDraft('');
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Price input local state (commits on blur / Enter) ───
|
// ─── Price input local state (commits on blur / Enter) ───
|
||||||
const [priceMinInput, setPriceMinInput] = React.useState(String(filterValues.priceRange[0]));
|
const [priceMinInput, setPriceMinInput] = React.useState(String(filterValues.priceRange[0]));
|
||||||
@@ -395,27 +409,82 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Location search */}
|
{/* Location search — committed location renders as a chip inside the
|
||||||
<TextField
|
input. Typing produces a draft; Enter or the search button commit
|
||||||
placeholder="Search a town or suburb..."
|
it. Deleting the chip clears the committed filter. */}
|
||||||
aria-label="Search providers by town or suburb"
|
<Autocomplete
|
||||||
value={searchQuery}
|
multiple
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
freeSolo
|
||||||
onKeyDown={(e) => {
|
options={[]}
|
||||||
if (e.key === 'Enter' && onSearch) {
|
value={searchQuery.trim() ? [searchQuery.trim()] : []}
|
||||||
e.preventDefault();
|
inputValue={searchDraft}
|
||||||
onSearch(searchQuery);
|
onInputChange={(_, newDraft, reason) => {
|
||||||
}
|
// Ignore the 'reset' input-change Autocomplete fires after a value
|
||||||
|
// commit (it echoes the committed value back into the input and
|
||||||
|
// would otherwise re-populate the draft we just cleared).
|
||||||
|
if (reason === 'reset') return;
|
||||||
|
setSearchDraft(newDraft);
|
||||||
}}
|
}}
|
||||||
fullWidth
|
onChange={(_, newValue) => {
|
||||||
|
if (newValue.length === 0) {
|
||||||
|
// Chip removed — clear the committed filter
|
||||||
|
onSearchChange('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Commit the most-recent entry (cap at 1 location)
|
||||||
|
const last = newValue[newValue.length - 1];
|
||||||
|
if (typeof last === 'string') commitSearch(last);
|
||||||
|
}}
|
||||||
|
renderTags={(value, getTagProps) =>
|
||||||
|
value.map((option, index) => {
|
||||||
|
const { key, ...chipProps } = getTagProps({ index });
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
key={key}
|
||||||
|
label={option}
|
||||||
size="small"
|
size="small"
|
||||||
|
selected
|
||||||
|
aria-label={`Current location: ${option}. Press delete to clear.`}
|
||||||
|
{...chipProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder={searchQuery.trim() ? '' : 'Search a town or suburb...'}
|
||||||
|
size="small"
|
||||||
|
inputProps={{
|
||||||
|
...params.inputProps,
|
||||||
|
'aria-label': 'Search providers by town or suburb',
|
||||||
|
}}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
|
...params.InputProps,
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<>
|
||||||
|
<InputAdornment position="start" sx={{ ml: 0.5, mr: 0.5 }}>
|
||||||
<LocationOnOutlinedIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
<LocationOnOutlinedIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
|
{params.InputProps.startAdornment}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end" sx={{ mr: 0.25 }}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Search"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => commitSearch(searchDraft)}
|
||||||
|
disabled={!searchDraft.trim()}
|
||||||
|
>
|
||||||
|
<SearchIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
sx={{ mb: 1.5 }}
|
sx={{ mb: 1.5 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user