diff --git a/src/components/molecules/MapPopup/MapPopup.stories.tsx b/src/components/molecules/MapPopup/MapPopup.stories.tsx index 9bf5631..2dfd3af 100644 --- a/src/components/molecules/MapPopup/MapPopup.stories.tsx +++ b/src/components/molecules/MapPopup/MapPopup.stories.tsx @@ -20,6 +20,9 @@ const meta: Meta = { values: [{ name: 'map', value: '#E5E3DF' }], }, }, + argTypes: { + onClick: { action: 'clicked' }, + }, }; export default meta; @@ -34,7 +37,6 @@ export const VerifiedProvider: Story = { location: 'Wollongong', rating: 4.8, verified: true, - onViewDetails: () => {}, }, }; @@ -44,7 +46,6 @@ export const UnverifiedProvider: Story = { name: 'Smith & Sons Funeral Services', price: 1200, location: 'Sutherland', - onViewDetails: () => {}, }, }; @@ -56,23 +57,31 @@ export const Venue: Story = { price: 450, location: 'Albany Creek', capacity: 120, - onViewDetails: () => {}, }, }; -/** Minimal — just name and view details */ +/** Long name — truncated at 1 line, tooltip on hover */ +export const LongName: Story = { + args: { + name: 'Botanical Funerals by Ian Allison — Sustainable & Eco-Friendly Services', + imageUrl: IMG_PROVIDER, + price: 1200, + location: 'Northern Beaches', + verified: true, + }, +}; + +/** Minimal — just name */ export const Minimal: Story = { args: { name: 'Local Funeral Provider', - onViewDetails: () => {}, }, }; -/** No view details link — display only */ -export const DisplayOnly: Story = { +/** Verified without image — inline verified indicator */ +export const VerifiedNoImage: Story = { args: { name: 'H.Parsons Funeral Directors', - imageUrl: IMG_PROVIDER, price: 900, location: 'Wollongong', verified: true, @@ -87,7 +96,6 @@ export const CustomPriceLabel: Story = { priceLabel: 'Price on application', location: 'Sydney CBD', verified: true, - onViewDetails: () => {}, }, }; @@ -122,7 +130,7 @@ export const WithPin: Story = { location="Wollongong" rating={4.8} verified - onViewDetails={() => {}} + onClick={() => {}} /> diff --git a/src/components/molecules/MapPopup/MapPopup.tsx b/src/components/molecules/MapPopup/MapPopup.tsx index 576a37a..208d72c 100644 --- a/src/components/molecules/MapPopup/MapPopup.tsx +++ b/src/components/molecules/MapPopup/MapPopup.tsx @@ -1,14 +1,13 @@ import React from 'react'; import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; +import Tooltip from '@mui/material/Tooltip'; import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; import StarRoundedIcon from '@mui/icons-material/StarRounded'; import PeopleOutlinedIcon from '@mui/icons-material/PeopleOutlined'; +import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined'; import type { SxProps, Theme } from '@mui/material/styles'; import { Typography } from '../../atoms/Typography'; -import { Badge } from '../../atoms/Badge'; -import { Link } from '../../atoms/Link'; -import type { BadgeProps } from '../../atoms/Badge/Badge'; // ─── Types ────────────────────────────────────────────────────────────────── @@ -18,7 +17,7 @@ export interface MapPopupProps { name: string; /** Hero image URL */ imageUrl?: string; - /** Price in dollars */ + /** Price in dollars — shown as "From $X" */ price?: number; /** Custom price label (e.g. "POA") — overrides formatted price */ priceLabel?: string; @@ -28,13 +27,11 @@ export interface MapPopupProps { rating?: number; /** Venue capacity */ capacity?: number; - /** Whether this provider is verified */ + /** Whether this provider is verified — shows icon badge in image */ verified?: boolean; - /** Badge colour for the verified badge */ - badgeColor?: BadgeProps['color']; - /** "View details" click handler */ - onViewDetails?: () => void; - /** MUI sx prop for the root Paper */ + /** Click handler — entire card is clickable */ + onClick?: () => void; + /** MUI sx prop for the root element */ sx?: SxProps; } @@ -50,15 +47,18 @@ const NUB_SIZE = 8; * Map popup card for the FA design system. * * Floating card anchored to a MapPin on click. Shows a compact - * preview of a provider or venue — image, name, price, meta row, - * and a "View details" link. A downward nub visually connects the - * popup to the pin below. + * preview of a provider or venue — image, name, meta, and price. + * The entire card is clickable to navigate to the provider/venue. + * + * Content hierarchy matches MiniCard: **title → meta → price**. + * Truncated names show a tooltip on hover. Verified providers + * show an icon-only badge floating in the image. * * Designed for use as a custom popup in Mapbox GL / Google Maps. * The parent map container handles positioning; this component * handles content and styling only. * - * Composes: Paper + Typography + Badge + Link. + * Composes: Paper + Typography + Tooltip. * * Usage: * ```tsx @@ -69,7 +69,7 @@ const NUB_SIZE = 8; * location="Wollongong" * rating={4.8} * verified - * onViewDetails={() => selectProvider(id)} + * onClick={() => selectProvider(id)} * /> * ``` */ @@ -84,8 +84,7 @@ export const MapPopup = React.forwardRef( rating, capacity, verified = false, - badgeColor = 'brand', - onViewDetails, + onClick, sx, }, ref, @@ -93,16 +92,52 @@ export const MapPopup = React.forwardRef( const hasMeta = location != null || rating != null || capacity != null; const hasPrice = price != null || priceLabel != null; + // Detect name truncation for tooltip + const nameRef = React.useRef(null); + const [isTruncated, setIsTruncated] = React.useState(false); + + React.useEffect(() => { + const el = nameRef.current; + if (el) { + setIsTruncated(el.scrollHeight > el.clientHeight + 1); + } + }, [name]); + return ( { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + } + : undefined + } + aria-label={onClick ? `View ${name}` : undefined} sx={[ { display: 'inline-flex', flexDirection: 'column', alignItems: 'center', - // Offset the popup upward so the nub sits on top of the pin filter: 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))', + cursor: onClick ? 'pointer' : 'default', + transition: 'transform 150ms ease-in-out', + '&:hover': onClick + ? { + transform: 'scale(1.02)', + } + : undefined, + '&:focus-visible': { + outline: '2px solid var(--fa-color-interactive-focus)', + outlineOffset: '2px', + borderRadius: 'var(--fa-card-border-radius-default)', + }, }, ...(Array.isArray(sx) ? sx : [sx]), ]} @@ -130,45 +165,48 @@ export const MapPopup = React.forwardRef( backgroundColor: 'var(--fa-color-neutral-100)', }} > - {/* Verified badge inside image */} + {/* Verified icon badge — floating top-right */} {verified && ( - - - Verified - - + + + + + )} )} {/* ── Content ── */} - {/* Name */} - - {name} - + {/* 1. Name — with tooltip when truncated */} + + + {name} + + - {/* Price */} - {hasPrice && ( - - {priceLabel ? ( - - {priceLabel} - - ) : ( - <> - - From - - - ${price!.toLocaleString('en-AU')} - - - )} - - )} - - {/* Meta row */} + {/* 2. Meta row */} {hasMeta && ( {location && ( @@ -209,26 +247,34 @@ export const MapPopup = React.forwardRef( )} - {/* Verified badge (no image fallback) */} - {verified && !imageUrl && ( - - Verified - + {/* 3. Price */} + {hasPrice && ( + + {priceLabel ? ( + + {priceLabel} + + ) : ( + <> + + From + + + ${price!.toLocaleString('en-AU')} + + + )} + )} - {/* View details link */} - {onViewDetails && ( - { - e.stopPropagation(); - onViewDetails(); - }} - sx={{ mt: 0.5, alignSelf: 'flex-start' }} - > - View details - + {/* Verified indicator (no-image fallback) */} + {verified && !imageUrl && ( + + + + Verified + + )}