ProvidersStep mobile: unify list-view controls + PackagesStep drill-in fix
Controls - Mobile list view's Filters/Sort/view-toggle now match the mobile map view's floating-chip treatment: white fill, neutral-300 border, shadow-sm. On mobile the sort label switches from "Sort: <value>" to a compact "Sort by"; desktop keeps its verbose label. - List/Map toggle font bumped to 14px / 600 (button-small token), so it reads on the same line as Filters + Sort by both on mobile and on the desktop floating pill over the map panel. PackagesStep drill-in - Added a local hasDrilledIn flag so the mobile layout only swaps to the detail view after an explicit user tap on a package. Previously any pre-selection (the demo route seeds selectedPackageId to the first matching package for desktop auto-display) also forced mobile into the detail view, so users arriving from the map drawer saw a single package instead of the list. Back/forward from the detail resets the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
@@ -206,17 +206,25 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
|||||||
const copy = TIER_COPY[providerTier];
|
const copy = TIER_COPY[providerTier];
|
||||||
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
|
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
|
||||||
|
|
||||||
// Mobile drill-in: when a package is selected on mobile, swap the list view
|
// Mobile drill-in: on mobile, the list is the default view — only when the
|
||||||
// for the detail view. Back button clears selection to return to the list.
|
// user explicitly taps a package do we swap in the detail panel. This
|
||||||
|
// distinguishes "parent pre-selected first package for desktop auto-display"
|
||||||
|
// (which should NOT jump to detail on mobile) from "user tapped a package".
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const mobileShowDetail = isMobile && selectedPackageId != null;
|
const [hasDrilledIn, setHasDrilledIn] = useState(false);
|
||||||
|
const mobileShowDetail = isMobile && hasDrilledIn && selectedPackageId != null;
|
||||||
|
|
||||||
|
const handleSelectPackage = (id: string | null) => {
|
||||||
|
setHasDrilledIn(id != null);
|
||||||
|
onSelectPackage(id);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mobileShowDetail) window.scrollTo({ top: 0, behavior: 'auto' });
|
if (mobileShowDetail) window.scrollTo({ top: 0, behavior: 'auto' });
|
||||||
}, [mobileShowDetail]);
|
}, [mobileShowDetail]);
|
||||||
|
|
||||||
const handleLayoutBack = mobileShowDetail ? () => onSelectPackage(null) : onBack;
|
const handleLayoutBack = mobileShowDetail ? () => handleSelectPackage(null) : onBack;
|
||||||
const layoutBackLabel = mobileShowDetail ? 'Back to packages' : 'Back';
|
const layoutBackLabel = mobileShowDetail ? 'Back to packages' : 'Back';
|
||||||
|
|
||||||
// Secondary list suppressed in "show all" mode.
|
// Secondary list suppressed in "show all" mode.
|
||||||
@@ -349,7 +357,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
|||||||
description={pkg.description}
|
description={pkg.description}
|
||||||
price={pkg.price}
|
price={pkg.price}
|
||||||
selected={selectedPackageId === pkg.id}
|
selected={selectedPackageId === pkg.id}
|
||||||
onClick={() => onSelectPackage(pkg.id)}
|
onClick={() => handleSelectPackage(pkg.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -385,7 +393,7 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
|||||||
description={pkg.description}
|
description={pkg.description}
|
||||||
price={pkg.price}
|
price={pkg.price}
|
||||||
selected={selectedPackageId === pkg.id}
|
selected={selectedPackageId === pkg.id}
|
||||||
onClick={() => onSelectPackage(pkg.id)}
|
onClick={() => handleSelectPackage(pkg.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -687,7 +687,7 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{/* View toggle — text labels on mobile, aligned height with
|
{/* View toggle — text labels on mobile, aligned height with
|
||||||
the buttons */}
|
the buttons; font matches Filters/Sort (14px / 600) */}
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
exclusive
|
exclusive
|
||||||
@@ -700,11 +700,11 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
boxShadow: 'var(--fa-shadow-sm)',
|
boxShadow: 'var(--fa-shadow-sm)',
|
||||||
'& .MuiToggleButton-root': {
|
'& .MuiToggleButton-root': {
|
||||||
height: 32,
|
height: 32,
|
||||||
px: 1.25,
|
px: 1.5,
|
||||||
py: 0,
|
py: 0,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontSize: '0.8125rem',
|
fontSize: 'var(--fa-button-font-size-sm)',
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
borderColor: 'var(--fa-color-neutral-300)',
|
borderColor: 'var(--fa-color-neutral-300)',
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
'&:hover': { bgcolor: 'background.paper' },
|
'&:hover': { bgcolor: 'background.paper' },
|
||||||
@@ -961,8 +961,8 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
height: 'var(--fa-button-height-sm)',
|
height: 'var(--fa-button-height-sm)',
|
||||||
px: 1.5,
|
px: 1.5,
|
||||||
py: 0,
|
py: 0,
|
||||||
fontSize: '0.8125rem',
|
fontSize: 'var(--fa-button-font-size-sm)',
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
gap: 0.75,
|
gap: 0.75,
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
@@ -1134,31 +1134,64 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
gap: 1,
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Filters */}
|
{/* Filters — on mobile, matches the map-view floating chip style
|
||||||
|
(white fill, neutral-300 border, shadow-sm). On desktop,
|
||||||
|
default Button small look. */}
|
||||||
<FilterPanel
|
<FilterPanel
|
||||||
activeCount={activeCount}
|
activeCount={activeCount}
|
||||||
onClear={handleClear}
|
onClear={handleClear}
|
||||||
sx={{ '& .MuiButton-root:focus-visible': { outline: 'none' } }}
|
sx={{
|
||||||
|
'& .MuiButton-root': {
|
||||||
|
height: { xs: 32, md: undefined },
|
||||||
|
bgcolor: { xs: 'background.paper', md: undefined },
|
||||||
|
borderColor: { xs: 'var(--fa-color-neutral-300)', md: undefined },
|
||||||
|
boxShadow: { xs: 'var(--fa-shadow-sm)', md: 'none' },
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: { xs: 'background.paper', md: undefined },
|
||||||
|
borderColor: { xs: 'var(--fa-color-neutral-300)', md: undefined },
|
||||||
|
},
|
||||||
|
'&:focus-visible': { outline: 'none' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{filterDialogChildren}
|
{filterDialogChildren}
|
||||||
</FilterPanel>
|
</FilterPanel>
|
||||||
|
|
||||||
{/* Sort — compact menu button, pushed right */}
|
{/* Sort — mobile shows a compact "Sort by" text button matching
|
||||||
|
the Filters chip style; desktop keeps the full "Sort: <value>"
|
||||||
|
label with its swap icon. */}
|
||||||
<Box sx={{ ml: 'auto' }}>
|
<Box sx={{ ml: 'auto' }}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<SwapVertIcon sx={{ fontSize: 16 }} />}
|
startIcon={isMobile ? undefined : <SwapVertIcon sx={{ fontSize: 16 }} />}
|
||||||
onClick={(e) => setSortAnchor(e.currentTarget)}
|
onClick={(e) => setSortAnchor(e.currentTarget)}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-label={`Sort by ${SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Recommended'}`}
|
aria-label={`Sort by ${SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Recommended'}`}
|
||||||
sx={{ textTransform: 'none', '&:focus-visible': { outline: 'none' } }}
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
height: { xs: 32, md: undefined },
|
||||||
|
bgcolor: { xs: 'background.paper', md: undefined },
|
||||||
|
borderColor: { xs: 'var(--fa-color-neutral-300)', md: undefined },
|
||||||
|
boxShadow: { xs: 'var(--fa-shadow-sm)', md: 'none' },
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: { xs: 'background.paper', md: undefined },
|
||||||
|
borderColor: { xs: 'var(--fa-color-neutral-300)', md: undefined },
|
||||||
|
},
|
||||||
|
'&:focus-visible': { outline: 'none' },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box component="span" sx={{ color: 'text.secondary', fontWeight: 400, mr: 0.5 }}>
|
{isMobile ? (
|
||||||
Sort:
|
'Sort by'
|
||||||
</Box>
|
) : (
|
||||||
{SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Recommended'}
|
<>
|
||||||
|
<Box component="span" sx={{ color: 'text.secondary', fontWeight: 400, mr: 0.5 }}>
|
||||||
|
Sort:
|
||||||
|
</Box>
|
||||||
|
{SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Recommended'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={sortAnchor}
|
anchorEl={sortAnchor}
|
||||||
@@ -1183,7 +1216,9 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Mobile-only view toggle — switches to the map-first layout */}
|
{/* Mobile-only view toggle — matches the map-view floating toggle:
|
||||||
|
text labels (List / Map), white fill, neutral-300 border, shadow,
|
||||||
|
14px / 600 type to align with the Filters + Sort by buttons. */}
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
exclusive
|
exclusive
|
||||||
@@ -1192,13 +1227,18 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
aria-label="View mode"
|
aria-label="View mode"
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: 'inline-flex', md: 'none' },
|
display: { xs: 'inline-flex', md: 'none' },
|
||||||
|
flexShrink: 0,
|
||||||
|
boxShadow: 'var(--fa-shadow-sm)',
|
||||||
'& .MuiToggleButton-root': {
|
'& .MuiToggleButton-root': {
|
||||||
px: 1,
|
height: 32,
|
||||||
py: 0.5,
|
px: 1.5,
|
||||||
gap: 0.5,
|
py: 0,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontSize: '0.75rem',
|
fontSize: 'var(--fa-button-font-size-sm)',
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
|
borderColor: 'var(--fa-color-neutral-300)',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
'&:hover': { bgcolor: 'background.paper' },
|
||||||
'&.Mui-selected': {
|
'&.Mui-selected': {
|
||||||
bgcolor: 'var(--fa-color-brand-100)',
|
bgcolor: 'var(--fa-color-brand-100)',
|
||||||
color: 'primary.main',
|
color: 'primary.main',
|
||||||
@@ -1208,10 +1248,10 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleButton value="list" aria-label="List view">
|
<ToggleButton value="list" aria-label="List view">
|
||||||
<ViewListOutlinedIcon sx={{ fontSize: 16 }} />
|
List
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="map" aria-label="Map view">
|
<ToggleButton value="map" aria-label="Map view">
|
||||||
<MapOutlinedIcon sx={{ fontSize: 16 }} />
|
Map
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user