Quality pass: fix P0 audit findings across FilterPanel, ArrangementDialog, steps
FilterPanel (4 P0 + 2 P1 fixes):
- Add forwardRef (project convention)
- Use React.useId() for unique popover/heading IDs (was static)
- Change aria-describedby to aria-controls (correct ARIA pattern)
- Add role="dialog" + aria-labelledby on Popover paper
- Popover header now uses label prop (was hardcoded "Filters")
- Clear all font size uses theme.typography.caption (was hardcoded)
- Badge uses aria-hidden + visually-hidden text (cleaner SR output)
- Add maxHeight + overflow scroll to body, aria-label on Done button
ArrangementDialog (3 P0 + 1 P1 fixes):
- Add forwardRef
- Focus management: titleRef focused on step change via useEffect
- Add aria-live region announcing step transitions to screen readers
- Fix borderRadius from 3 to 2 (theme convention)
Sticky header padding (visual fix):
- ProvidersStep + VenueStep: mx/px now responsive { xs: -2/2, md: -3/3 }
matching the panel's px: { xs: 2, md: 3 } — fixes mobile misalignment
CoffinDetailsStep:
- Wrap CTA area in form element with onSubmit + aria-busy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,128 +39,146 @@ export interface FilterPanelProps {
|
|||||||
* D-C: Popover for desktop MVP. Mobile Drawer variant planned for later.
|
* D-C: Popover for desktop MVP. Mobile Drawer variant planned for later.
|
||||||
*
|
*
|
||||||
* Used in ProvidersStep, VenueStep, and CoffinsStep.
|
* Used in ProvidersStep, VenueStep, and CoffinsStep.
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```tsx
|
|
||||||
* <FilterPanel activeCount={2} onClear={handleClear}>
|
|
||||||
* <TextField select label="Category" ... />
|
|
||||||
* <TextField select label="Price" ... />
|
|
||||||
* </FilterPanel>
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const FilterPanel: React.FC<FilterPanelProps> = ({
|
export const FilterPanel = React.forwardRef<HTMLDivElement, FilterPanelProps>(
|
||||||
label = 'Filters',
|
({ label = 'Filters', activeCount = 0, children, onClear, minWidth = 280, sx }, ref) => {
|
||||||
activeCount = 0,
|
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
||||||
children,
|
const open = Boolean(anchorEl);
|
||||||
onClear,
|
const uniqueId = React.useId();
|
||||||
minWidth = 280,
|
const popoverId = `filter-panel-${uniqueId}`;
|
||||||
sx,
|
const headingId = `filter-panel-heading-${uniqueId}`;
|
||||||
}) => {
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
const popoverId = open ? 'filter-panel-popover' : undefined;
|
|
||||||
|
|
||||||
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Trigger button */}
|
{/* Trigger button */}
|
||||||
<Box sx={[{ display: 'inline-flex' }, ...(Array.isArray(sx) ? sx : [sx])]}>
|
<Box ref={ref} sx={[{ display: 'inline-flex' }, ...(Array.isArray(sx) ? sx : [sx])]}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<TuneIcon />}
|
startIcon={<TuneIcon />}
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
aria-describedby={popoverId}
|
aria-controls={open ? popoverId : undefined}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{activeCount > 0 && (
|
{activeCount > 0 && (
|
||||||
<Badge
|
<Badge variant="filled" color="brand" size="small" sx={{ ml: 1 }} aria-hidden="true">
|
||||||
variant="filled"
|
{activeCount}
|
||||||
color="brand"
|
</Badge>
|
||||||
size="small"
|
)}
|
||||||
sx={{ ml: 1 }}
|
{activeCount > 0 && (
|
||||||
aria-label={`${activeCount} active filter${activeCount !== 1 ? 's' : ''}`}
|
<Box
|
||||||
>
|
component="span"
|
||||||
{activeCount}
|
sx={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden' }}
|
||||||
</Badge>
|
>
|
||||||
)}
|
{activeCount} active filter{activeCount !== 1 ? 's' : ''}
|
||||||
</Button>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
|
|
||||||
{/* Popover panel */}
|
|
||||||
<Popover
|
|
||||||
id={popoverId}
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
onClose={handleClose}
|
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
||||||
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
|
||||||
slotProps={{
|
|
||||||
paper: {
|
|
||||||
sx: {
|
|
||||||
minWidth,
|
|
||||||
mt: 1,
|
|
||||||
borderRadius: 2,
|
|
||||||
boxShadow: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
px: 2.5,
|
|
||||||
pt: 2,
|
|
||||||
pb: 1.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6">Filters</Typography>
|
|
||||||
{onClear && activeCount > 0 && (
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
onClick={() => {
|
|
||||||
onClear();
|
|
||||||
}}
|
|
||||||
underline="hover"
|
|
||||||
sx={{ fontSize: '0.8125rem' }}
|
|
||||||
>
|
|
||||||
Clear all
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{/* Filter controls */}
|
|
||||||
<Box sx={{ px: 2.5, py: 2, display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{/* Footer — done button */}
|
|
||||||
<Box sx={{ px: 2.5, py: 1.5, display: 'flex', justifyContent: 'flex-end' }}>
|
|
||||||
<Button variant="contained" size="small" onClick={handleClose}>
|
|
||||||
Done
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Popover>
|
|
||||||
</>
|
{/* Popover panel */}
|
||||||
);
|
<Popover
|
||||||
};
|
id={popoverId}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
|
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
sx: {
|
||||||
|
minWidth,
|
||||||
|
maxHeight: '70vh',
|
||||||
|
mt: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: 3,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
PaperProps={{
|
||||||
|
role: 'dialog' as const,
|
||||||
|
'aria-labelledby': headingId,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
px: 2.5,
|
||||||
|
pt: 2,
|
||||||
|
pb: 1.5,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography id={headingId} variant="h6">
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
{onClear && activeCount > 0 && (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
onClick={() => {
|
||||||
|
onClear();
|
||||||
|
}}
|
||||||
|
underline="hover"
|
||||||
|
sx={{ fontSize: (theme: Theme) => theme.typography.caption.fontSize }}
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Filter controls */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: 2.5,
|
||||||
|
py: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2.5,
|
||||||
|
overflowY: 'auto',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Footer — done button */}
|
||||||
|
<Box
|
||||||
|
sx={{ px: 2.5, py: 1.5, display: 'flex', justifyContent: 'flex-end', flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Close filters"
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
FilterPanel.displayName = 'FilterPanel';
|
FilterPanel.displayName = 'FilterPanel';
|
||||||
export default FilterPanel;
|
export default FilterPanel;
|
||||||
|
|||||||
@@ -157,385 +157,412 @@ function getAuthCTALabel(subStep: AuthSubStep): string {
|
|||||||
*
|
*
|
||||||
* Pure presentation component — props in, callbacks out.
|
* Pure presentation component — props in, callbacks out.
|
||||||
*/
|
*/
|
||||||
export const ArrangementDialog: React.FC<ArrangementDialogProps> = ({
|
export const ArrangementDialog = React.forwardRef<HTMLDivElement, ArrangementDialogProps>(
|
||||||
open,
|
(
|
||||||
onClose,
|
{
|
||||||
step,
|
open,
|
||||||
onStepChange,
|
onClose,
|
||||||
provider,
|
step,
|
||||||
selectedPackage,
|
onStepChange,
|
||||||
nextSteps = DEFAULT_NEXT_STEPS,
|
provider,
|
||||||
isPrePlanning = false,
|
selectedPackage,
|
||||||
onExplore,
|
nextSteps = DEFAULT_NEXT_STEPS,
|
||||||
authValues,
|
isPrePlanning = false,
|
||||||
onAuthChange,
|
onExplore,
|
||||||
authErrors,
|
authValues,
|
||||||
onGoogleSSO,
|
onAuthChange,
|
||||||
onMicrosoftSSO,
|
authErrors,
|
||||||
onContinue,
|
onGoogleSSO,
|
||||||
loading = false,
|
onMicrosoftSSO,
|
||||||
sx,
|
onContinue,
|
||||||
}) => {
|
loading = false,
|
||||||
const isEmailOnly = authValues.contactPreference === 'email_only';
|
sx,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const isEmailOnly = authValues.contactPreference === 'email_only';
|
||||||
|
const titleRef = React.useRef<HTMLHeadingElement>(null);
|
||||||
|
|
||||||
const handleAuthField = (field: keyof AuthValues, value: string) => {
|
// Focus the dialog title when step changes
|
||||||
onAuthChange({ ...authValues, [field]: value });
|
React.useEffect(() => {
|
||||||
};
|
if (open && titleRef.current) {
|
||||||
|
titleRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [step, open]);
|
||||||
|
|
||||||
return (
|
const handleAuthField = (field: keyof AuthValues, value: string) => {
|
||||||
<Dialog
|
onAuthChange({ ...authValues, [field]: value });
|
||||||
open={open}
|
};
|
||||||
onClose={onClose}
|
|
||||||
maxWidth="sm"
|
return (
|
||||||
fullWidth
|
<Dialog
|
||||||
scroll="body"
|
ref={ref}
|
||||||
aria-labelledby="arrangement-dialog-title"
|
open={open}
|
||||||
sx={sx}
|
onClose={onClose}
|
||||||
PaperProps={{
|
maxWidth="sm"
|
||||||
sx: { borderRadius: 3 },
|
fullWidth
|
||||||
}}
|
scroll="body"
|
||||||
>
|
aria-labelledby="arrangement-dialog-title"
|
||||||
{/* ─── Header ─── */}
|
sx={sx}
|
||||||
<Box
|
PaperProps={{
|
||||||
sx={{
|
sx: { borderRadius: 2 },
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
px: 3,
|
|
||||||
pt: 2.5,
|
|
||||||
pb: 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
{/* ─── Header ─── */}
|
||||||
{step === 'auth' && (
|
<Box
|
||||||
<IconButton
|
sx={{
|
||||||
size="small"
|
display: 'flex',
|
||||||
onClick={() => onStepChange('preview')}
|
alignItems: 'center',
|
||||||
aria-label="Back to preview"
|
justifyContent: 'space-between',
|
||||||
>
|
px: 3,
|
||||||
<ArrowBackIcon fontSize="small" />
|
pt: 2.5,
|
||||||
</IconButton>
|
pb: 1,
|
||||||
)}
|
}}
|
||||||
<Typography id="arrangement-dialog-title" variant="h5">
|
>
|
||||||
{step === 'preview' ? 'Your selected package' : 'Save your plan'}
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
</Typography>
|
{step === 'auth' && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => onStepChange('preview')}
|
||||||
|
aria-label="Back to preview"
|
||||||
|
>
|
||||||
|
<ArrowBackIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<Typography id="arrangement-dialog-title" ref={titleRef} tabIndex={-1} variant="h5">
|
||||||
|
{step === 'preview' ? 'Your selected package' : 'Save your plan'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small" onClick={onClose} aria-label="Close">
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="small" onClick={onClose} aria-label="Close">
|
|
||||||
<CloseIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<DialogContent sx={{ px: 3, pb: 3 }}>
|
{/* Screen reader step announcement */}
|
||||||
{/* ═══════════ Step 1: Preview ═══════════ */}
|
<Box
|
||||||
{step === 'preview' && (
|
aria-live="polite"
|
||||||
<Box>
|
aria-atomic="true"
|
||||||
{/* Provider */}
|
sx={{ position: 'absolute', width: 1, height: 1, overflow: 'hidden' }}
|
||||||
<Box sx={{ mb: 3 }}>
|
>
|
||||||
<ProviderCardCompact
|
{step === 'preview' ? 'Viewing package preview' : 'Create your account'}
|
||||||
name={provider.name}
|
</Box>
|
||||||
location={provider.location}
|
|
||||||
imageUrl={provider.imageUrl}
|
|
||||||
rating={provider.rating}
|
|
||||||
reviewCount={provider.reviewCount}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Package summary */}
|
<DialogContent sx={{ px: 3, pb: 3 }}>
|
||||||
<Box
|
{/* ═══════════ Step 1: Preview ═══════════ */}
|
||||||
sx={{
|
{step === 'preview' && (
|
||||||
bgcolor: 'var(--fa-color-brand-50)',
|
<Box>
|
||||||
borderRadius: 2,
|
{/* Provider */}
|
||||||
p: 2.5,
|
<Box sx={{ mb: 3 }}>
|
||||||
mb: 3,
|
<ProviderCardCompact
|
||||||
}}
|
name={provider.name}
|
||||||
>
|
location={provider.location}
|
||||||
|
imageUrl={provider.imageUrl}
|
||||||
|
rating={provider.rating}
|
||||||
|
reviewCount={provider.reviewCount}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Package summary */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'var(--fa-color-brand-50)',
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 2.5,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">{selectedPackage.name}</Typography>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
${(selectedPackage.total ?? selectedPackage.price).toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{selectedPackage.sections.map((section) => (
|
||||||
|
<Box key={section.heading} sx={{ mb: 1.5 }}>
|
||||||
|
<Typography variant="labelSm" color="text.secondary" sx={{ mb: 0.5 }}>
|
||||||
|
{section.heading}
|
||||||
|
</Typography>
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<Typography key={item.name} variant="body2" sx={{ pl: 1 }}>
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
You'll be able to customise everything in the next steps.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* What's next */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 1.5 }}>
|
||||||
|
What happens next
|
||||||
|
</Typography>
|
||||||
|
<List disablePadding>
|
||||||
|
{nextSteps.map((s) => (
|
||||||
|
<ListItem
|
||||||
|
key={s.number}
|
||||||
|
disablePadding
|
||||||
|
sx={{ mb: 1, alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ minWidth: 36, mt: 0.25 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: 'var(--fa-color-brand-100)',
|
||||||
|
color: 'var(--fa-color-brand-700)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.number}
|
||||||
|
</Box>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={s.label}
|
||||||
|
primaryTypographyProps={{ variant: 'body2', color: 'text.primary' }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mb: 3 }} />
|
||||||
|
|
||||||
|
{/* CTAs */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
alignItems: 'baseline',
|
gap: 2,
|
||||||
mb: 2,
|
justifyContent: 'flex-end',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6">{selectedPackage.name}</Typography>
|
{isPrePlanning && onExplore && (
|
||||||
<Typography variant="h6" color="primary">
|
<Button variant="outlined" color="secondary" size="large" onClick={onExplore}>
|
||||||
${(selectedPackage.total ?? selectedPackage.price).toLocaleString('en-AU')}
|
Explore other options
|
||||||
</Typography>
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
onClick={() => onStepChange('auth')}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Continue with this package
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{selectedPackage.sections.map((section) => (
|
|
||||||
<Box key={section.heading} sx={{ mb: 1.5 }}>
|
|
||||||
<Typography variant="labelSm" color="text.secondary" sx={{ mb: 0.5 }}>
|
|
||||||
{section.heading}
|
|
||||||
</Typography>
|
|
||||||
{section.items.map((item) => (
|
|
||||||
<Typography key={item.name} variant="body2" sx={{ pl: 1 }}>
|
|
||||||
{item.name}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
{/* ═══════════ Step 2: Auth ═══════════ */}
|
||||||
You'll be able to customise everything in the next steps.
|
{step === 'auth' && (
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* What's next */}
|
|
||||||
<Box sx={{ mb: 3 }}>
|
|
||||||
<Typography variant="h6" sx={{ mb: 1.5 }}>
|
|
||||||
What happens next
|
|
||||||
</Typography>
|
|
||||||
<List disablePadding>
|
|
||||||
{nextSteps.map((s) => (
|
|
||||||
<ListItem key={s.number} disablePadding sx={{ mb: 1, alignItems: 'flex-start' }}>
|
|
||||||
<ListItemIcon sx={{ minWidth: 36, mt: 0.25 }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: '50%',
|
|
||||||
bgcolor: 'var(--fa-color-brand-100)',
|
|
||||||
color: 'var(--fa-color-brand-700)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{s.number}
|
|
||||||
</Box>
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
primary={s.label}
|
|
||||||
primaryTypographyProps={{ variant: 'body2', color: 'text.primary' }}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider sx={{ mb: 3 }} />
|
|
||||||
|
|
||||||
{/* CTAs */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
component="form"
|
||||||
display: 'flex',
|
noValidate
|
||||||
flexDirection: { xs: 'column', sm: 'row' },
|
aria-busy={loading}
|
||||||
gap: 2,
|
onSubmit={(e: React.FormEvent) => {
|
||||||
justifyContent: 'flex-end',
|
e.preventDefault();
|
||||||
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isPrePlanning && onExplore && (
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
<Button variant="outlined" color="secondary" size="large" onClick={onExplore}>
|
{isPrePlanning
|
||||||
Explore other options
|
? 'Save your plan to return and update it anytime.'
|
||||||
</Button>
|
: 'We need a few details so a funeral arranger can help you with the next steps.'}
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size="large"
|
|
||||||
onClick={() => onStepChange('auth')}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
Continue with this package
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ═══════════ Step 2: Auth ═══════════ */}
|
|
||||||
{step === 'auth' && (
|
|
||||||
<Box
|
|
||||||
component="form"
|
|
||||||
noValidate
|
|
||||||
aria-busy={loading}
|
|
||||||
onSubmit={(e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!loading) onContinue();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
|
||||||
{isPrePlanning
|
|
||||||
? 'Save your plan to return and update it anytime.'
|
|
||||||
: 'We need a few details so a funeral arranger can help you with the next steps.'}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* SSO buttons */}
|
|
||||||
<Box
|
|
||||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, mb: 3 }}
|
|
||||||
role="group"
|
|
||||||
aria-label="Sign in options"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
size="large"
|
|
||||||
fullWidth
|
|
||||||
startIcon={<GoogleIcon />}
|
|
||||||
onClick={onGoogleSSO}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
size="large"
|
|
||||||
fullWidth
|
|
||||||
startIcon={<MicrosoftIcon />}
|
|
||||||
onClick={onMicrosoftSSO}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Continue with Microsoft
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider sx={{ my: 3 }}>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
or
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Divider>
|
|
||||||
|
|
||||||
{/* Email */}
|
{/* SSO buttons */}
|
||||||
<TextField
|
|
||||||
label="Your email address"
|
|
||||||
type="email"
|
|
||||||
value={authValues.email}
|
|
||||||
onChange={(e) => handleAuthField('email', e.target.value)}
|
|
||||||
error={!!authErrors?.email}
|
|
||||||
helperText={authErrors?.email}
|
|
||||||
placeholder="you@example.com"
|
|
||||||
autoComplete="email"
|
|
||||||
inputMode="email"
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
disabled={authValues.subStep !== 'email'}
|
|
||||||
sx={{ mb: 3 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Details (after email) */}
|
|
||||||
<Collapse in={authValues.subStep === 'details' || authValues.subStep === 'verify'}>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2.5, mb: 3 }}
|
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, mb: 3 }}
|
||||||
role="group"
|
role="group"
|
||||||
aria-label="Your details"
|
aria-label="Sign in options"
|
||||||
>
|
>
|
||||||
<Typography variant="labelLg" component="h2" sx={{ mb: 0.5 }}>
|
<Button
|
||||||
A few details to save your plan
|
variant="outlined"
|
||||||
</Typography>
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<GoogleIcon />}
|
||||||
|
onClick={onGoogleSSO}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Continue with Google
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<MicrosoftIcon />}
|
||||||
|
onClick={onMicrosoftSSO}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Continue with Microsoft
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
or
|
||||||
|
</Typography>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<TextField
|
||||||
|
label="Your email address"
|
||||||
|
type="email"
|
||||||
|
value={authValues.email}
|
||||||
|
onChange={(e) => handleAuthField('email', e.target.value)}
|
||||||
|
error={!!authErrors?.email}
|
||||||
|
helperText={authErrors?.email}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
autoComplete="email"
|
||||||
|
inputMode="email"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={authValues.subStep !== 'email'}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Details (after email) */}
|
||||||
|
<Collapse in={authValues.subStep === 'details' || authValues.subStep === 'verify'}>
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', gap: 2.5, mb: 3 }}
|
||||||
|
role="group"
|
||||||
|
aria-label="Your details"
|
||||||
|
>
|
||||||
|
<Typography variant="labelLg" component="h2" sx={{ mb: 0.5 }}>
|
||||||
|
A few details to save your plan
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="First name"
|
||||||
|
value={authValues.firstName}
|
||||||
|
onChange={(e) => handleAuthField('firstName', e.target.value)}
|
||||||
|
error={!!authErrors?.firstName}
|
||||||
|
helperText={authErrors?.firstName}
|
||||||
|
autoComplete="given-name"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={authValues.subStep === 'verify'}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Last name"
|
||||||
|
value={authValues.lastName}
|
||||||
|
onChange={(e) => handleAuthField('lastName', e.target.value)}
|
||||||
|
error={!!authErrors?.lastName}
|
||||||
|
helperText={authErrors?.lastName}
|
||||||
|
autoComplete="family-name"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={authValues.subStep === 'verify'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 2 }}>
|
|
||||||
<TextField
|
<TextField
|
||||||
label="First name"
|
label={isEmailOnly ? 'Phone (optional)' : 'Best number to reach you'}
|
||||||
value={authValues.firstName}
|
type="tel"
|
||||||
onChange={(e) => handleAuthField('firstName', e.target.value)}
|
value={authValues.phone}
|
||||||
error={!!authErrors?.firstName}
|
onChange={(e) => handleAuthField('phone', e.target.value)}
|
||||||
helperText={authErrors?.firstName}
|
error={!!authErrors?.phone}
|
||||||
autoComplete="given-name"
|
helperText={authErrors?.phone}
|
||||||
|
placeholder="e.g. 0412 345 678"
|
||||||
|
autoComplete="tel"
|
||||||
|
inputMode="tel"
|
||||||
fullWidth
|
fullWidth
|
||||||
required
|
required={!isEmailOnly}
|
||||||
disabled={authValues.subStep === 'verify'}
|
disabled={authValues.subStep === 'verify'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Last name"
|
select
|
||||||
value={authValues.lastName}
|
label="Contact preference"
|
||||||
onChange={(e) => handleAuthField('lastName', e.target.value)}
|
value={authValues.contactPreference}
|
||||||
error={!!authErrors?.lastName}
|
onChange={(e) => handleAuthField('contactPreference', e.target.value)}
|
||||||
helperText={authErrors?.lastName}
|
fullWidth
|
||||||
autoComplete="family-name"
|
disabled={authValues.subStep === 'verify'}
|
||||||
|
>
|
||||||
|
<MenuItem value="call_anytime">Call me anytime</MenuItem>
|
||||||
|
<MenuItem value="email_preferred">Email is preferred</MenuItem>
|
||||||
|
<MenuItem value="email_only">Only contact by email</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* Verification */}
|
||||||
|
<Collapse in={authValues.subStep === 'verify'}>
|
||||||
|
<Box sx={{ mb: 3 }} role="group" aria-label="Email verification">
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
We've sent a 6-digit code to <strong>{authValues.email}</strong>. Please
|
||||||
|
enter it below.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Verification code"
|
||||||
|
value={authValues.verificationCode}
|
||||||
|
onChange={(e) => handleAuthField('verificationCode', e.target.value)}
|
||||||
|
error={!!authErrors?.verificationCode}
|
||||||
|
helperText={
|
||||||
|
authErrors?.verificationCode || 'Check your email for the 6-digit code'
|
||||||
|
}
|
||||||
|
placeholder="Enter 6-digit code"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
inputMode="numeric"
|
||||||
fullWidth
|
fullWidth
|
||||||
required
|
required
|
||||||
disabled={authValues.subStep === 'verify'}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
<TextField
|
{/* Terms */}
|
||||||
label={isEmailOnly ? 'Phone (optional)' : 'Best number to reach you'}
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 3 }}>
|
||||||
type="tel"
|
By continuing, you agree to the{' '}
|
||||||
value={authValues.phone}
|
<Box
|
||||||
onChange={(e) => handleAuthField('phone', e.target.value)}
|
component="a"
|
||||||
error={!!authErrors?.phone}
|
href="#"
|
||||||
helperText={authErrors?.phone}
|
sx={{
|
||||||
placeholder="e.g. 0412 345 678"
|
color: 'var(--fa-color-text-brand)',
|
||||||
autoComplete="tel"
|
textDecoration: 'underline',
|
||||||
inputMode="tel"
|
fontSize: 'inherit',
|
||||||
fullWidth
|
}}
|
||||||
required={!isEmailOnly}
|
|
||||||
disabled={authValues.subStep === 'verify'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
label="Contact preference"
|
|
||||||
value={authValues.contactPreference}
|
|
||||||
onChange={(e) => handleAuthField('contactPreference', e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
disabled={authValues.subStep === 'verify'}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="call_anytime">Call me anytime</MenuItem>
|
terms and conditions
|
||||||
<MenuItem value="email_preferred">Email is preferred</MenuItem>
|
</Box>
|
||||||
<MenuItem value="email_only">Only contact by email</MenuItem>
|
.
|
||||||
</TextField>
|
</Typography>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button type="submit" variant="contained" size="large" loading={loading}>
|
||||||
|
{getAuthCTALabel(authValues.subStep)}
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Collapse>
|
|
||||||
|
|
||||||
{/* Verification */}
|
|
||||||
<Collapse in={authValues.subStep === 'verify'}>
|
|
||||||
<Box sx={{ mb: 3 }} role="group" aria-label="Email verification">
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
||||||
We've sent a 6-digit code to <strong>{authValues.email}</strong>. Please
|
|
||||||
enter it below.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Verification code"
|
|
||||||
value={authValues.verificationCode}
|
|
||||||
onChange={(e) => handleAuthField('verificationCode', e.target.value)}
|
|
||||||
error={!!authErrors?.verificationCode}
|
|
||||||
helperText={
|
|
||||||
authErrors?.verificationCode || 'Check your email for the 6-digit code'
|
|
||||||
}
|
|
||||||
placeholder="Enter 6-digit code"
|
|
||||||
autoComplete="one-time-code"
|
|
||||||
inputMode="numeric"
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Collapse>
|
|
||||||
|
|
||||||
{/* Terms */}
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 3 }}>
|
|
||||||
By continuing, you agree to the{' '}
|
|
||||||
<Box
|
|
||||||
component="a"
|
|
||||||
href="#"
|
|
||||||
sx={{
|
|
||||||
color: 'var(--fa-color-text-brand)',
|
|
||||||
textDecoration: 'underline',
|
|
||||||
fontSize: 'inherit',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
terms and conditions
|
|
||||||
</Box>
|
|
||||||
.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Divider sx={{ my: 3 }} />
|
|
||||||
|
|
||||||
{/* CTA */}
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
||||||
<Button type="submit" variant="contained" size="large" loading={loading}>
|
|
||||||
{getAuthCTALabel(authValues.subStep)}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
)}
|
</DialogContent>
|
||||||
</DialogContent>
|
</Dialog>
|
||||||
</Dialog>
|
);
|
||||||
);
|
},
|
||||||
};
|
);
|
||||||
|
|
||||||
ArrangementDialog.displayName = 'ArrangementDialog';
|
ArrangementDialog.displayName = 'ArrangementDialog';
|
||||||
export default ArrangementDialog;
|
export default ArrangementDialog;
|
||||||
|
|||||||
@@ -161,6 +161,13 @@ export const CoffinDetailsStep: React.FC<CoffinDetailsStepProps> = ({
|
|||||||
|
|
||||||
{/* CTAs */}
|
{/* CTAs */}
|
||||||
<Box
|
<Box
|
||||||
|
component="form"
|
||||||
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
|
onSubmit={(e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!loading) onContinue();
|
||||||
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@@ -170,13 +177,13 @@ export const CoffinDetailsStep: React.FC<CoffinDetailsStepProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{onSaveAndExit ? (
|
{onSaveAndExit ? (
|
||||||
<Button variant="text" color="secondary" onClick={onSaveAndExit}>
|
<Button variant="text" color="secondary" onClick={onSaveAndExit} type="button">
|
||||||
Save and continue later
|
Save and continue later
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Box />
|
<Box />
|
||||||
)}
|
)}
|
||||||
<Button variant="contained" size="large" onClick={onContinue} loading={loading}>
|
<Button type="submit" variant="contained" size="large" loading={loading}>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
bgcolor: 'background.default',
|
bgcolor: 'background.default',
|
||||||
pb: 1,
|
pb: 1,
|
||||||
mx: -3,
|
mx: { xs: -2, md: -3 },
|
||||||
px: 3,
|
px: { xs: 2, md: 3 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="display3" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
<Typography variant="display3" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
||||||
|
|||||||
@@ -214,8 +214,8 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
|||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
bgcolor: 'background.default',
|
bgcolor: 'background.default',
|
||||||
pb: 1,
|
pb: 1,
|
||||||
mx: -3,
|
mx: { xs: -2, md: -3 },
|
||||||
px: 3,
|
px: { xs: 2, md: 3 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="display3" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
<Typography variant="display3" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
||||||
|
|||||||
Reference in New Issue
Block a user