From dce0f60e7d11e62f0bac8a39a7e92e2590bbc22c Mon Sep 17 00:00:00 2001 From: Doma Date: Fri, 5 May 2023 02:01:27 +0800 Subject: [PATCH] =?UTF-8?q?perf(a11y):=20add=20keyboard=20navigation=20sup?= =?UTF-8?q?port=20to=20SiteHeader=20more=20action=E2=80=A6=20(#464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/ConnectButton.tsx | 68 +++++----- src/components/site/SiteHeader.tsx | 68 +++++----- src/components/ui/Menu.tsx | 170 +++++++++++++++--------- src/components/ui/UniLink.tsx | 13 +- 4 files changed, 194 insertions(+), 125 deletions(-) diff --git a/src/components/common/ConnectButton.tsx b/src/components/common/ConnectButton.tsx index 213ea896..c062171b 100644 --- a/src/components/common/ConnectButton.tsx +++ b/src/components/common/ConnectButton.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "next-i18next" -import { useEffect, useState } from "react" +import React, { useEffect, useState } from "react" import { GeneralAccount, @@ -33,14 +33,17 @@ import { SITE_URL } from "~/lib/env" import { getSiteLink } from "~/lib/helpers" import { cn } from "~/lib/utils" -import { UniLink } from "../ui/UniLink" - type HeaderLinkType = { icon?: React.ReactNode label: string | JSX.Element - url?: string - onClick?: () => void -} +} & ( + | { + href: string + } + | { + onClick: React.MouseEventHandler + } +) export const ConnectButton: React.FC<{ left?: boolean @@ -113,7 +116,7 @@ export const ConnectButton: React.FC<{ ? { icon: "icon-[mingcute--home-1-line]", label: t("My xLog") || "", - url: getSiteLink({ + href: getSiteLink({ subdomain: account?.character?.handle || "", }), } @@ -125,12 +128,15 @@ export const ConnectButton: React.FC<{ { icon: "icon-[mingcute--grid-line]", label: t("Dashboard") || "", - url: `${SITE_URL}/dashboard`, + href: `${SITE_URL}/dashboard`, }, { icon: "icon-[mingcute--copy-2-line]", label: t(copyLabelDisplay) || "", - onClick: copyLabel, + onClick: (e) => { + e.preventDefault() + copyLabel() + }, }, ...(account?.type === "wallet" ? [ @@ -242,7 +248,7 @@ export const ConnectButton: React.FC<{ placement="bottom-end" target={ - } - dropdown={ -
- {moreMenuItems.map((item) => { - return ( - - - {item.icon} - - {t(item.text)} - - ) - })} -
- } - /> + {t(item.text)} + + ))} +
@@ -357,3 +346,22 @@ export const SiteHeader: React.FC<{ ) } + +function MoreActions({ children }: React.PropsWithChildren<{}>) { + return ( + + + + } + dropdown={
{children}
} + /> + ) +} + +MoreActions.Item = Menu.Item diff --git a/src/components/ui/Menu.tsx b/src/components/ui/Menu.tsx index ebdc00b2..8a3f0366 100644 --- a/src/components/ui/Menu.tsx +++ b/src/components/ui/Menu.tsx @@ -1,70 +1,114 @@ -import { useState } from "react" +import Link from "next/link" +import { Fragment } from "react" -import { - Placement, - autoUpdate, - flip, - offset, - shift, - useClick, - useDismiss, - useFloating, - useInteractions, - useRole, - useTransitionStyles, -} from "@floating-ui/react" +import { Menu as HeadlessUiMenu } from "@headlessui/react" -export const Menu: React.FC<{ +import { cn } from "~/lib/utils" + +export function Menu({ + target, + dropdown, + placement = "bottom-start", +}: React.PropsWithChildren<{ target: JSX.Element - dropdown: JSX.Element - placement?: Placement -}> = ({ target, dropdown, placement }) => { - const [isOpen, setIsOpen] = useState(false) - - const { x, y, strategy, refs, context } = useFloating({ - placement: placement || "bottom-start", - open: isOpen, - onOpenChange: setIsOpen, - middleware: [offset(5), flip(), shift({ padding: 8 })], - whileElementsMounted: autoUpdate, - }) - - const click = useClick(context) - const { getReferenceProps, getFloatingProps } = useInteractions([ - click, - useRole(context, { role: "tooltip" }), - useDismiss(context), - ]) - - const { isMounted, styles } = useTransitionStyles(context, { - duration: 100, - }) - + dropdown: React.ReactNode + placement?: "bottom-start" | "bottom-end" +}>) { return ( - <> -
+ {target} + - {target} -
- {isMounted && ( -
- {dropdown} -
- )} - + {dropdown} + + + ) +} + +type MenuItemProps = { + icon?: React.ReactNode + className?: string +} & ( + | { + type: "link" + href: string + } + | { + type: "button" + onClick: React.MouseEventHandler + } +) + +Menu.Item = function MenuItem({ + icon, + children, + className: classNameProp, + ...props +}: React.PropsWithChildren) { + const childElement = ( + <> + + {icon} + + {children} + + ) + + return ( + + {({ active }) => { + const className = cn( + "w-full h-10 px-3 flex items-center flex-nowrap", + { + "bg-hover": active, + }, + classNameProp, + ) + + // Can't use here because headlessui Menu.Item assigns `onClick` to its child + if (props.type === "button") { + return ( + + ) + } + if (typeof props.href === "undefined") { + return {childElement} + } + + const isExternal = + /^https?:\/\//.test(props.href) || props.href.startsWith("/feed") + + if (isExternal) { + return ( + + {childElement} + + ) + } + + return ( + + {childElement} + + ) + }} + ) } diff --git a/src/components/ui/UniLink.tsx b/src/components/ui/UniLink.tsx index 5f1468e6..4d2e1a3a 100644 --- a/src/components/ui/UniLink.tsx +++ b/src/components/ui/UniLink.tsx @@ -1,12 +1,21 @@ import Link from "next/link" -export const UniLink: React.FC<{ +export type UniLinkProps = { href?: string onClick?: () => void children: React.ReactNode className?: string target?: string -}> = ({ href, onClick, children, className, target, ...props }) => { +} + +export const UniLink: React.FC = ({ + href, + onClick, + children, + className, + target, + ...props +}) => { if (onClick) { return (