Groom wizard steps 1-15: critique/harden/polish pass
- [P0] CrematoriumStep: Fix <option> → <MenuItem> in priority select
- [P1] All form steps: Add aria-busy={loading} + loading guard on submit
- [P1] Error messages: Replace color="error" (red) with copper
(var(--fa-color-text-brand)) across ProvidersStep, PackagesStep,
VenueStep, CrematoriumStep, CemeteryStep, CoffinsStep, PaymentStep
- [P2] IntroStep: "Has the person died?" → "Has this person passed away?"
- [P2] DateTimeStep: "About the person who died" → "who has passed"
- [P2] ProvidersStep: "Showing results from X" → "X providers found"
- [P2] Empty states: Add guidance text for ProvidersStep, PackagesStep,
VenueStep, CoffinsStep empty results
Steps 4, 13, 15 passed with no issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,9 +151,10 @@ export const AdditionalServicesStep: React.FC<AdditionalServicesStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ─── Section 1: Complimentary inclusions ─── */}
|
{/* ─── Section 1: Complimentary inclusions ─── */}
|
||||||
|
|||||||
@@ -156,9 +156,10 @@ export const AuthGateStep: React.FC<AuthGateStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ─── Sub-step 1: SSO + Email ─── */}
|
{/* ─── Sub-step 1: SSO + Email ─── */}
|
||||||
|
|||||||
@@ -159,9 +159,10 @@ export const CemeteryStep: React.FC<CemeteryStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ─── Burial plot question ─── */}
|
{/* ─── Burial plot question ─── */}
|
||||||
@@ -177,7 +178,11 @@ export const CemeteryStep: React.FC<CemeteryStepProps> = ({
|
|||||||
<FormControlLabel value="no" control={<Radio />} label="No, we need to find one" />
|
<FormControlLabel value="no" control={<Radio />} label="No, we need to find one" />
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
{errors?.burialOwn && (
|
{errors?.burialOwn && (
|
||||||
<Typography variant="body2" color="error" sx={{ mt: 0.5 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 0.5, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.burialOwn}
|
{errors.burialOwn}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -201,7 +206,11 @@ export const CemeteryStep: React.FC<CemeteryStepProps> = ({
|
|||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
{errors?.burialCustom && (
|
{errors?.burialCustom && (
|
||||||
<Typography variant="body2" color="error" sx={{ mt: 0.5 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 0.5, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.burialCustom}
|
{errors.burialCustom}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -257,7 +266,11 @@ export const CemeteryStep: React.FC<CemeteryStepProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{errors?.selectedCemeteryId && (
|
{errors?.selectedCemeteryId && (
|
||||||
<Typography variant="body2" color="error" sx={{ mt: 1 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 1, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.selectedCemeteryId}
|
{errors.selectedCemeteryId}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -272,9 +272,10 @@ export const CoffinDetailsStep: React.FC<CoffinDetailsStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OptionSection
|
<OptionSection
|
||||||
|
|||||||
@@ -191,9 +191,10 @@ export const CoffinsStep: React.FC<CoffinsStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Page heading */}
|
{/* Page heading */}
|
||||||
@@ -293,11 +294,26 @@ export const CoffinsStep: React.FC<CoffinsStepProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{coffins.length === 0 && (
|
||||||
|
<Box sx={{ py: 6, textAlign: 'center', gridColumn: '1 / -1' }}>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
No coffins match your selected filters.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Try adjusting the category or price range.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Validation error */}
|
{/* Validation error */}
|
||||||
{errors?.selectedCoffinId && (
|
{errors?.selectedCoffinId && (
|
||||||
<Typography variant="body2" color="error" sx={{ mb: 2 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.selectedCoffinId}
|
{errors.selectedCoffinId}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
import FormLabel from '@mui/material/FormLabel';
|
import FormLabel from '@mui/material/FormLabel';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
@@ -151,9 +152,10 @@ export const CrematoriumStep: React.FC<CrematoriumStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ─── Crematorium selection ─── */}
|
{/* ─── Crematorium selection ─── */}
|
||||||
@@ -226,7 +228,11 @@ export const CrematoriumStep: React.FC<CrematoriumStepProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{errors?.selectedCrematoriumId && (
|
{errors?.selectedCrematoriumId && (
|
||||||
<Typography variant="body2" color="error" sx={{ mt: 1 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 1, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.selectedCrematoriumId}
|
{errors.selectedCrematoriumId}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -250,7 +256,11 @@ export const CrematoriumStep: React.FC<CrematoriumStepProps> = ({
|
|||||||
<FormControlLabel value="no" control={<Radio />} label="No" />
|
<FormControlLabel value="no" control={<Radio />} label="No" />
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
{errors?.attend && (
|
{errors?.attend && (
|
||||||
<Typography variant="body2" color="error" sx={{ mt: 0.5 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 0.5, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.attend}
|
{errors.attend}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -268,9 +278,9 @@ export const CrematoriumStep: React.FC<CrematoriumStepProps> = ({
|
|||||||
sx={{ mb: 3 }}
|
sx={{ mb: 3 }}
|
||||||
>
|
>
|
||||||
{priorityOptions.map((opt) => (
|
{priorityOptions.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value}>
|
<MenuItem key={opt.value} value={opt.value}>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</option>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const DateTimeStep: React.FC<DateTimeStepProps> = ({
|
|||||||
onChange({ ...values, [field]: value });
|
onChange({ ...values, [field]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const personSectionHeading = isAtNeed ? 'About the person who died' : 'About the person';
|
const personSectionHeading = isAtNeed ? 'About the person who has passed' : 'About the person';
|
||||||
|
|
||||||
const schedulingHeading = isAtNeed
|
const schedulingHeading = isAtNeed
|
||||||
? 'When are you hoping to have the service?'
|
? 'When are you hoping to have the service?'
|
||||||
@@ -178,9 +178,10 @@ export const DateTimeStep: React.FC<DateTimeStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ─── Section 1: About the person ─── */}
|
{/* ─── Section 1: About the person ─── */}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function getSubheading(values: IntroStepValues): string {
|
|||||||
*
|
*
|
||||||
* Entry point with urgency-sensitive segmentation. User selects who
|
* Entry point with urgency-sensitive segmentation. User selects who
|
||||||
* the funeral is for, and (if arranging for someone else) whether
|
* the funeral is for, and (if arranging for someone else) whether
|
||||||
* that person has died.
|
* that person has passed away.
|
||||||
*
|
*
|
||||||
* Uses the Centered Form layout variant. Progressive disclosure:
|
* Uses the Centered Form layout variant. Progressive disclosure:
|
||||||
* selecting "Someone else" reveals the hasPassedAway question.
|
* selecting "Someone else" reveals the hasPassedAway question.
|
||||||
@@ -117,9 +117,10 @@ export const IntroStep: React.FC<IntroStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* forWhom field */}
|
{/* forWhom field */}
|
||||||
@@ -151,7 +152,7 @@ export const IntroStep: React.FC<IntroStepProps> = ({
|
|||||||
<Collapse in={showHasPassedAway}>
|
<Collapse in={showHasPassedAway}>
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
label="Has the person died?"
|
label="Has this person passed away?"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 'yes',
|
value: 'yes',
|
||||||
|
|||||||
@@ -221,7 +221,11 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
|||||||
|
|
||||||
{/* Error message */}
|
{/* Error message */}
|
||||||
{error && (
|
{error && (
|
||||||
<Typography variant="body2" color="error" sx={{ mb: 2 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -262,9 +266,12 @@ export const PackagesStep: React.FC<PackagesStepProps> = ({
|
|||||||
|
|
||||||
{packages.length === 0 && (
|
{packages.length === 0 && (
|
||||||
<Box sx={{ py: 6, textAlign: 'center' }}>
|
<Box sx={{ py: 6, textAlign: 'center' }}>
|
||||||
<Typography variant="body1" color="text.secondary">
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
No packages match the selected budget range.
|
No packages match the selected budget range.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Try selecting "All packages" to see the full range.
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -161,9 +161,10 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onConfirmPayment();
|
if (!loading) onConfirmPayment();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ─── Payment plan ─── */}
|
{/* ─── Payment plan ─── */}
|
||||||
@@ -229,7 +230,11 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({
|
|||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
{errors?.card && (
|
{errors?.card && (
|
||||||
<Typography variant="body2" color="error" sx={{ mt: 1 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mt: 1, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.card}
|
{errors.card}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -310,7 +315,11 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({
|
|||||||
sx={{ mb: 1, alignItems: 'flex-start', '& .MuiCheckbox-root': { pt: 0.5 } }}
|
sx={{ mb: 1, alignItems: 'flex-start', '& .MuiCheckbox-root': { pt: 0.5 } }}
|
||||||
/>
|
/>
|
||||||
{errors?.termsAccepted && (
|
{errors?.termsAccepted && (
|
||||||
<Typography variant="body2" color="error" sx={{ mb: 2, ml: 4 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mb: 2, ml: 4, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.termsAccepted}
|
{errors.termsAccepted}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -188,12 +188,16 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
sx={{ mb: 2, display: 'block' }}
|
sx={{ mb: 2, display: 'block' }}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
Showing results from {providers.length} provider{providers.length !== 1 ? 's' : ''}
|
{providers.length} provider{providers.length !== 1 ? 's' : ''} found
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Error message */}
|
{/* Error message */}
|
||||||
{error && (
|
{error && (
|
||||||
<Typography variant="body2" color="error" sx={{ mb: 2 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -230,9 +234,12 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body1" color="text.secondary">
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
No providers found matching your search.
|
No providers found matching your search.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Try adjusting your search or clearing filters.
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -207,9 +207,10 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
noValidate
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
onSubmit={(e: React.FormEvent) => {
|
onSubmit={(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContinue();
|
if (!loading) onContinue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ─── Search + Filters ─── */}
|
{/* ─── Search + Filters ─── */}
|
||||||
@@ -276,11 +277,26 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{venues.length === 0 && (
|
||||||
|
<Box sx={{ py: 6, textAlign: 'center' }}>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
No venues found in this area.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Try adjusting your search or clearing filters.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Validation error */}
|
{/* Validation error */}
|
||||||
{errors?.selectedVenueId && (
|
{errors?.selectedVenueId && (
|
||||||
<Typography variant="body2" color="error" sx={{ mb: 2 }} role="alert">
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{errors.selectedVenueId}
|
{errors.selectedVenueId}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user