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:
@@ -157,385 +157,412 @@ function getAuthCTALabel(subStep: AuthSubStep): string {
|
||||
*
|
||||
* Pure presentation component — props in, callbacks out.
|
||||
*/
|
||||
export const ArrangementDialog: React.FC<ArrangementDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
step,
|
||||
onStepChange,
|
||||
provider,
|
||||
selectedPackage,
|
||||
nextSteps = DEFAULT_NEXT_STEPS,
|
||||
isPrePlanning = false,
|
||||
onExplore,
|
||||
authValues,
|
||||
onAuthChange,
|
||||
authErrors,
|
||||
onGoogleSSO,
|
||||
onMicrosoftSSO,
|
||||
onContinue,
|
||||
loading = false,
|
||||
sx,
|
||||
}) => {
|
||||
const isEmailOnly = authValues.contactPreference === 'email_only';
|
||||
export const ArrangementDialog = React.forwardRef<HTMLDivElement, ArrangementDialogProps>(
|
||||
(
|
||||
{
|
||||
open,
|
||||
onClose,
|
||||
step,
|
||||
onStepChange,
|
||||
provider,
|
||||
selectedPackage,
|
||||
nextSteps = DEFAULT_NEXT_STEPS,
|
||||
isPrePlanning = false,
|
||||
onExplore,
|
||||
authValues,
|
||||
onAuthChange,
|
||||
authErrors,
|
||||
onGoogleSSO,
|
||||
onMicrosoftSSO,
|
||||
onContinue,
|
||||
loading = false,
|
||||
sx,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isEmailOnly = authValues.contactPreference === 'email_only';
|
||||
const titleRef = React.useRef<HTMLHeadingElement>(null);
|
||||
|
||||
const handleAuthField = (field: keyof AuthValues, value: string) => {
|
||||
onAuthChange({ ...authValues, [field]: value });
|
||||
};
|
||||
// Focus the dialog title when step changes
|
||||
React.useEffect(() => {
|
||||
if (open && titleRef.current) {
|
||||
titleRef.current.focus();
|
||||
}
|
||||
}, [step, open]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
scroll="body"
|
||||
aria-labelledby="arrangement-dialog-title"
|
||||
sx={sx}
|
||||
PaperProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
>
|
||||
{/* ─── Header ─── */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 3,
|
||||
pt: 2.5,
|
||||
pb: 1,
|
||||
const handleAuthField = (field: keyof AuthValues, value: string) => {
|
||||
onAuthChange({ ...authValues, [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ref={ref}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
scroll="body"
|
||||
aria-labelledby="arrangement-dialog-title"
|
||||
sx={sx}
|
||||
PaperProps={{
|
||||
sx: { borderRadius: 2 },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{step === 'auth' && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onStepChange('preview')}
|
||||
aria-label="Back to preview"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography id="arrangement-dialog-title" variant="h5">
|
||||
{step === 'preview' ? 'Your selected package' : 'Save your plan'}
|
||||
</Typography>
|
||||
{/* ─── Header ─── */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 3,
|
||||
pt: 2.5,
|
||||
pb: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{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>
|
||||
<IconButton size="small" onClick={onClose} aria-label="Close">
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<DialogContent sx={{ px: 3, pb: 3 }}>
|
||||
{/* ═══════════ Step 1: Preview ═══════════ */}
|
||||
{step === 'preview' && (
|
||||
<Box>
|
||||
{/* Provider */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<ProviderCardCompact
|
||||
name={provider.name}
|
||||
location={provider.location}
|
||||
imageUrl={provider.imageUrl}
|
||||
rating={provider.rating}
|
||||
reviewCount={provider.reviewCount}
|
||||
/>
|
||||
</Box>
|
||||
{/* Screen reader step announcement */}
|
||||
<Box
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
sx={{ position: 'absolute', width: 1, height: 1, overflow: 'hidden' }}
|
||||
>
|
||||
{step === 'preview' ? 'Viewing package preview' : 'Create your account'}
|
||||
</Box>
|
||||
|
||||
{/* Package summary */}
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'var(--fa-color-brand-50)',
|
||||
borderRadius: 2,
|
||||
p: 2.5,
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<DialogContent sx={{ px: 3, pb: 3 }}>
|
||||
{/* ═══════════ Step 1: Preview ═══════════ */}
|
||||
{step === 'preview' && (
|
||||
<Box>
|
||||
{/* Provider */}
|
||||
<Box sx={{ 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
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline',
|
||||
mb: 2,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 2,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{selectedPackage.name}</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
${(selectedPackage.total ?? selectedPackage.price).toLocaleString('en-AU')}
|
||||
</Typography>
|
||||
{isPrePlanning && onExplore && (
|
||||
<Button variant="outlined" color="secondary" size="large" onClick={onExplore}>
|
||||
Explore other options
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => onStepChange('auth')}
|
||||
loading={loading}
|
||||
>
|
||||
Continue with this package
|
||||
</Button>
|
||||
</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 */}
|
||||
{/* ═══════════ Step 2: Auth ═══════════ */}
|
||||
{step === 'auth' && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 2,
|
||||
justifyContent: 'flex-end',
|
||||
component="form"
|
||||
noValidate
|
||||
aria-busy={loading}
|
||||
onSubmit={(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!loading) onContinue();
|
||||
}}
|
||||
>
|
||||
{isPrePlanning && onExplore && (
|
||||
<Button variant="outlined" color="secondary" size="large" onClick={onExplore}>
|
||||
Explore other options
|
||||
</Button>
|
||||
)}
|
||||
<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 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>
|
||||
</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'}>
|
||||
{/* SSO buttons */}
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2.5, mb: 3 }}
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, mb: 3 }}
|
||||
role="group"
|
||||
aria-label="Your details"
|
||||
aria-label="Sign in options"
|
||||
>
|
||||
<Typography variant="labelLg" component="h2" sx={{ mb: 0.5 }}>
|
||||
A few details to save your plan
|
||||
</Typography>
|
||||
<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>
|
||||
</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
|
||||
label="First name"
|
||||
value={authValues.firstName}
|
||||
onChange={(e) => handleAuthField('firstName', e.target.value)}
|
||||
error={!!authErrors?.firstName}
|
||||
helperText={authErrors?.firstName}
|
||||
autoComplete="given-name"
|
||||
label={isEmailOnly ? 'Phone (optional)' : 'Best number to reach you'}
|
||||
type="tel"
|
||||
value={authValues.phone}
|
||||
onChange={(e) => handleAuthField('phone', e.target.value)}
|
||||
error={!!authErrors?.phone}
|
||||
helperText={authErrors?.phone}
|
||||
placeholder="e.g. 0412 345 678"
|
||||
autoComplete="tel"
|
||||
inputMode="tel"
|
||||
fullWidth
|
||||
required
|
||||
required={!isEmailOnly}
|
||||
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"
|
||||
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>
|
||||
<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
|
||||
required
|
||||
disabled={authValues.subStep === 'verify'}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
|
||||
<TextField
|
||||
label={isEmailOnly ? 'Phone (optional)' : 'Best number to reach you'}
|
||||
type="tel"
|
||||
value={authValues.phone}
|
||||
onChange={(e) => handleAuthField('phone', e.target.value)}
|
||||
error={!!authErrors?.phone}
|
||||
helperText={authErrors?.phone}
|
||||
placeholder="e.g. 0412 345 678"
|
||||
autoComplete="tel"
|
||||
inputMode="tel"
|
||||
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'}
|
||||
{/* 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',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
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>
|
||||
</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>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ArrangementDialog.displayName = 'ArrangementDialog';
|
||||
export default ArrangementDialog;
|
||||
|
||||
Reference in New Issue
Block a user