diff --git a/packages/components/package.json b/packages/components/package.json index faaf582a..95032ad8 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -68,7 +68,7 @@ } }, "dependencies": { - "@dolthub/react-contexts": "^0.3.1", + "@dolthub/react-contexts": "^0.3.2", "@dolthub/react-hooks": "^0.3.2", "@dolthub/web-utils": "^0.3.2", "classnames": "^2.5.1", diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css new file mode 100644 index 00000000..c44196b0 --- /dev/null +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css @@ -0,0 +1,49 @@ +/* Static, so a full-width panel anchors to the header. */ +.dropdown { + @apply flex self-stretch items-center; +} + +.triggerWrap { + @apply flex self-stretch items-center; +} + +.anchor { + @apply relative; +} + +.trigger { + @apply relative flex h-full items-center gap-2; +} + +/* A pseudo-element, so the box is unchanged and the label does not shift. */ +.triggerOpen::after { + @apply absolute inset-x-0 bottom-0 h-0.5 bg-sky-400; + content: ""; +} + +.caret { + @apply shrink-0 text-sm; +} + +.scrim { + @apply absolute left-0 right-0 top-full h-screen bg-[rgba(0,0,0,0.56)]; +} + +.panel { + @apply absolute left-0 right-0 top-full text-left; +} + +/* Must follow `.panel` to win the left/right tie. */ +.panelLeft { + @apply right-auto; +} + +.panelRight { + @apply left-auto right-0; +} + +/* Two classes, to outrank the navbar's `.inner a` / `.left a` styling. */ +.dropdown .panel a, +.dropdown .panel button { + @apply mx-0 text-base font-normal tracking-normal text-white hover:text-white; +} diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx b/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx new file mode 100644 index 00000000..3d10c2aa --- /dev/null +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx @@ -0,0 +1,127 @@ +import { useOnClickOutside } from "@dolthub/react-hooks"; +import { FaCaretDown, FaCaretUp } from "react-icons/fa"; +import cx from "classnames"; +import React, { ReactNode, useEffect, useRef, useState } from "react"; +import Btn from "../../Btn"; +import css from "./NavDropdown.module.css"; + +type Props = { + label: ReactNode; + children: ReactNode; + // "full" spans the page width; "left"/"right" anchor it to the trigger. + align?: "full" | "left" | "right"; + hideCaret?: boolean; + underlineOnOpen?: boolean; + // Click still toggles, keeping the menu reachable by keyboard and on touch. + openOnHover?: boolean; + defaultOpen?: boolean; + className?: string; + triggerClassName?: string; + panelClassName?: string; + isOpen?: boolean; + setIsOpen?: (o: boolean) => void; + ["data-cy"]?: string; +}; + +// A nav item whose panel dims the page beneath it. Must be rendered inside +// DesktopNavbar, which a full-width panel anchors to. +export default function NavDropdown({ + label, + children, + align = "full", + hideCaret = false, + underlineOnOpen = false, + openOnHover = false, + defaultOpen = false, + isOpen, + setIsOpen, + ...props +}: Props) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); + const controlled = isOpen !== undefined && setIsOpen !== undefined; + const open = controlled ? isOpen : uncontrolledOpen; + const ref = useRef(null); + + const setOpen = (o: boolean) => { + if (controlled) { + setIsOpen(o); + } else { + setUncontrolledOpen(o); + } + }; + + useOnClickOutside(ref, () => setOpen(false)); + + // Delayed, so the pointer can cross the gap to the panel without it closing. + const closeTimer = useRef | undefined>( + undefined, + ); + const cancelClose = () => { + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = undefined; + } + }; + useEffect(() => cancelClose, []); + + // On the wrapper, not the root: the root's scrim would pin the menu open. + const hoverProps = openOnHover + ? { + onMouseEnter: () => { + cancelClose(); + setOpen(true); + }, + onMouseLeave: () => { + cancelClose(); + closeTimer.current = setTimeout(() => setOpen(false), 150); + }, + } + : {}; + + return ( +
+ {/* Outside the anchor below, so it always covers the full page. */} + {open && ( +
setOpen(false)} /> + )} +
+ setOpen(!open)} + data-cy={props["data-cy"]} + > + {label} + {!hideCaret && + (open ? ( + + ) : ( + + ))} + + {open && ( +
+ {children} +
+ )} +
+
+ ); +} diff --git a/packages/components/src/Navbar/ForDesktop/index.module.css b/packages/components/src/Navbar/ForDesktop/index.module.css index 5d8f99a4..8e9d8178 100644 --- a/packages/components/src/Navbar/ForDesktop/index.module.css +++ b/packages/components/src/Navbar/ForDesktop/index.module.css @@ -1,5 +1,6 @@ +/* `relative` so a dropdown panel anchors here and can span the full width. */ .container { - @apply hidden lg:block w-full text-white pt-2 h-12 z-50 mx-auto pb-2; + @apply hidden lg:block relative w-full text-white h-12 z-50 mx-auto; } .inner { @@ -27,12 +28,9 @@ } } +/* `gap`, not margins on `a`: those skipped the div-wrapped dropdown triggers. */ .left { - @apply flex w-1/3 ml-2 order-first; - - a { - @apply ml-2 mr-10; - } + @apply flex self-stretch w-1/3 ml-2 order-first items-center gap-12; } .logo { @@ -62,11 +60,12 @@ } } +/* Not `relative`: that would make this, not the header, the panel's anchor. */ .logoLeft { - @apply justify-start relative; + @apply justify-start; .left { - @apply order-2 w-auto ml-6; + @apply order-2 w-auto ml-16; } .logo { @@ -78,6 +77,6 @@ } .right { - @apply order-3 w-auto absolute right-0 mr-8; + @apply order-3 w-auto ml-auto mr-8; } } diff --git a/packages/components/src/Navbar/ForMobile/NavDropdown.module.css b/packages/components/src/Navbar/ForMobile/NavDropdown.module.css new file mode 100644 index 00000000..e46dceee --- /dev/null +++ b/packages/components/src/Navbar/ForMobile/NavDropdown.module.css @@ -0,0 +1,15 @@ +.dropdown { + @apply flex flex-col items-stretch w-full; +} + +.trigger { + @apply w-full justify-between; +} + +.caret { + @apply shrink-0 flex px-2 py-1 rounded bg-white/10 text-xl; +} + +.panel { + @apply flex flex-col items-stretch w-full border-b border-white/10; +} diff --git a/packages/components/src/Navbar/ForMobile/NavDropdown.tsx b/packages/components/src/Navbar/ForMobile/NavDropdown.tsx new file mode 100644 index 00000000..c751279d --- /dev/null +++ b/packages/components/src/Navbar/ForMobile/NavDropdown.tsx @@ -0,0 +1,61 @@ +import { FiChevronDown, FiChevronUp } from "react-icons/fi"; +import cx from "classnames"; +import React, { ReactNode, useState } from "react"; +import Btn from "../../Btn"; +import css from "./NavDropdown.module.css"; + +type Props = { + label: ReactNode; + children: ReactNode; + defaultOpen?: boolean; + className?: string; + isOpen?: boolean; + setIsOpen?: (o: boolean) => void; + ["data-cy"]?: string; +}; + +// A nav item that expands in place, pushing the items below it down. +export default function NavDropdown({ + label, + children, + defaultOpen = false, + isOpen, + setIsOpen, + ...props +}: Props) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); + const controlled = isOpen !== undefined && setIsOpen !== undefined; + const open = controlled ? isOpen : uncontrolledOpen; + const toggle = () => { + if (controlled) { + setIsOpen(!isOpen); + } else { + setUncontrolledOpen(!uncontrolledOpen); + } + }; + + return ( +
+ + {label} + + {open ? : } + + + {open && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/packages/components/src/Navbar/ForMobile/index.module.css b/packages/components/src/Navbar/ForMobile/index.module.css index f3e5a8f5..db6a0a69 100644 --- a/packages/components/src/Navbar/ForMobile/index.module.css +++ b/packages/components/src/Navbar/ForMobile/index.module.css @@ -3,20 +3,24 @@ } .topContainer { - @apply w-full text-white px-5 py-2 h-14; + @apply w-full text-white px-5 py-2 h-[62px] shrink-0; +} + +.menuTop { + @apply sticky top-0 z-10; } .top { - @apply flex justify-between items-center; + @apply flex h-full justify-between items-center; button { - @apply text-xl p-2.5 text-white hover:text-white/70; + @apply text-xl p-2 rounded bg-white/10 text-white hover:text-white/70; } } .darkTop { button { - @apply text-background-acc-1 hover:text-primary; + @apply text-background-acc-1 bg-transparent hover:text-primary; } } @@ -31,28 +35,53 @@ } .openMenu { - @apply fixed top-0 bottom-0 left-0 right-0 z-100; + @apply fixed top-0 bottom-0 left-0 right-0 z-100 flex flex-col overflow-y-auto overscroll-contain; } +/* Descendant, not `> a`: sign-out buttons arrive wrapped in a div. */ .links { - @apply flex flex-col items-center mx-0 mt-10; + @apply flex flex-col items-stretch w-full border-t border-white/10; + + a:not([data-nav-panel] *), + button:not([data-nav-panel] *) { + @apply flex items-center h-[60px] px-6 text-base font-semibold tracking-[0.16px] text-white/90 border-b border-white/10 hover:text-sky-100; + } +} + +.account { + @apply shrink-0 px-6 pt-5 pb-2; +} + +.actions { + @apply mt-auto shrink-0 px-6 pt-8 md:mx-auto md:w-[375px]; a, button { - @apply text-xl my-4 mx-0 font-semibold tracking-widest text-white/90 hover:text-sky-100; + @apply flex items-center justify-center w-full h-[45px] px-12 rounded border border-white bg-white/10 text-[15px] font-semibold tracking-[0.3px] text-white hover:bg-white/20; + } +} + +.pillActions { + a, + button { + @apply rounded-full; } } .bottomLinks { - @apply absolute left-0 right-0 bottom-8 flex justify-between mx-auto px-4 max-w-xs; + @apply mt-auto flex justify-center gap-6 px-6 py-8; img, svg { - @apply text-white h-9 w-9; + @apply text-white h-6 w-6; } } -.links a { +.actions + .bottomLinks { + @apply mt-0; +} + +.links a:not([data-nav-panel] *) { @apply flex items-center; > svg { @apply mr-3; diff --git a/packages/components/src/Navbar/ForMobile/index.tsx b/packages/components/src/Navbar/ForMobile/index.tsx index 2deeda92..78c2fa28 100644 --- a/packages/components/src/Navbar/ForMobile/index.tsx +++ b/packages/components/src/Navbar/ForMobile/index.tsx @@ -12,6 +12,9 @@ type CommonProps = { type Props = CommonProps & { dark?: boolean; children: ReactNode; + mobileActions?: ReactNode; + mobileActionsPill?: boolean; + mobileAccount?: ReactNode; mobileBottomLinks?: ReactNode; }; @@ -47,17 +50,21 @@ export default function ForMobile(props: Props) { type NavProps = CommonProps & { onClose: () => void; children: ReactNode; + mobileActions?: ReactNode; + mobileActionsPill?: boolean; + mobileAccount?: ReactNode; mobileBottomLinks?: ReactNode; }; function NavMenu(props: NavProps) { + const menuBgColor = getBgColor(props.bgColor, true); return ( -
+
+ {/* The menu's own color, so links scrolling under the sticky bar stay hidden. */} {props.children}
+ {props.mobileAccount && ( +
+ {props.mobileAccount} +
+ )} + {props.mobileActions && ( +
+ {props.mobileActions} +
+ )} {props.mobileBottomLinks && (
{props.mobileBottomLinks} diff --git a/packages/components/src/Navbar/index.tsx b/packages/components/src/Navbar/index.tsx index b060c172..61b950b5 100644 --- a/packages/components/src/Navbar/index.tsx +++ b/packages/components/src/Navbar/index.tsx @@ -14,7 +14,11 @@ type Props = { logoLeft?: boolean; // Mobile-only + mobileActions?: ReactNode; // Full-width call to action below the links + mobileActionsPill?: boolean; // Use pill-shaped corners for mobile actions + mobileAccount?: ReactNode; // Signed in user's section between the links and actions mobileBottomLinks?: ReactNode; + leftLinksMobile?: ReactNode; // Overrides `leftLinks` for mobile rightLinksMobile?: ReactNode; // Overrides `rightLinks` for mobile }; @@ -33,10 +37,13 @@ export default function Navbar(props: Props) { - {props.leftLinks} + {props.leftLinksMobile ?? props.leftLinks} {props.rightLinksMobile ?? props.rightLinks} diff --git a/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx b/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx new file mode 100644 index 00000000..e2268f68 --- /dev/null +++ b/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx @@ -0,0 +1,283 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React, { ReactNode, useState } from "react"; +import { expect, userEvent, within } from "storybook/test"; +import Navbar from "../Navbar"; +import DesktopNavDropdown from "../Navbar/ForDesktop/NavDropdown"; +import { dolthubLogo } from "./images"; + +// The columns and cards below stand in for what the app would pass as +// children; they are DoltHub marketing content, not library UI. +const meta: Meta = { + title: "DesktopNavDropdown", + component: Navbar, + tags: ["autodocs"], + parameters: { layout: "fullscreen" }, +}; + +export default meta; + +type Story = StoryObj; + +const navBg = "bg-space-900"; + +type Product = { name: string; description: string }; + +function Card({ name, description }: Product) { + return ( + + + + {name} + {description} + + + ); +} + +function Column({ + heading, + products, +}: { + heading: string; + products: Product[]; +}) { + return ( +
+

{heading}

+ {products.map(p => ( + + ))} +
+ ); +} + +const databases: Product[] = [ + { name: "Dolt", description: "MySQL-compatible" }, + { name: "DoltgreSQL", description: "PostgreSQL-compatible" }, + { name: "DoltLite", description: "SQLite-compatible" }, +]; + +const platforms: Product[] = [ + { name: "DoltHub", description: "Collaborate with forks, clones, and PRs" }, + { name: "DoltLab", description: "DoltHub on your machine" }, + { + name: "Dolt Workbench", + description: "A desktop workbench with agent mode", + }, +]; + +const productsMenu = (defaultOpen: boolean) => ( + + + +); + +const docsLinks = ["Dolt", "DoltgreSQL", "DoltLab"]; + +type DocsMenuProps = Partial>; + +// A small panel anchored to its trigger, like the Docs menu. +const docsMenu = (props: DocsMenuProps) => ( + +
+ {docsLinks.map(name => ( + + {name} + + ))} +
+
+); + +function navArgs(products: ReactNode, docs: ReactNode) { + return { + logo: DoltHub, + bgColor: navBg, + logoLeft: true, + large: true, + leftLinks: ( + <> + {products} + Pricing + {docs} + Blog + + ), + rightLinks: Sign in, + }; +} + +function args(defaultOpen: boolean) { + return navArgs(productsMenu(defaultOpen), Docs); +} + +// A tall hero so the scrim has something to dim. +function withHero(story: React.ComponentType) { + const StoryComponent = story; + return ( +
+ +
+

+ AGENTS NEED BRANCHES +

+

+ Dolt is the Database for Agents +

+
+
+ ); +} + +export const Closed: Story = { + args: args(false), + decorators: [withHero], +}; + +export const Open: Story = { + args: args(true), + decorators: [withHero], +}; + +export const OpenedByClick: Story = { + args: args(false), + decorators: [withHero], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button", { name: /products/i })); + await expect( + canvas.getByLabelText("nav dropdown panel"), + ).toBeInTheDocument(); + }, +}; + +export const AlignLeft: Story = { + args: navArgs( + productsMenu(false), + docsMenu({ align: "left", defaultOpen: true }), + ), + decorators: [withHero], +}; + +export const AlignRight: Story = { + args: navArgs( + productsMenu(false), + docsMenu({ align: "right", defaultOpen: true }), + ), + decorators: [withHero], +}; + +export const HideCaret: Story = { + args: navArgs( + productsMenu(false), + docsMenu({ align: "left", hideCaret: true }), + ), + decorators: [withHero], +}; + +export const UnderlineOnOpen: Story = { + args: navArgs( + productsMenu(false), + docsMenu({ align: "left", underlineOnOpen: true, defaultOpen: true }), + ), + decorators: [withHero], +}; + +export const OpenOnHover: Story = { + args: navArgs( + productsMenu(false), + docsMenu({ align: "left", openOnHover: true }), + ), + decorators: [withHero], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.hover(canvas.getByRole("button", { name: /docs/i })); + await expect( + await canvas.findByLabelText("nav dropdown panel"), + ).toBeInTheDocument(); + }, +}; + +// `className` wraps the whole item; `triggerClassName` styles the button. +export const CustomClassNames: Story = { + args: navArgs( + productsMenu(false), + docsMenu({ + align: "left", + defaultOpen: true, + className: "rounded bg-white/5 px-2", + // The navbar sets link color and weight, so show one it doesn't. + triggerClassName: "uppercase", + }), + ), + decorators: [withHero], +}; + +function ControlledDocsMenu() { + const [open, setOpen] = useState(false); + return ( + <> + {docsMenu({ align: "left", isOpen: open, setIsOpen: setOpen })} + + {open ? "open" : "closed"} + + + ); +} + +export const Controlled: Story = { + args: navArgs(productsMenu(false), ), + decorators: [withHero], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByText("closed")).toBeInTheDocument(); + await userEvent.click(canvas.getByRole("button", { name: /docs/i })); + await expect(canvas.getByText("open")).toBeInTheDocument(); + await expect( + canvas.getByLabelText("nav dropdown panel"), + ).toBeInTheDocument(); + }, +}; diff --git a/packages/components/src/__stories__/MobileNavbar.stories.tsx b/packages/components/src/__stories__/MobileNavbar.stories.tsx new file mode 100644 index 00000000..3b96a37a --- /dev/null +++ b/packages/components/src/__stories__/MobileNavbar.stories.tsx @@ -0,0 +1,351 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { AiFillDiscord } from "react-icons/ai"; +import { FaGithub, FaLinkedinIn, FaYoutube } from "react-icons/fa"; +import React, { useState } from "react"; +import { expect, userEvent, within } from "storybook/test"; +import Navbar from "../Navbar"; +import MobileNavDropdown from "../Navbar/ForMobile/NavDropdown"; +import { dolthubLogo } from "./images"; + +// Each story mirrors a real consumer's link shape, so Chromatic catches +// regressions to apps this PR does not touch. +const meta: Meta = { + title: "MobileNavbar", + component: Navbar, + tags: ["autodocs"], + parameters: { layout: "fullscreen" }, + globals: { viewport: { value: "iphonex" } }, +}; + +export default meta; + +type Story = StoryObj; + +const logo = DoltHub; + +// The menu only mounts once the hamburger is clicked. +async function openMobileMenu(canvasElement: HTMLElement) { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByLabelText("open mobile navbar menu")); + await expect(canvas.getByLabelText("mobile nav menu")).toBeInTheDocument(); +} + +const socialLinks = ( + <> + + + + + + + + + + + + + +); + +const signInAction = ( + +); + +// EXISTING CONSUMER SHAPES + +const dolthubArgs = { + logo, + leftLinks: ( + <> + Databases + Pricing + Documentation + Blog + + ), + rightLinks: Sign In, + rightLinksMobile: ( + <> + My Organizations + Settings + Sign In + + ), + mobileBottomLinks: socialLinks, + logoLeft: true, +}; + +export const Closed: Story = { args: dolthubArgs }; + +export const OpenDoltHub: Story = { + args: dolthubArgs, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +export const OpenDoltLab: Story = { + args: { + logo, + bgColor: "bg-space-700", + logoLeft: true, + leftLinks: ( + <> + Documentation + Blog + Demo + + ), + rightLinks: Discord, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +export const OpenHosted: Story = { + args: { + logo, + bgColor: "bg-ocean-400", + large: true, + leftLinks: ( + <> + Deployments + Pricing + Documentation + + ), + rightLinks: Sign In, + rightLinksMobile: ( + <> + Discord + GitHub + Settings + + ), + }, + globals: { theme: "hosted", viewport: { value: "iphonex" } }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +// Workbench passes an empty fragment for `leftLinks`. +export const OpenWorkbench: Story = { + args: { + logo, + bgColor: "bg-transparent", + logoLeft: true, + leftLinks: <>, + rightLinks: ( + <> + Blog + Discord + GitHub + + ), + }, + globals: { theme: "workbench", viewport: { value: "iphonex" } }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +// NEW BEHAVIOUR + +// More links than fit on a phone. +export const OpenScrollable: Story = { + args: { + ...dolthubArgs, + leftLinks: ( + <> + {[ + "Databases", + "Pricing", + "Documentation", + "Blog", + "Public Databases", + "Dolt", + "DoltgreSQL", + "DoltLite", + "DoltHub", + "DoltLab", + "Dolt Workbench", + "Hosted Dolt", + ].map(name => ( + + {name} + + ))} + + ), + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +const products = [ + { name: "Dolt", href: "#dolt" }, + { name: "DoltgreSQL", href: "#doltgresql" }, + { name: "DoltLite", href: "#doltlite" }, + { name: "DoltHub", href: "#dolthub" }, + { name: "DoltLab", href: "#doltlab" }, + { name: "Dolt Workbench", href: "#dolt-workbench" }, +]; + +const withDropdown = (defaultOpen: boolean) => ( + <> + + {products.map(p => ( + + {p.name} + + ))} + + Pricing + Docs + Public Databases + Blog + +); + +export const OpenWithDropdown: Story = { + args: { + ...dolthubArgs, + leftLinksMobile: withDropdown(false), + rightLinksMobile: <>, + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +export const OpenWithSignIn: Story = { + args: { + ...dolthubArgs, + rightLinksMobile: ( + <> + My Organizations + Settings + + ), + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +export const OpenWithPillActions: Story = { + args: { + ...dolthubArgs, + rightLinksMobile: <>, + mobileActions: signInAction, + mobileActionsPill: true, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +export const OpenWithDropdownExpanded: Story = { + args: { + ...dolthubArgs, + leftLinksMobile: withDropdown(true), + rightLinksMobile: <>, + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +const signOutAction = ( + +); + +const account = ( + <> +
+ + Signed in as{" "} + + tbantle + + + +
+
    + {["Settings", "My Organizations", "Contact DoltHub"].map(name => ( +
  • + {name} +
  • + ))} +
+ +); + +// `mobileAccount` sits between the links and the actions, outside the row +// styling. +export const OpenWithAccount: Story = { + args: { + ...dolthubArgs, + leftLinksMobile: withDropdown(false), + rightLinksMobile: <>, + mobileAccount: account, + mobileActions: signOutAction, + }, + play: async ({ canvasElement }) => { + await openMobileMenu(canvasElement); + const canvas = within(canvasElement); + await expect(canvas.getByText(/Signed in as/)).toBeVisible(); + await expect( + canvas.getByRole("button", { name: "Sign out" }), + ).toBeInTheDocument(); + }, +}; + +function ControlledDropdown() { + const [open, setOpen] = useState(false); + return ( + <> + + {products.map(p => ( + + {p.name} + + ))} + + Pricing + Blog + + ); +} + +export const OpenWithControlledDropdown: Story = { + args: { + ...dolthubArgs, + leftLinksMobile: , + rightLinksMobile: <>, + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => { + await openMobileMenu(canvasElement); + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /products \(closed\)/i }), + ); + await expect( + canvas.getByRole("button", { name: /products \(open\)/i }), + ).toBeInTheDocument(); + await expect( + canvas.getByLabelText("nav dropdown panel"), + ).toBeInTheDocument(); + }, +}; diff --git a/packages/components/src/__tests__/Navbar.test.tsx b/packages/components/src/__tests__/Navbar.test.tsx index 21bdafb6..39f1ba18 100644 --- a/packages/components/src/__tests__/Navbar.test.tsx +++ b/packages/components/src/__tests__/Navbar.test.tsx @@ -134,10 +134,11 @@ describe("test MobileNavbar", () => { ); await user.click(screen.getByLabelText("open mobile navbar menu")); - const tops = screen.getAllByLabelText("mobile navbar top"); - tops.forEach(top => { - expect(top).toHaveClass(bgColor); - }); + // The bar over the page stays transparent; the open menu's own bar takes + // the menu's solid color so links scrolling under it stay hidden. + const [pageTop, menuTop] = screen.getAllByLabelText("mobile navbar top"); + expect(pageTop).toHaveClass(bgColor); + expect(menuTop).toHaveClass("bg-background-acc-1"); expect(screen.getByLabelText("mobile nav menu")).toHaveClass( "bg-background-acc-1", ); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 7121e4c4..0a4fcda7 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -35,7 +35,9 @@ export { export { default as FormModal } from "./Modal/ForForm"; export { default as Navbar } from "./Navbar"; export { default as DesktopNavbar } from "./Navbar/ForDesktop"; +export { default as DesktopNavDropdown } from "./Navbar/ForDesktop/NavDropdown"; export { default as MobileNavbar } from "./Navbar/ForMobile"; +export { default as MobileNavDropdown } from "./Navbar/ForMobile/NavDropdown"; export { default as NoneFoundMsg } from "./NoneFoundMsg"; export { default as Popup, PopupProps } from "./Popup"; export { default as QueryHandler } from "./QueryHandler"; diff --git a/packages/components/src/tailwind/theme/base/colors.ts b/packages/components/src/tailwind/theme/base/colors.ts index dc5ce4c0..f76bb321 100644 --- a/packages/components/src/tailwind/theme/base/colors.ts +++ b/packages/components/src/tailwind/theme/base/colors.ts @@ -82,6 +82,7 @@ export const staticColors = { 500: "#202F57", 600: "#1F2942", 700: "#182134", + 900: "#070F25", }, stone: { 50: "#F1F3F8", diff --git a/yarn.lock b/yarn.lock index 018b4b38..372c785d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2771,7 +2771,7 @@ __metadata: "@babel/preset-env": "npm:^8.0.2" "@babel/preset-react": "npm:^8.0.1" "@babel/preset-typescript": "npm:^8.0.1" - "@dolthub/react-contexts": "npm:^0.3.1" + "@dolthub/react-contexts": "npm:^0.3.2" "@dolthub/react-hooks": "npm:^0.3.2" "@dolthub/web-utils": "npm:^0.3.2" "@rollup/plugin-commonjs": "npm:^29.0.3" @@ -2852,7 +2852,7 @@ __metadata: languageName: unknown linkType: soft -"@dolthub/react-contexts@npm:^0.3.1, @dolthub/react-contexts@workspace:packages/contexts": +"@dolthub/react-contexts@npm:^0.3.2, @dolthub/react-contexts@workspace:packages/contexts": version: 0.0.0-use.local resolution: "@dolthub/react-contexts@workspace:packages/contexts" dependencies: