Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-4205] - Flyout Component for Meganav #625

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "^0.8.11",
Expand Down
138 changes: 138 additions & 0 deletions src/core/Flyout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as React 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: {
aralovelace marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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.
*/
panelStyling?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contradicts the ...ClassName conventions used elsewhere

}[];
/**
* 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;
setIsOpen: (val: boolean) => void;
};

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";

export const FlyOverlay = ({ className }: { className: string }) => (
<div
className={cn(
"absolute left-0 right-0 h-screen w-full opacity-0 animate-[fadeInTenPercent_200ms_ease]",
className,
)}
style={{ height: componentMaxHeight(HEADER_HEIGHT) }}
></div>
);

const Flyout = ({
menuItems,
className,
flyOutClassName,
menuLinkClassName,
viewPortClassName,
hasAnimation,
setIsOpen,
}: FlyoutProps) => {
return (
<NavigationMenu
className={cn(className, "flex w-full")}
onValueChange={(val) => setIsOpen(!!val)}
>
<NavigationMenuList className="flex list-none center">
{menuItems.map(({ label, content, link, panelStyling }) =>
content ? (
<NavigationMenuItem key={label}>
<NavigationMenuTrigger
className={cn(
"group outline-none focus:outline-none select-none cursor-pointer relative",
DEFAULT_MENU_LINK_STYLING,
menuLinkClassName,
)}
>
{label}
</NavigationMenuTrigger>
<NavigationMenuContent
className={cn(
"absolute inset-x-0 top-0 p-24 z-10",
hasAnimation && PANEL_ANIMATION,
panelStyling,
)}
>
{content}
</NavigationMenuContent>
</NavigationMenuItem>
) : (
<NavigationMenuLink key={label}>
<a
href={link}
className={cn(DEFAULT_MENU_LINK_STYLING, menuLinkClassName)}
>
{label}
</a>
</NavigationMenuLink>
),
)}
</NavigationMenuList>

<div className={cn("absolute left-0 top-full", flyOutClassName)}>
<NavigationMenuViewport
className={cn(DEFAULT_VIEWPORT_STYLING, viewPortClassName)}
/>
</div>
</NavigationMenu>
);
};

export default Flyout;
175 changes: 175 additions & 0 deletions src/core/Flyout/Flyout.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React, { useState } from "react";
import Flyout, { FlyOverlay } 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 panelStyling = "w-full sm:w-[816px] bg-neutral-000 dark:bg-neutral-1300";

const ProductsGrid = () => {
const [selectedProduct, setSelectedProduct] = useState<ProductName | null>(
null,
);
return (
<div className="grid grid-cols-2">
{Object.keys(products).map((product) => (
<ProductTile
name={product as ProductName}
key={product}
selected={selectedProduct === product}
onClick={() => setSelectedProduct(product as ProductName)}
/>
))}
</div>
);
};

const Panels = ({
panelLeft,
platforms,
}: {
panelLeft: React.ReactNode;
platforms: string[];
}) => (
<div className="flex flex-row gap-x-24">
<div className="flex-6">{panelLeft}</div>
<div className="flex-4 pt-12">
<p className="ui-text-overline2 text-neutral-700 dark:text-neutral-600 pb-6">
platform
</p>
{platforms.map((item) => (
<li className="list-none py-6" key={item}>
<a
className="ui-text-menu3 text-neutral-1000 dark:text-neutral-300"
href={`/platform/${item.toLowerCase()}`}
>
{item}
</a>
</li>
))}
</div>
</div>
);

const DefaultPanelLeft = ({ title, desc }: { title: string; desc: string }) => (
<div className="bg-neutral-100 dark:bg-neutral-1200 w-full p-24">
<h4 className="ui-text-h4 text-neutral-1300 dark:text-neutral-000">
{title}
</h4>
<p className="ui-text-p3 text-neutral-800 dark:text-neutral-500 mt-8">
{desc}
</p>
<FeaturedLink
url=""
additionalCSS="text-neutral-1300 dark:text-neutral-000"
iconColor="text-orange-600"
>
Learn more
</FeaturedLink>
</div>
);

const menuItems = [
{ label: "Home", content: null, link: "" },
{
label: "Products",
content: <Panels panelLeft={<ProductsGrid />} platforms={platforms} />,
panelStyling: panelStyling,
},
{
label: "Solutions",
content: (
<Panels
panelLeft={
<DefaultPanelLeft
title="Fan engagement"
desc=" Capture the attention of millions of fans during live events."
/>
}
platforms={platforms}
/>
),
panelStyling: panelStyling,
},
{
label: "Company",
content: (
<Panels
panelLeft={
<DefaultPanelLeft
title="Leading the realtime revolution"
desc="Hear from our founders about Ably’s ambitious plans to become the world’s definitive realtime platform."
/>
}
platforms={platforms}
/>
),
panelStyling: panelStyling,
},
{ label: "Pricing", content: null, link: "/pricing" },
{ label: "Docs", content: null, link: "/docs" },
];

const FlyoutStory = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);

return (
<div>
<Flyout
menuItems={menuItems}
className="justify-center relative z-40 ui-standard-container"
flyOutClassName="flex w-full justify-center"
viewPortClassName="ui-shadow-lg-medium border border-neutral-000 dark:border-neutral-1300 rounded-2xl mt-8"
hasAnimation={true}
setIsOpen={setIsOpen}
/>
{isOpen && (
<FlyOverlay className="bg-neutral-1300 dark:bg-neutral-000 opacity-10 z-20 h-screen" />
)}
</div>
);
};

export const Default = {
render: () => <FlyoutStory />,
};

export const StandardContainer = {
render: () => (
<section>
<FlyoutStory />
<div className="ui-standard-container relative z-0">
<div className="w-full h-[150px] bg-orange-400 flex justify-center items-center">
Content 1
</div>
<div className="w-full h-[150px] bg-yellow-300 flex justify-center items-center">
Content 2
</div>
<div className="w-full h-[150px] bg-neutral-1300 flex justify-center items-center text-neutral-000">
Content 3
</div>
</div>
</section>
),
parameters: {
docs: {
description: {
story:
"The Flyout component is positioned within a standard container and content and Animation is enabled",
},
},
},
};
Loading