Mobile Menu and accessability

This commit is contained in:
SwiftyOS
2024-10-17 12:25:12 +02:00
parent 6318a976b5
commit f96f2f101b
7 changed files with 382 additions and 7 deletions

View File

@@ -3,6 +3,7 @@ import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-a11y",
"@storybook/addon-onboarding",
"@storybook/addon-links",
"@storybook/addon-essentials",

View File

@@ -0,0 +1,22 @@
import type { TestRunnerConfig } from "@storybook/test-runner";
import { injectAxe, checkA11y } from "axe-playwright";
/*
* See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api
* to learn more about the test-runner hooks API.
*/
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page) {
await checkA11y(page, "#storybook-root", {
detailedReport: true,
detailedReportOptions: {
html: true,
},
});
},
};
export default config;

View File

@@ -53,6 +53,7 @@
"date-fns": "^3.6.0",
"dotenv": "^16.4.5",
"embla-carousel-react": "^8.3.0",
"framer-motion": "^11.11.9",
"lucide-react": "^0.407.0",
"moment": "^2.30.1",
"next": "^14.2.13",
@@ -73,6 +74,7 @@
},
"devDependencies": {
"@playwright/test": "^1.47.1",
"@storybook/addon-a11y": "^8.3.5",
"@storybook/addon-essentials": "^8.3.5",
"@storybook/addon-interactions": "^8.3.5",
"@storybook/addon-links": "^8.3.5",
@@ -86,6 +88,7 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-modal": "^3.16.3",
"axe-playwright": "^2.0.3",
"chromatic": "^11.12.5",
"concurrently": "^9.0.1",
"eslint": "^8",

View File

@@ -62,6 +62,7 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
src={`https://www.youtube.com/embed/${getYouTubeVideoId(image)}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title="YouTube video player"
></iframe>
) : (
<div className="relative h-full w-full overflow-hidden">
@@ -75,6 +76,7 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
onPlay={() => handlePlay(index)}
onPause={() => handlePause(index)}
autoPlay={false}
title="Video"
>
<source src={image} type="video/mp4" />
Your browser does not support the video tag.

View File

@@ -0,0 +1,108 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MobileNavBar } from "./MobileNavBar";
import { userEvent, within } from "@storybook/test";
import { IconType } from "../ui/icons";
const meta = {
title: "AGPTUI/MobileNavBar",
component: MobileNavBar,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
userName: { control: "text" },
userEmail: { control: "text" },
activeLink: { control: "text" },
avatarSrc: { control: "text" },
menuItemGroups: { control: "object" },
},
} satisfies Meta<typeof MobileNavBar>;
export default meta;
type Story = StoryObj<typeof meta>;
const defaultMenuItemGroups = [
{
items: [
{ icon: IconType.Marketplace, text: "Marketplace", href: "/marketplace" },
{ icon: IconType.Library, text: "Library", href: "/library" },
{ icon: IconType.Builder, text: "Builder", href: "/builder" },
],
},
{
items: [
{ icon: IconType.Edit, text: "Edit profile", href: "/profile/edit" },
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
href: "/publish",
},
],
},
{
items: [{ icon: IconType.Settings, text: "Settings", href: "/settings" }],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
onClick: () => console.log("Logged out"),
},
],
},
];
export const Default: Story = {
args: {
userName: "John Doe",
userEmail: "john.doe@example.com",
activeLink: "/marketplace",
avatarSrc: "https://avatars.githubusercontent.com/u/123456789?v=4",
menuItemGroups: defaultMenuItemGroups,
},
};
export const NoAvatar: Story = {
args: {
userName: "Jane Smith",
userEmail: "jane.smith@example.com",
activeLink: "/library",
menuItemGroups: defaultMenuItemGroups,
},
};
export const LongUserName: Story = {
args: {
userName: "Alexander Bartholomew Christopherson III",
userEmail: "alexander@example.com",
activeLink: "/builder",
avatarSrc: "https://avatars.githubusercontent.com/u/987654321?v=4",
menuItemGroups: defaultMenuItemGroups,
},
};
export const WithInteraction: Story = {
args: {
...Default.args,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const menuTrigger = canvas.getByRole("button");
await userEvent.click(menuTrigger);
// Wait for the popover to appear
await canvas.findByText("Edit profile");
},
};

View File

@@ -0,0 +1,182 @@
import * as React from "react";
import Link from "next/link";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverPortal,
} from "@/components/ui/popover";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
import {
IconType,
IconMenu,
IconChevronUp,
IconEdit,
IconLayoutDashboard,
IconUploadCloud,
IconSettings,
IconLogOut,
IconMarketplace,
IconLibrary,
IconBuilder,
} from "../ui/icons";
import { AnimatePresence, motion } from "framer-motion";
interface MobileNavBarProps {
userName: string;
userEmail?: string;
activeLink: string;
avatarSrc?: string;
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
}
const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
({ children }, ref) => (
<div ref={ref} className="h-screen w-screen backdrop-blur-md">
{children}
</div>
),
);
Overlay.displayName = "Overlay";
const PopoutMenuItem: React.FC<{
icon: IconType;
isActive: boolean;
text: React.ReactNode;
href?: string;
onClick?: () => void;
}> = ({ icon, isActive, text, href, onClick }) => {
const getIcon = (iconType: IconType) => {
const iconClass = "w-6 h-6 relative";
switch (iconType) {
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
default:
return null;
}
};
const content = (
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0]">
{getIcon(icon)}
<div className="relative">
<div
className={`font-['Inter'] text-base font-normal leading-7 text-[#474747] ${isActive ? "font-semibold text-[#272727]" : "text-[#474747]"}`}
>
{text}
</div>
{isActive && (
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727]"></div>
)}
</div>
</div>
);
if (onClick)
return (
<div className="w-full" onClick={onClick}>
{content}
</div>
);
if (href)
return (
<Link href={href} className="w-full">
{content}
</Link>
);
return content;
};
export const MobileNavBar: React.FC<MobileNavBarProps> = ({
userName,
userEmail,
activeLink,
avatarSrc,
menuItemGroups,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<div className="z-50 inline-flex h-8 w-screen items-center justify-end rounded-lg p-8 md:hidden">
{isOpen ? (
<IconChevronUp className="ui-not-focus-visible:outline-none h-8 w-8 rounded-md border-2 border-gray-600 hover:bg-gray-200/50 hover:stroke-gray-600 active:stroke-gray-900" />
) : (
<IconMenu className="ui-not-focus-visible:outline-none h-8 w-8 rounded-md border-2 border-gray-600 hover:bg-gray-200/50 hover:stroke-gray-600 active:stroke-gray-900" />
)}
</div>
</PopoverTrigger>
<AnimatePresence>
<PopoverPortal>
<Overlay>
<PopoverContent asChild>
<motion.div
initial={{ opacity: 0, y: -32 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -32, transition: { duration: 0.2 } }}
className="w-screen rounded-b-2xl"
>
<div className="mb-4 inline-flex items-end justify-start gap-4">
<Avatar className="h-14 w-14 border border-[#474747]">
<AvatarImage src={avatarSrc} alt={userName} />
<AvatarFallback>{userName.charAt(0)}</AvatarFallback>
</Avatar>
<div className="relative h-14 w-[153px]">
<div className="absolute left-0 top-0 font-['Inter'] text-lg font-semibold leading-7 text-[#474747]">
{userName}
</div>
<div className="absolute left-0 top-6 font-['Inter'] text-base font-normal leading-7 text-[#474747]">
{userEmail}
</div>
</div>
</div>
<Separator className="mb-4" />
{menuItemGroups.map((group, groupIndex) => (
<React.Fragment key={groupIndex}>
{group.items.map((item, itemIndex) => (
<PopoutMenuItem
key={itemIndex}
icon={item.icon}
isActive={item.href === activeLink}
text={item.text}
onClick={item.onClick}
href={item.href}
/>
))}
{groupIndex < menuItemGroups.length - 1 && (
<Separator className="my-4" />
)}
</React.Fragment>
))}
</motion.div>
</PopoverContent>
</Overlay>
</PopoverPortal>
</AnimatePresence>
</Popover>
);
};

