Add IntroStep page (wizard step 1) + audit fixes

IntroStep: urgency-sensitive segmentation entry point. ToggleButtonGroup
for forWhom (Myself/Someone else) with progressive disclosure revealing
hasPassedAway (Yes/No) via Collapse. Auto-sets hasPassedAway="no" when
forWhom="myself". Grief-sensitive copy adapts subheading per selection.
Pure presentation — props in, callbacks out.

Audit fixes (18/20 → 20/20):
- P1: Add <main> landmark wrapper in WizardLayout (all variants)
- P2: Wrap IntroStep fields in <form> for landmark + Enter-to-submit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 14:26:53 +11:00
parent 110c62e21e
commit 2631a2e4bb
4 changed files with 410 additions and 1 deletions

View File

@@ -0,0 +1,191 @@
import React from 'react';
import Box from '@mui/material/Box';
import type { SxProps, Theme } from '@mui/material/styles';
import { WizardLayout } from '../../templates/WizardLayout';
import { ToggleButtonGroup } from '../../atoms/ToggleButtonGroup';
import { Collapse } from '../../atoms/Collapse';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Form values for the intro step */
export interface IntroStepValues {
/** Who the funeral is being arranged for */
forWhom: 'myself' | 'someone' | null;
/** Whether the person has passed away (only relevant when forWhom="someone") */
hasPassedAway: 'yes' | 'no' | null;
}
/** Field-level error messages */
export interface IntroStepErrors {
/** Error for the forWhom field */
forWhom?: string;
/** Error for the hasPassedAway field */
hasPassedAway?: string;
}
/** Props for the IntroStep page component */
export interface IntroStepProps {
/** Current form values */
values: IntroStepValues;
/** Callback when any field value changes */
onChange: (values: IntroStepValues) => void;
/** Callback when the Continue button is clicked */
onContinue: () => void;
/** Field-level validation errors */
errors?: IntroStepErrors;
/** Whether the Continue button is in a loading state */
loading?: boolean;
/** Navigation bar — passed through to WizardLayout */
navigation?: React.ReactNode;
/** Hide the help bar */
hideHelpBar?: boolean;
/** MUI sx prop for the root */
sx?: SxProps<Theme>;
}
// ─── Copy helpers ────────────────────────────────────────────────────────────
function getSubheading(values: IntroStepValues): string {
if (values.forWhom === 'someone' && values.hasPassedAway === 'yes') {
return "We'll guide you through each step. You can save your progress and come back anytime.";
}
if (values.forWhom === 'myself' || values.hasPassedAway === 'no') {
return "Explore your options and plan at your own pace. Nothing is locked in until you're ready.";
}
return "We'll guide you through arranging a funeral, step by step. You can save your progress and come back anytime.";
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Step 1 — Intro page for the FA arrangement wizard.
*
* Entry point with urgency-sensitive segmentation. User selects who
* the funeral is for, and (if arranging for someone else) whether
* that person has died.
*
* Uses the Centered Form layout variant. Progressive disclosure:
* selecting "Someone else" reveals the hasPassedAway question.
* Selecting "Myself" auto-sets hasPassedAway to "no" (pre-planning).
*
* Pure presentation component — props in, callbacks out.
* No routing, state management, or API calls.
*
* Spec: documentation/steps/steps/01_intro.yaml
*/
export const IntroStep: React.FC<IntroStepProps> = ({
values,
onChange,
onContinue,
errors,
loading = false,
navigation,
hideHelpBar,
sx,
}) => {
const handleForWhomChange = (newValue: string | null) => {
const forWhom = newValue as IntroStepValues['forWhom'];
if (forWhom === 'myself') {
// Auto-set hasPassedAway to 'no' — user is alive, pre-planning
onChange({ forWhom, hasPassedAway: 'no' });
} else {
// Reset hasPassedAway when switching to "someone"
onChange({ forWhom, hasPassedAway: null });
}
};
const handleHasPassedAwayChange = (newValue: string | null) => {
onChange({ ...values, hasPassedAway: newValue as IntroStepValues['hasPassedAway'] });
};
const showHasPassedAway = values.forWhom === 'someone';
return (
<WizardLayout variant="centered-form" navigation={navigation} hideHelpBar={hideHelpBar} sx={sx}>
{/* Page heading — receives focus on entry for screen readers */}
<Typography variant="display3" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
Let&apos;s get started
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 5 }} aria-live="polite">
{getSubheading(values)}
</Typography>
<Box
component="form"
noValidate
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
onContinue();
}}
>
{/* forWhom field */}
<Box sx={{ mb: 3 }}>
<ToggleButtonGroup
label="Who is this funeral being arranged for?"
options={[
{
value: 'myself',
label: 'Myself',
description: 'I want to plan my own funeral',
},
{
value: 'someone',
label: 'Someone else',
description: 'I am arranging for a family member or friend',
},
]}
value={values.forWhom}
onChange={handleForWhomChange}
error={!!errors?.forWhom}
helperText={errors?.forWhom}
required
fullWidth
/>
</Box>
{/* hasPassedAway field — revealed when forWhom="someone" */}
<Collapse in={showHasPassedAway}>
<Box sx={{ mb: 3 }}>
<ToggleButtonGroup
label="Has the person died?"
options={[
{
value: 'yes',
label: 'Yes',
description: 'I need to arrange a funeral now',
},
{
value: 'no',
label: 'No',
description: 'I am planning ahead',
},
]}
value={values.hasPassedAway}
onChange={handleHasPassedAwayChange}
error={!!errors?.hasPassedAway}
helperText={errors?.hasPassedAway}
required
fullWidth
/>
</Box>
</Collapse>
<Divider sx={{ my: 4 }} />
{/* CTA */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button type="submit" variant="contained" size="large" loading={loading}>
Continue
</Button>
</Box>
</Box>
</WizardLayout>
);
};
IntroStep.displayName = 'IntroStep';
export default IntroStep;