[web] interviews/sidebar: make the product menu open on hover (#952)

This commit is contained in:
Nitesh Seram 2024-11-15 10:31:47 +05:30 committed by GitHub
parent f73368ebdc
commit fc88020f2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 142 additions and 91 deletions

View File

@ -1,9 +1,10 @@
import clsx from 'clsx';
import React from 'react';
import React, { useState } from 'react';
import { RiArrowDownSLine } from 'react-icons/ri';
import { useDebounce } from 'usehooks-ts';
import LogoMark from '~/components/global/logos/LogoMark';
import NavProductDropdownMenuContent from '~/components/global/navbar/NavProductDropdownMenuContent';
import NavProductPopoverContent from '~/components/global/navbar/NavProductPopoverContent';
import Anchor from '~/components/ui/Anchor';
import Divider from '~/components/ui/Divider';
import Text, { textVariants } from '~/components/ui/Text';
@ -18,7 +19,7 @@ import {
import LogoComboMark from '../logos/LogoComboMark';
import { useProductMenuUnseenIndicator } from '../product-theme/useProductMenuUnseenIndicator';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import * as PopoverPrimitive from '@radix-ui/react-popover';
type ProductValue = 'interviews' | 'projects';
@ -42,13 +43,24 @@ type Props = Readonly<{
variant: 'compact' | 'full';
}>;
export default function NavProductDropdownMenu({
export default function NavProductPopover({
variant,
product,
triggerClassname,
}: Props) {
const { label } = labels[product];
const [showUnseenIndicator] = useProductMenuUnseenIndicator();
const [open, setOpen] = useState(false);
// To debounce open state when quick hovering on and out
const debouncedOpen = useDebounce(open, 100);
function handleMouseEnter() {
setOpen(true);
}
function handleMouseLeave() {
setOpen(false);
}
return (
<div
@ -67,8 +79,11 @@ export default function NavProductDropdownMenu({
color="emphasized"
direction="vertical"
/>
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild={true}>
<PopoverPrimitive.Root open={debouncedOpen} onOpenChange={setOpen}>
<PopoverPrimitive.Trigger
asChild={true}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<button
className={clsx(
'-ml-2',
@ -107,16 +122,20 @@ export default function NavProductDropdownMenu({
className={clsx('size-5 shrink-0', themeTextSubtleColor)}
/>
</button>
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Portal>
<NavProductDropdownMenuContent product={product} />
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<NavProductPopoverContent
product={product}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
</div>
);
}
export function NavProductDropdownMenuLogoOnly({
export function NavProductPopoverLogoOnly({
product,
triggerClassname,
}: Readonly<{
@ -124,10 +143,24 @@ export function NavProductDropdownMenuLogoOnly({
triggerClassname?: string;
}>) {
const [showUnseenIndicator] = useProductMenuUnseenIndicator();
const [open, setOpen] = useState(false);
// To debounce open state when quick hovering on and out
const debouncedOpen = useDebounce(open, 100);
function handleMouseEnter() {
setOpen(true);
}
function handleMouseLeave() {
setOpen(false);
}
return (
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild={true}>
<PopoverPrimitive.Root open={debouncedOpen} onOpenChange={setOpen}>
<PopoverPrimitive.Trigger
asChild={true}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<Anchor
aria-label="Select product"
className={clsx(
@ -159,10 +192,14 @@ export function NavProductDropdownMenuLogoOnly({
/>
)}
</Anchor>
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Portal>
<NavProductDropdownMenuContent product={product} />
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<NavProductPopoverContent
product={product}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
}

View File

@ -19,11 +19,11 @@ import {
import LogoComboMark from '../logos/LogoComboMark';
import { useProductMenuUnseenIndicator } from '../product-theme/useProductMenuUnseenIndicator';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import * as PopoverPrimitive from '@radix-ui/react-popover';
type ProductValue = 'interviews' | 'projects';
function NavProductDropdownMenuItem({
function NavProductMenuItem({
beta = false,
href,
label,
@ -37,8 +37,8 @@ function NavProductDropdownMenuItem({
showNewIndicator: boolean;
}>) {
return (
<DropdownMenuPrimitive.Item
asChild={true}
<Anchor
aria-label={product}
className={clsx(
'relative flex flex-col gap-3',
'rounded',
@ -46,80 +46,78 @@ function NavProductDropdownMenuItem({
'select-none outline-none',
themeBackgroundElementEmphasizedStateColor_Hover,
themeBackgroundElementEmphasizedStateColor_Focus,
)}>
<Anchor
aria-label={product}
href={href}
locale="en-US"
prefetch={null}
variant="unstyled">
<div className="flex h-5 items-center justify-between">
<div className="relative flex items-center gap-2.5">
<LogoComboMark height={17} />
<Divider
className="h-3.5"
color="emphasized"
direction="vertical"
)}
href={href}
locale="en-US"
prefetch={null}
variant="unstyled">
<div className="flex h-5 items-center justify-between">
<div className="relative flex items-center gap-2.5">
<LogoComboMark height={17} />
<Divider className="h-3.5" color="emphasized" direction="vertical" />
<Text className="text-[13px]" size="inherit" weight="bold">
{label}
</Text>
{showNewIndicator && (
<span
aria-hidden={true}
className={clsx(
'size-1 inline-block',
'bg-red rounded-full',
'absolute -right-1.5 top-1',
)}
/>
<Text className="text-[13px]" size="inherit" weight="bold">
{label}
</Text>
{showNewIndicator && (
<span
aria-hidden={true}
className={clsx(
'size-1 inline-block',
'bg-red rounded-full',
'absolute -right-1.5 top-1',
)}
/>
)}
</div>
{beta && (
<span className="flex">
<Badge label="Beta" size="xs" variant="primary" />
</span>
)}
</div>
</Anchor>
</DropdownMenuPrimitive.Item>
{beta && (
<span className="flex">
<Badge label="Beta" size="xs" variant="primary" />
</span>
)}
</div>
</Anchor>
);
}
type Props = Readonly<{ product: ProductValue }>;
type Props = PopoverPrimitive.PopoverContentProps &
Readonly<{
product: ProductValue;
}>;
const roadmapLinks: Record<ProductValue, string> = {
interviews: '/interviews/roadmap',
projects: '/projects/roadmap',
};
export default function NavProductDropdownMenuContent({ product }: Props) {
export default function NavProductPopoverContent({ product, ...props }: Props) {
const items = useCommonNavItems();
const [showUnseenIndicator] = useProductMenuUnseenIndicator();
return (
<DropdownMenuPrimitive.Content
<PopoverPrimitive.Content
align="start"
className={clsx(
'flex flex-col',
'w-[360px] rounded-lg',
'w-[322px] rounded-lg',
['border', themeBorderElementColor],
themeBackgroundElementColor,
'z-dropdown',
'outline-none',
)}
sideOffset={8}>
sideOffset={8}
{...props}>
<div className={clsx('flex flex-col gap-3', 'px-5 pb-3 pt-4')}>
<Text color="secondary" size="body3">
Products
</Text>
<div className={clsx('flex flex-col gap-1.5')}>
<NavProductDropdownMenuItem
<NavProductMenuItem
href="/"
label="Interviews"
product="GreatFrontEnd Interviews"
showNewIndicator={false}
/>
<NavProductDropdownMenuItem
<NavProductMenuItem
beta={true}
href="/projects"
label="Projects"
@ -182,6 +180,6 @@ export default function NavProductDropdownMenuContent({ product }: Props) {
))}
</div>
</div>
</DropdownMenuPrimitive.Content>
</PopoverPrimitive.Content>
);
}

View File

@ -1,9 +1,11 @@
import clsx from 'clsx';
import { useState } from 'react';
import { RiArrowDownSLine } from 'react-icons/ri';
import { useDebounce } from 'usehooks-ts';
import LogoMark from '~/components/global/logos/LogoMark';
import ProjectsLogo from '~/components/global/logos/ProjectsLogo';
import NavProductDropdownMenuContent from '~/components/global/navbar/NavProductDropdownMenuContent';
import NavProductPopoverContent from '~/components/global/navbar/NavProductPopoverContent';
import {
themeBackgroundElementEmphasizedStateColor_Hover,
themeBackgroundElementPressedStateColor_Active,
@ -14,7 +16,7 @@ import {
import { useProductMenuUnseenIndicator } from '../product-theme/useProductMenuUnseenIndicator';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import * as PopoverPrimitive from '@radix-ui/react-popover';
type Props = Readonly<{
product: 'interviews' | 'projects';
@ -34,7 +36,7 @@ const buttonBaseClassname = clsx(
themeBackgroundElementPressedStateColor_Active,
);
export default function NavProductDropdownMenu_DEPRECATED({
export default function NavProductPopover_DEPRECATED({
variant,
product,
}: Props) {
@ -43,10 +45,24 @@ export default function NavProductDropdownMenu_DEPRECATED({
}
const [showUnseenIndicator] = useProductMenuUnseenIndicator();
const [open, setOpen] = useState(false);
// To debounce open state when quick hovering on and out
const debouncedOpen = useDebounce(open, 100);
function handleMouseEnter() {
setOpen(true);
}
function handleMouseLeave() {
setOpen(false);
}
return (
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild={true}>
<PopoverPrimitive.Root open={debouncedOpen} onOpenChange={setOpen}>
<PopoverPrimitive.Trigger
asChild={true}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
{variant === 'full' ? (
<button
aria-label="Select product"
@ -101,10 +117,14 @@ export default function NavProductDropdownMenu_DEPRECATED({
)}
</button>
)}
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Portal>
<NavProductDropdownMenuContent product={product} />
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<NavProductPopoverContent
product={product}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
}

View File

@ -25,8 +25,8 @@ import SidebarColorSchemeSubMenu from './SidebarColorSchemeSubMenu';
import type { SidebarLinkEntity } from './SidebarLinksSection';
import SidebarLinksSection from './SidebarLinksSection';
import NavProductDropdownMenu, {
NavProductDropdownMenuLogoOnly,
} from '../navbar/NavProductDropdownMenu';
NavProductPopoverLogoOnly,
} from '../navbar/NavProductPopover';
export function SidebarCollapsed({
moreMenuItems,
@ -61,7 +61,7 @@ export function SidebarCollapsed({
'py-4',
)}>
<div className="pb-8">
<NavProductDropdownMenuLogoOnly product={product} />
<NavProductPopoverLogoOnly product={product} />
</div>
{topAddonElements}
<ul className="flex grow flex-col gap-1">

View File

@ -20,7 +20,7 @@ import type { NavbarTopLevelItem } from '~/components/ui/Navbar/NavTypes';
import SidebarAuthDropdownItem from './SidebarAuthDropdownItem';
import SidebarColorSchemeSubMenu from './SidebarColorSchemeSubMenu';
import NavProductDropdownMenu_DEPRECATED from '../navbar/NavProductDropdownMenu_DEPRECATED';
import NavProductPopover_DEPRECATED from '../navbar/NavProductPopover_DEPRECATED';
export type SidebarItem = NavbarTopLevelItem;
@ -38,9 +38,7 @@ export function SidebarCollapsed({
moreMenuItems: React.ReactElement | false | null | undefined;
notificationItem?: React.ReactElement | false | null | undefined;
onCollapseClick: () => void;
product: React.ComponentProps<
typeof NavProductDropdownMenu_DEPRECATED
>['product'];
product: React.ComponentProps<typeof NavProductPopover_DEPRECATED>['product'];
showPremiumDiscord: boolean;
sidebarItems: SidebarItems;
topAddonElements?: React.ReactNode;
@ -56,7 +54,7 @@ export function SidebarCollapsed({
'relative h-full',
'px-3 py-4',
)}>
<NavProductDropdownMenu_DEPRECATED product={product} variant="compact" />
<NavProductPopover_DEPRECATED product={product} variant="compact" />
{topAddonElements}
<ul className="flex grow flex-col gap-1">
{startItems.map((item) => (
@ -156,9 +154,7 @@ export function SidebarExpanded({
moreMenuItems: React.ReactElement | false | null | undefined;
notificationItem?: React.ReactElement | false | null | undefined;
onCollapseClick?: () => void;
product: React.ComponentProps<
typeof NavProductDropdownMenu_DEPRECATED
>['product'];
product: React.ComponentProps<typeof NavProductPopover_DEPRECATED>['product'];
renderBottomAddonElements?: (fadeInClassname: string) => React.ReactNode;
renderTopAddonElements?: (fadeInClassname: string) => React.ReactNode;
sidebarItems: SidebarItems;
@ -173,7 +169,7 @@ export function SidebarExpanded({
return (
<nav className={clsx('flex flex-col gap-y-4', 'relative h-full p-4')}>
<NavProductDropdownMenu_DEPRECATED product={product} variant="full" />
<NavProductPopover_DEPRECATED product={product} variant="full" />
{renderTopAddonElements?.(fadeInClass)}
<ul className={clsx('flex grow flex-col gap-2', fadeInClass)}>
{startItems.map((item) => (

View File

@ -10,7 +10,7 @@ import useUserProfile from '~/hooks/user/useUserProfile';
import { useColorSchemePreferences } from '~/components/global/color-scheme/ColorSchemePreferencesProvider';
import ColorSchemeSelect from '~/components/global/color-scheme/ColorSchemeSelect';
import I18nSelect from '~/components/global/i18n/I18nSelect';
import NavProductDropdownMenu from '~/components/global/navbar/NavProductDropdownMenu';
import NavProductPopover from '~/components/global/navbar/NavProductPopover';
import { useIntl } from '~/components/intl';
import Anchor from '~/components/ui/Anchor';
import Avatar from '~/components/ui/Avatar';
@ -145,7 +145,7 @@ export default function InterviewsNavbar({
isLoading={isUserProfileLoading}
links={navLinksFull}
logo={
<NavProductDropdownMenu
<NavProductPopover
product="interviews"
triggerClassname="-ml-2"
variant="full"

View File

@ -13,7 +13,7 @@ import useCommonNavItems from '~/components/common/navigation/useCommonNavItems'
import { useColorSchemePreferences } from '~/components/global/color-scheme/ColorSchemePreferencesProvider';
import ColorSchemeSelect from '~/components/global/color-scheme/ColorSchemeSelect';
import NavColorSchemeDropdown from '~/components/global/navbar/NavColorSchemeDropdown';
import NavProductDropdownMenu from '~/components/global/navbar/NavProductDropdownMenu';
import NavProductPopover from '~/components/global/navbar/NavProductPopover';
import NavProfileIcon from '~/components/global/navbar/NavProfileIcon';
import { useIntl } from '~/components/intl';
import useProjectsNotificationUnreadCount from '~/components/projects/notifications/hooks/useProjectsNotificationUnreadCount';
@ -260,7 +260,7 @@ export default function ProjectsNavbar({ hideOnDesktop = false }: Props) {
isLoading={isUserProfileLoading}
links={navLinks}
logo={
<NavProductDropdownMenu
<NavProductPopover
product="projects"
triggerClassname="-ml-2"
variant="full"