View File

@@ -2719,6 +2719,14 @@
dependencies:
"@sinonjs/commons" "^3.0.0"
"@storybook/addon-a11y@^8.3.5":
version "8.3.5"
resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.3.5.tgz"
integrity sha512-/19UO8IXbyfcYK5K8ejSYF+hC+EK79c0bBPHMNeYSFOHSqQM3KoMo+TLIcLsuhuRClmlM+4Zs+VSIYDwc+d3ig==
dependencies:
"@storybook/addon-highlight" "8.3.5"
axe-core "^4.2.0"
"@storybook/addon-actions@8.3.5":
version "8.3.5"
resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.3.5.tgz"
@@ -3549,6 +3557,11 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/junit-report-builder@^3.0.2":
version "3.0.2"
resolved "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz"
integrity sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==
"@types/lodash@^4.14.167":
version "4.17.10"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz"
@@ -4412,11 +4425,29 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
axe-core@^4.9.1:
axe-core@^4.10.0, axe-core@^4.2.0, axe-core@^4.9.1, axe-core@>=3:
version "4.10.0"
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz"
integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==
axe-html-reporter@2.2.11:
version "2.2.11"
resolved "https://registry.npmjs.org/axe-html-reporter/-/axe-html-reporter-2.2.11.tgz"
integrity sha512-WlF+xlNVgNVWiM6IdVrsh+N0Cw7qupe5HT9N6Uyi+aN7f6SSi92RDomiP1noW8OWIV85V6x404m5oKMeqRV3tQ==
dependencies:
mustache "^4.0.1"
axe-playwright@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/axe-playwright/-/axe-playwright-2.0.3.tgz"
integrity sha512-s7iI2okyHHsD3XZK4RMJtTy2UASkNWLQtnzLuaHiK3AWkERf+cqZJqkxb7O4b56fnbib9YnZVRByTl92ME3o6g==
dependencies:
"@types/junit-report-builder" "^3.0.2"
axe-core "^4.10.0"
axe-html-reporter "2.2.11"
junit-report-builder "^5.1.1"
picocolors "^1.1.0"
axios@^1.6.1:
version "1.7.7"
resolved "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz"
@@ -6783,6 +6814,13 @@ forwarded@0.2.0:
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
framer-motion@^11.11.9:
version "11.11.9"
resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.9.tgz"
integrity sha512-XpdZseuCrZehdHGuW22zZt3SF5g6AHJHJi7JwQIigOznW4Jg1n0oGPMJQheMaKLC+0rp5gxUKMRYI6ytd3q4RQ==
dependencies:
tslib "^2.4.0"
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz"
@@ -8387,6 +8425,15 @@ jsonfile@^6.0.1:
object.assign "^4.1.4"
object.values "^1.1.6"
junit-report-builder@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-5.1.1.tgz"
integrity sha512-ZNOIIGMzqCGcHQEA2Q4rIQQ3Df6gSIfne+X9Rly9Bc2y55KxAZu8iGv+n2pP0bLf0XAOctJZgeloC54hWzCahQ==
dependencies:
lodash "^4.17.21"
make-dir "^3.1.0"
xmlbuilder "^15.1.1"
keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz"
@@ -8570,7 +8617,7 @@ magic-string@0.30.8:
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
make-dir@^3.0.0, make-dir@^3.0.2:
make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@@ -9086,6 +9133,11 @@ ms@2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mustache@^4.0.1:
version "4.2.0"
resolved "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz"
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
@@ -9623,10 +9675,10 @@ pg-types@^2.2.0:
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
picocolors@^1.0.0, picocolors@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz"
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz"
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
version "2.3.1"
@@ -9662,7 +9714,7 @@ playwright-core@>=1.2.0, playwright-core@1.47.2:
resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz"
integrity sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==
playwright@^1.14.0, playwright@1.47.2:
playwright@^1.14.0, playwright@>1.0.0, playwright@1.47.2:
version "1.47.2"
resolved "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz"
integrity sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==
@@ -12182,6 +12234,11 @@ xml@^1.0.1:
resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz"
integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==
xmlbuilder@^15.1.1:
version "15.1.1"
resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz"
integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
xtend@^4.0.0, xtend@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz"