[web] interviews/sidebar: make the product menu open on hover (#952)
This commit is contained in:
parent
f73368ebdc
commit
fc88020f2f
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue