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:
2026-03-30 08:22:06 +11:00
parent 4ff18d6a9f
commit ae4bcef4c9
5 changed files with 516 additions and 464 deletions

View File

@@ -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;

View File

@@ -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&apos;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&apos;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&apos;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&apos;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;

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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}>