From 95fc17692826d38784e3dd8f5bfed7e79b82c513 Mon Sep 17 00:00:00 2001 From: April Smith Date: Thu, 30 Jan 2025 13:38:29 +0000 Subject: [PATCH] feat: Flyout for Meganav add fade-out animation to flyout exit, consolidate flyoverlay Add overlay for Meganav --- package.json | 3 +- src/core/Flyout.tsx | 167 +++++++++++++++++ src/core/Flyout/Flyout.stories.tsx | 168 ++++++++++++++++++ .../__snapshots__/Flyout.stories.tsx.snap | 158 ++++++++++++++++ src/core/Header.tsx | 24 ++- tailwind.config.js | 36 +++- yarn.lock | 45 +++++ 7 files changed, 596 insertions(+), 5 deletions(-) create mode 100644 src/core/Flyout.tsx create mode 100644 src/core/Flyout/Flyout.stories.tsx create mode 100644 src/core/Flyout/__snapshots__/Flyout.stories.tsx.snap diff --git a/package.json b/package.json index d770231f..ddaf176b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/ui", - "version": "15.3.6", + "version": "15.4.0", "description": "Home of the Ably design system library ([design.ably.com](https://design.ably.com)). It provides a showcase, development/test environment and a publishing pipeline for different distributables.", "repository": { "type": "git", @@ -81,6 +81,7 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-navigation-menu": "^1.2.4", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", "addsearch-js-client": "^1.0.2", diff --git a/src/core/Flyout.tsx b/src/core/Flyout.tsx new file mode 100644 index 00000000..396b3e8b --- /dev/null +++ b/src/core/Flyout.tsx @@ -0,0 +1,167 @@ +import React, { useState } from "react"; +import { + NavigationMenu, + NavigationMenuItem, + NavigationMenuList, + NavigationMenuTrigger, + NavigationMenuContent, + NavigationMenuViewport, + NavigationMenuLink, +} from "@radix-ui/react-navigation-menu"; +import cn from "./utils/cn"; +import { componentMaxHeight, HEADER_HEIGHT } from "./utils/heights"; + +/** + * Props for the Flyout component. + */ +type FlyoutProps = { + /** + * Array of menu items to be displayed in the flyout. + */ + menuItems: { + /** + * Label for the menu item. + */ + label: string; + /** + * Optional content to be displayed in the flyout panel. + */ + content?: React.ReactNode; + /** + * Optional link for the menu item. + */ + link?: string; + /** + * Optional styling for the flyout panel. + */ + panelClassName?: string; + }[]; + /** + * Optional class name for the flyout container. + */ + className?: string; + /** + * Optional class name for the flyout element. + */ + flyOutClassName?: string; + /** + * Optional class name for the menu link. + */ + menuLinkClassName?: string; + /** + * Optional class name for the viewport. + */ + viewPortClassName?: string; + /** + * Flag to indicate if animation should be applied. + */ + hasAnimation: boolean; +}; + +const DEFAULT_MENU_LINK_STYLING = + "ui-text-menu3 font-bold text-neutral-1000 dark:neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-1200 hover:text-neutral-1300 dark:hover:text-neutral-000 px-12 py-8 flex items-center justify-between"; +const DEFAULT_VIEWPORT_STYLING = + "relative overflow-hidden w-full h-[var(--radix-navigation-menu-viewport-height)] origin-[top_center] transition-[width,_height] duration-300 data-[state=closed]:animate-scale-out data-[state=open]:animate-scale-in sm:w-[var(--radix-navigation-menu-viewport-width)]"; +const PANEL_ANIMATION = + "data-[motion=from-end]:animate-enter-from-right data-[motion=from-start]:animate-enter-from-left data-[motion=to-end]:animate-exit-to-right data-[motion=to-start]:animate-exit-to-left"; + +const FlyOverlay = ({ + className, + fadingOut, +}: { + className: string; + fadingOut: boolean; +}) => ( +
+); + +const Flyout = ({ + menuItems, + className, + flyOutClassName, + menuLinkClassName, + viewPortClassName, + hasAnimation, +}: FlyoutProps) => { + const [isOpen, setIsOpen] = useState(false); + const [fadingOut, setFadingOut] = useState(false); + + const closeMenu = () => { + setFadingOut(true); + + setTimeout(() => { + setIsOpen(false); + setFadingOut(false); + }, 150); + }; + + return ( + <> + (val ? setIsOpen(true) : closeMenu())} + delayDuration={0} + > + + {menuItems.map(({ label, content, link, panelClassName }) => + content ? ( + + + {label} + + + {content} + + + ) : ( + + + {label} + + + ), + )} + + +
+ +
+
+ {isOpen ? ( + + ) : null} + + ); +}; + +export default Flyout; diff --git a/src/core/Flyout/Flyout.stories.tsx b/src/core/Flyout/Flyout.stories.tsx new file mode 100644 index 00000000..dd43b014 --- /dev/null +++ b/src/core/Flyout/Flyout.stories.tsx @@ -0,0 +1,168 @@ +import React, { useState } from "react"; +import Flyout from "../Flyout"; +import ProductTile from "../ProductTile"; +import FeaturedLink from "../FeaturedLink"; +import { ProductName, products } from "../ProductTile/data"; + +export default { + title: "Components/Flyout", + component: Flyout, + tags: ["autodocs"], +}; + +const platforms = [ + "Infrastructure", + "Integrations", + "SDKs", + "Security & Compliance", +]; + +const panelClassName = + "w-full sm:w-[816px] bg-neutral-000 dark:bg-neutral-1300"; + +const ProductsGrid = () => { + const [selectedProduct, setSelectedProduct] = useState( + null, + ); + return ( +
+ {Object.keys(products).map((product) => ( + setSelectedProduct(product as ProductName)} + /> + ))} +
+ ); +}; + +const Panels = ({ + panelLeft, + platforms, +}: { + panelLeft: React.ReactNode; + platforms: string[]; +}) => ( +
+
{panelLeft}
+
+

+ platform +

+ {platforms.map((item) => ( +
  • + + {item} + +
  • + ))} +
    +
    +); + +const DefaultPanelLeft = ({ title, desc }: { title: string; desc: string }) => ( +
    +

    + {title} +

    +

    + {desc} +

    + + Learn more + +
    +); + +const menuItems = [ + { label: "Home", content: null, link: "" }, + { + label: "Products", + content: } platforms={platforms} />, + panelClassName: panelClassName, + }, + { + label: "Solutions", + content: ( + + } + platforms={platforms} + /> + ), + panelClassName: panelClassName, + }, + { + label: "Company", + content: ( + + } + platforms={platforms} + /> + ), + panelClassName: panelClassName, + }, + { label: "Pricing", content: null, link: "/pricing" }, + { label: "Docs", content: null, link: "/docs" }, +]; + +const FlyoutStory = () => { + return ( + + ); +}; + +export const Default = { + render: () => , +}; + +export const StandardContainer = { + render: () => ( +
    +
    + +
    + Content 1 +
    +
    + Content 2 +
    +
    + Content 3 +
    +
    +
    + ), + parameters: { + docs: { + description: { + story: + "The Flyout component is positioned within a standard container and content and Animation is enabled", + }, + }, + }, +}; diff --git a/src/core/Flyout/__snapshots__/Flyout.stories.tsx.snap b/src/core/Flyout/__snapshots__/Flyout.stories.tsx.snap new file mode 100644 index 00000000..f18efec0 --- /dev/null +++ b/src/core/Flyout/__snapshots__/Flyout.stories.tsx.snap @@ -0,0 +1,158 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Components/Flyout Default smoke-test 1`] = ` + +`; + +exports[`Components/Flyout StandardContainer smoke-test 1`] = ` +
    +
    + +
    + Content 1 +
    +
    + Content 2 +
    +
    + Content 3 +
    +
    +
    +`; diff --git a/src/core/Header.tsx b/src/core/Header.tsx index b5f28e78..2332ee68 100644 --- a/src/core/Header.tsx +++ b/src/core/Header.tsx @@ -120,9 +120,19 @@ const Header: React.FC = ({ searchButtonVisibility = "all", }) => { const [showMenu, setShowMenu] = useState(false); + const [fadingOut, setFadingOut] = useState(false); const [scrollpointClasses, setScrollpointClasses] = useState(""); const menuRef = useRef(null); + const closeMenu = () => { + setFadingOut(true); + + setTimeout(() => { + setShowMenu(false); + setFadingOut(false); + }, 150); + }; + useEffect(() => { const handleResize = () => { if (window.innerWidth >= 1040) { @@ -239,9 +249,17 @@ const Header: React.FC = ({ {showMenu ? ( <>
    setShowMenu(!showMenu)} - onKeyDown={(e) => e.key === "Escape" && setShowMenu(false)} + className={cn( + "fixed inset-0 bg-neutral-1300 dark:bg-neutral-1300", + { + "animate-[fade-in-ten-percent_150ms_ease-in-out_forwards]": + !fadingOut, + "animate-[fade-out-ten-percent_150ms_ease-in-out_forwards]": + fadingOut, + }, + )} + onClick={closeMenu} + onKeyDown={(e) => e.key === "Escape" && closeMenu()} role="presentation" />