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:
191
src/components/pages/IntroStep/IntroStep.tsx
Normal file
191
src/components/pages/IntroStep/IntroStep.tsx
Normal 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'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;
|
||||
Reference in New Issue
Block a user