import React from 'react'; import Box from '@mui/material/Box'; import Select, { type SelectChangeEvent } from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; import OutlinedInput from '@mui/material/OutlinedInput'; import InputAdornment from '@mui/material/InputAdornment'; import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import type { SxProps, Theme } from '@mui/material/styles'; import { Typography } from '../../atoms/Typography'; import { Button } from '../../atoms/Button'; import { Divider } from '../../atoms/Divider'; // ─── Types ─────────────────────────────────────────────────────────────────── type Status = 'immediate' | 'preplanning'; type FuneralType = | 'cremation-funeral' | 'cremation-only' | 'burial-funeral' | 'graveside-only' | 'water-cremation' | 'show-all'; /** Search parameters returned on form submission */ export interface FuneralFinderV3SearchParams { /** User's current situation */ status: Status; /** Type of funeral selected (defaults to show-all if not chosen) */ funeralType: FuneralType; /** Suburb or postcode */ location: string; } /** Props for the FuneralFinder v3 organism */ export interface FuneralFinderV3Props { /** Called when the user submits with valid data */ onSearch?: (params: FuneralFinderV3SearchParams) => void; /** Shows loading state on the CTA */ loading?: boolean; /** Optional heading override */ heading?: string; /** Optional subheading override */ subheading?: string; /** MUI sx override for the root container */ sx?: SxProps; } // ─── Options ───────────────────────────────────────────────────────────────── const STATUS_OPTIONS: { key: Status; title: string; description: string }[] = [ { key: 'immediate', title: 'Immediate Need', description: 'A recent loss or one expected soon', }, { key: 'preplanning', title: 'Pre-planning', description: 'Planning ahead for yourself or a loved one', }, ]; const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [ { value: 'cremation-funeral', label: 'Cremation with funeral' }, { value: 'cremation-only', label: 'Cremation only (no funeral, no attendance)', }, { value: 'burial-funeral', label: 'Burial with funeral' }, { value: 'graveside-only', label: 'Graveside burial only' }, { value: 'water-cremation', label: 'Water cremation (QLD only)' }, { value: 'show-all', label: 'Show all options' }, ]; /** Hoisted outside component to avoid re-creation on render */ const selectPlaceholder = ( Select funeral type ); // ─── Sub-components ────────────────────────────────────────────────────────── /** Uppercase section label — overline style */ function SectionLabel({ children, id, }: { children: React.ReactNode; id?: string; }) { return ( {children} ); } /** Tappable status card with roving tabindex support for radiogroup pattern */ const StatusCard = React.forwardRef< HTMLButtonElement, { title: string; description: string; selected: boolean; onClick: () => void; tabIndex?: number; onKeyDown?: React.KeyboardEventHandler; } >(({ title, description, selected, onClick, tabIndex, onKeyDown }, ref) => ( {title} {description} )); StatusCard.displayName = 'StatusCard'; // ─── Shared field styles ──────────────────────────────────────────────────── /** Standard outlined fields — white bg, neutral border, no focus ring per design spec */ const fieldBaseSx = { width: '100%', bgcolor: 'var(--fa-color-surface-default, #fff)', borderRadius: 'var(--fa-border-radius-md, 8px)', '& .MuiOutlinedInput-notchedOutline': { borderColor: 'var(--fa-color-border-default, #E8E8E8)', borderRadius: 'var(--fa-border-radius-md, 8px)', }, '&:hover:not(.Mui-disabled) .MuiOutlinedInput-notchedOutline': { borderColor: 'var(--fa-color-border-strong, #BFBFBF)', }, '&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: 'var(--fa-color-border-brand, #BA834E)', borderWidth: 1, }, '&.Mui-focused': { boxShadow: 'none', }, }; const fieldInputStyles = { py: '14px', px: 2, fontSize: '1rem', fontFamily: 'var(--fa-font-family-body)', }; const selectMenuProps = { PaperProps: { sx: { mt: 0.5, borderRadius: 'var(--fa-border-radius-md, 8px)', boxShadow: 'var(--fa-shadow-md)', '& .MuiMenuItem-root': { py: 1.5, px: 2, minHeight: 44, fontSize: '0.9375rem', fontFamily: 'var(--fa-font-family-body)', whiteSpace: 'normal' as const, '&:hover': { bgcolor: 'var(--fa-color-surface-subtle)' }, '&.Mui-selected': { bgcolor: 'var(--fa-color-surface-warm)', fontWeight: 600, '&:hover': { bgcolor: 'var(--fa-color-surface-warm)' }, }, }, }, }, }; // ─── Component ─────────────────────────────────────────────────────────────── /** * Hero search widget v3 — clean vertical form with status cards. * * Two tappable status cards (Immediate Need / Pre-planning), a funeral type * dropdown, a location input, and a CTA. Standard card container with * overline section labels. CTA is always active — * clicking it with missing required fields scrolls to the first gap. * * Required fields: status + location (min 3 chars). * Funeral type defaults to "show all" if not selected. */ export const FuneralFinderV3 = React.forwardRef< HTMLDivElement, FuneralFinderV3Props >((props, ref) => { const { onSearch, loading = false, heading = 'Find funeral directors near you', subheading = "Tell us what you need and we\u2019ll show options in your area.", sx, } = props; // ─── IDs for aria-labelledby ────────────────────────────── const id = React.useId(); const statusLabelId = `${id}-status`; const funeralTypeLabelId = `${id}-funeral-type`; const locationLabelId = `${id}-location`; // ─── State ─────────────────────────────────────────────── const [status, setStatus] = React.useState('immediate'); const [funeralType, setFuneralType] = React.useState(''); const [location, setLocation] = React.useState(''); const [errors, setErrors] = React.useState<{ status?: boolean; location?: boolean; }>({}); // ─── Refs ──────────────────────────────────────────────── const statusSectionRef = React.useRef(null); const locationSectionRef = React.useRef(null); const locationInputRef = React.useRef(null); const cardRefs = React.useRef<(HTMLButtonElement | null)[]>([null, null]); // ─── Clear errors as fields are filled ─────────────────── const prevStatus = React.useRef(status); React.useEffect(() => { if (status !== prevStatus.current) { prevStatus.current = status; if (status && errors.status) { setErrors((prev) => ({ ...prev, status: false })); } } }, [status, errors.status]); const prevLocation = React.useRef(location); React.useEffect(() => { if (location !== prevLocation.current) { prevLocation.current = location; if (location.trim().length >= 3 && errors.location) { setErrors((prev) => ({ ...prev, location: false })); } } }, [location, errors.location]); // ─── Radiogroup keyboard nav (WAI-ARIA pattern) ────────── const activeStatusIndex = status ? STATUS_OPTIONS.findIndex((o) => o.key === status) : 0; const handleStatusKeyDown = (e: React.KeyboardEvent) => { const isNext = e.key === 'ArrowRight' || e.key === 'ArrowDown'; const isPrev = e.key === 'ArrowLeft' || e.key === 'ArrowUp'; if (!isNext && !isPrev) return; e.preventDefault(); const current = cardRefs.current.indexOf( e.target as HTMLButtonElement, ); if (current === -1) return; const next = isNext ? Math.min(current + 1, STATUS_OPTIONS.length - 1) : Math.max(current - 1, 0); if (next !== current) { cardRefs.current[next]?.focus(); setStatus(STATUS_OPTIONS[next].key); } }; // ─── Handlers ──────────────────────────────────────────── const handleFuneralType = (e: SelectChangeEvent) => { setFuneralType(e.target.value as FuneralType); }; const handleSubmit = () => { if (!status) { setErrors({ status: true }); statusSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center', }); return; } if (location.trim().length < 3) { setErrors({ location: true }); locationSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center', }); locationInputRef.current?.focus(); return; } setErrors({}); onSearch?.({ status, funeralType: funeralType || 'show-all', location: location.trim(), }); }; // ─── Render ────────────────────────────────────────────── return ( {/* ── Header ──────────────────────────────────────────── */} {heading} {subheading} {/* ── How can we help ─────────────────────────────────── */} How Can We Help {STATUS_OPTIONS.map((opt, i) => ( { cardRefs.current[i] = el; }} title={opt.title} description={opt.description} selected={status === opt.key} onClick={() => setStatus(opt.key)} tabIndex={i === activeStatusIndex ? 0 : -1} onKeyDown={handleStatusKeyDown} /> ))} {errors.status && ( Please select how we can help )} {/* ── Funeral Type ────────────────────────────────────── */} Funeral Type {/* ── Location ────────────────────────────────────────── */} Location setLocation(e.target.value)} placeholder="Enter suburb or postcode" inputRef={locationInputRef} startAdornment={ } onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }} sx={{ ...fieldBaseSx, '& .MuiOutlinedInput-input': { ...fieldInputStyles, '&::placeholder': { color: 'var(--fa-color-text-disabled)', opacity: 1, }, }, }} inputProps={{ 'aria-labelledby': locationLabelId, 'aria-required': true, }} /> {errors.location && ( Please enter a suburb or postcode )} {/* ── CTA ─────────────────────────────────────────────── */} Free to use · No obligation ); }); FuneralFinderV3.displayName = 'FuneralFinderV3'; export default FuneralFinderV3;