import { useState, type ComponentProps } from "react"; import { AddressInputBanner } from "./index"; import { Button } from "@shared/contentful/blocks/button"; import { DocsPage } from "@shared/stories/DocsTemplate"; import type { Meta, StoryObj } from "@storybook/react-vite"; // Sample address suggestions shown once the user types 3+ characters. const ADDRESS_SUGGESTIONS = [ "123 Dale Springs, Apt B, Binsport, NC, 17032", "123 Dale Springs, Apt C, Binsport, NC, 17032", "123 Dale Avenue, Binsport, NC, 17033", ]; type AddressBarProps = { buttonVariant?: "primary_brand" | "primary_inverse" | "secondary"; ctaLabel?: string; desktopPrompt?: string; mobilePrompt?: string; placeholder?: string; promptTextClass?: string; }; // Shared address bar (title + location input + check-plans button) used across // the stories. The button variant is configurable so themed variants (e.g. // Yellow) can render an inverse/navy CTA. const AddressBar = ({ buttonVariant = "primary_brand", ctaLabel = "Check plans", desktopPrompt = "To see plans for your address, check availability", mobilePrompt = "Show me the best offers in my area", placeholder = "123 Dale Springs, Apt B, Binsport, NC, 17032", promptTextClass = "text-text", }: AddressBarProps) => { const [query, setQuery] = useState(""); const showResults = query.trim().length >= 3; // Mobile "Go" button reuses the BrandButton color tokens but is rendered as a // plain 56x56 button so it can honor the fixed Figma width (the shared Button // has a baked-in 60px horizontal padding that cannot be overridden here). const goVariantClass = buttonVariant === "primary_inverse" ? "bg-bg-fill-inverse text-text-inverse" : buttonVariant === "secondary" ? "border border-border-secondary-on-bg-fill bg-bg-fill-secondary text-text" : "bg-bg-fill-brand text-text-brand-on-bg-fill"; return (
{mobilePrompt}
setQuery(event.target.value)} placeholder={placeholder} className="body2 w-full min-w-0 bg-transparent text-text outline-none placeholder:text-input-text-placeholder" />
{showResults && (
{ADDRESS_SUGGESTIONS.map(address => ( ))}
)}
); }; // Story args extend the banner props with the editable mock fields so the // address bar text and CTA can be controlled from the Storybook controls // instead of being hardcoded. type StoryArgs = ComponentProps & { ctaLabel?: string; ctaButtonVariant?: "primary_brand" | "primary_inverse" | "secondary"; mobilePrompt?: string; placeholder?: string; }; const meta: Meta = { title: "Contentful Blocks/AddressInputBanner", component: AddressInputBanner, tags: ["autodocs"], // Provide a tall, scrollable canvas so the sticky banner's `navHeight` offset // is observable — CSS `position: sticky` only applies its `top` value while // scrolling within a container taller than the viewport. decorators: [ Story => (

Scroll to see the banner stick at the top (offset by navHeight).

), ], parameters: { layout: "fullscreen", docs: { page: DocsPage, description: { component: "A sticky address input banner component that supports multiple themed variants and an optional CTA. It can be shown/hidden dynamically and adjusts positioning based on the navigation height.", }, }, }, argTypes: { title: { control: { type: "text" }, description: "Title shown as the desktop heading next to the input", }, variant: { control: { type: "select" }, options: ["yellow", "white", "navy", "green"], description: "Themed color variant of the banner", }, navHeight: { control: { type: "number" }, description: "Navigation height used to offset the sticky position", }, isVisible: { control: { type: "boolean" }, description: "Whether the banner is rendered", }, ctaLabel: { control: { type: "text" }, description: "CTA button label", }, ctaButtonVariant: { control: { type: "select" }, options: ["primary_brand", "primary_inverse", "secondary"], description: "CTA button variant", }, mobilePrompt: { control: { type: "text" }, description: "Prompt text shown on mobile", }, placeholder: { control: { type: "text" }, description: "Address input placeholder text", }, entryName: { table: { disable: true }, }, cta: { table: { disable: true }, }, }, args: { title: "To see plans for your address, check availability", variant: "yellow", isVisible: true, navHeight: 0, ctaLabel: "Check plans", mobilePrompt: "Show me the best offers in my area", placeholder: "123 Dale Springs, Apt B, Binsport, NC, 17032", }, render: ({ title, ctaLabel, ctaButtonVariant, mobilePrompt, placeholder, renderCheckPlans, cta, ...args }) => { // Dark-background variants need light prompt text for contrast. const promptTextClass = args.variant === "navy" || args.variant === "green" ? "text-white" : "text-text"; // Derive the CTA button variant from the banner variant so it adapts when // the variant control changes. An explicit ctaButtonVariant still overrides. const effectiveButtonVariant = ctaButtonVariant ?? (args.variant === "yellow" || args.variant === "green" ? "primary_inverse" : "primary_brand"); // Keep the cta's buttonVariant in sync with the effective variant so the // ctaButtonVariant control also drives stories that supply their own cta // and renderCheckPlans (e.g. WithCta). const effectiveCta = cta ? { ...cta, buttonVariant: effectiveButtonVariant } : cta; return ( ( )) } /> ); }, }; export default meta; type Story = StoryObj; // Default banner using all the default entries from meta args export const Default: Story = { args: {}, parameters: { docs: { description: { story: "Default AddressInputBanner rendered with all the default entries (yellow variant, visible, with the address bar CTA).", }, }, }, }; // Yellow variant export const Yellow: Story = { args: { variant: "yellow", }, }; // White variant export const White: Story = { args: { variant: "white", }, }; // Navy variant export const Navy: Story = { args: { variant: "navy", }, }; // Green variant export const Green: Story = { args: { variant: "green", }, }; // Hidden banner renders nothing export const Hidden: Story = { args: { isVisible: false, }, parameters: { docs: { description: { story: "When isVisible is false, the banner renders nothing to the DOM.", }, }, }, }; // Banner supplied with an explicit cta prop. It falls back to the default // address bar render, so it looks consistent with the other variant stories // while still exercising the cta overrides. export const WithCta: Story = { args: { cta: { buttonLabel: "Check plans", buttonVariant: "primary_brand", showButtonAs: "solid", }, }, parameters: { docs: { description: { story: "AddressInputBanner rendered with an explicit `cta` prop. It uses the default address bar render, so it looks consistent with the other variant stories while still exercising the `cta` overrides.", }, }, }, }; // Demonstrates the sticky positioning offset with a non-zero navHeight value. // A mock fixed nav bar is layered on top of the shared scrollable canvas so the // banner can be seen sticking directly below it once the page scrolls. export const CustomNavHeight: Story = { args: { navHeight: 80, }, decorators: [ (Story, context) => { // Keep the mock nav height/label in sync with the navHeight control. const navHeight = (context.args.navHeight as number | undefined) ?? 80; return (
{/* Mock fixed navigation bar that matches the navHeight control */}
{`Mock Navigation (${navHeight}px)`}
{/* Spacer so content starts below the fixed nav */}
{/* Tall content in the SAME container as the banner so it has room to stick while scrolling. */}
{`Scroll — the banner stays pinned ${navHeight}px from the top, below the nav.`}
); }, ], parameters: { docs: { description: { story: "Demonstrates the sticky positioning with a non-zero `navHeight` (80px), which offsets the banner's `top` value so it sticks below a fixed navigation bar. Scroll the canvas to see the offset take effect.", }, }, }, };