Move key commands to <AppViewSwitcher />, fix auto-focus tooltip issue

This commit is contained in:
Sam Becker 2025-04-25 20:04:12 -05:00
parent 8a72e3d7ce
commit 63e843e2d6
8 changed files with 42 additions and 54 deletions

View File

@ -24,7 +24,6 @@ import AdminUploadPanel from '@/admin/upload/AdminUploadPanel';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import RecipeModal from '@/recipe/RecipeModal'; import RecipeModal from '@/recipe/RecipeModal';
import ThemeColors from '@/app/ThemeColors'; import ThemeColors from '@/app/ThemeColors';
import AppKeyListener from '@/app/AppKeyListener';
import '../tailwind.css'; import '../tailwind.css';
@ -115,7 +114,6 @@ export default function RootLayout({
</SwrConfigClient> </SwrConfigClient>
<Analytics debug={false} /> <Analytics debug={false} />
<SpeedInsights debug={false} /> <SpeedInsights debug={false} />
<AppKeyListener />
<PhotoEscapeHandler /> <PhotoEscapeHandler />
<ToasterWithThemes /> <ToasterWithThemes />
</ThemeProvider> </ThemeProvider>

View File

@ -1,28 +0,0 @@
'use client';
import useKeydownHandler from '@/utility/useKeydownHandler';
import { PATH_FEED_INFERRED, PATH_GRID_INFERRED } from './paths';
import { useCallback } from 'react';
import { useRouter, usePathname } from 'next/navigation';
const PATH_MAPS = {
G: PATH_GRID_INFERRED,
F: PATH_FEED_INFERRED,
};
export default function AppKeyListener() {
const pathname = usePathname();
const router = useRouter();
const onKeyDown = useCallback((e: KeyboardEvent) => {
const path = PATH_MAPS[e.key.toLocaleUpperCase() as keyof typeof PATH_MAPS];
if (path && pathname !== path) {
router.push(path);
}
}, [router, pathname]);
useKeydownHandler({ onKeyDown });
return null;
}

View File

@ -12,7 +12,9 @@ import { GRID_HOMEPAGE_ENABLED } from './config';
import AdminAppMenu from '@/admin/AdminAppMenu'; import AdminAppMenu from '@/admin/AdminAppMenu';
import Spinner from '@/components/Spinner'; import Spinner from '@/components/Spinner';
import clsx from 'clsx/lite'; import clsx from 'clsx/lite';
import { useState, useRef, useEffect } from 'react'; import { useCallback, useRef, useState } from 'react';
import useKeydownHandler from '@/utility/useKeydownHandler';
import { usePathname } from 'next/navigation';
export type SwitcherSelection = 'feed' | 'grid' | 'admin'; export type SwitcherSelection = 'feed' | 'grid' | 'admin';
@ -23,32 +25,39 @@ export default function AppViewSwitcher({
currentSelection?: SwitcherSelection currentSelection?: SwitcherSelection
className?: string className?: string
}) { }) {
const pathname = usePathname();
const { const {
isUserSignedIn, isUserSignedIn,
isUserSignedInEager, isUserSignedInEager,
setIsCommandKOpen, setIsCommandKOpen,
} = useAppState(); } = useAppState();
const hasAdminMenuOpenedOnce = useRef(false); const refHrefFeed = useRef<HTMLAnchorElement>(null);
const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false); const refHrefGrid = useRef<HTMLAnchorElement>(null);
useEffect(() => { const onKeyDown = useCallback((e: KeyboardEvent) => {
if (isAdminMenuOpen) { switch (e.key.toLocaleUpperCase()) {
hasAdminMenuOpenedOnce.current = true; case 'F':
} else if (hasAdminMenuOpenedOnce.current) { if (pathname !== PATH_FEED_INFERRED) { refHrefFeed.current?.click(); }
// Blur admin menu button to avoid tooltip on dismiss break;
setTimeout(() => { case 'G':
if (document.activeElement instanceof HTMLElement) { if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); }
document.activeElement.blur(); break;
} case 'A':
}, 50); if (isUserSignedIn) { setIsAdminMenuOpen(true); }
break;
} }
}, [isAdminMenuOpen]); }, [pathname, isUserSignedIn]);
useKeydownHandler({ onKeyDown });
const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false);
const renderItemFeed = const renderItemFeed =
<SwitcherItem <SwitcherItem
icon={<IconFeed includeTitle={false} />} icon={<IconFeed includeTitle={false} />}
href={PATH_FEED_INFERRED} href={PATH_FEED_INFERRED}
hrefRef={refHrefFeed}
active={currentSelection === 'feed'} active={currentSelection === 'feed'}
tooltip={{ tooltip={{
content: 'Feed', content: 'Feed',
@ -61,6 +70,7 @@ export default function AppViewSwitcher({
<SwitcherItem <SwitcherItem
icon={<IconGrid includeTitle={false} />} icon={<IconGrid includeTitle={false} />}
href={PATH_GRID_INFERRED} href={PATH_GRID_INFERRED}
hrefRef={refHrefGrid}
active={currentSelection === 'grid'} active={currentSelection === 'grid'}
tooltip={{ tooltip={{
content: 'Grid', content: 'Grid',
@ -93,7 +103,10 @@ export default function AppViewSwitcher({
isOpen={isAdminMenuOpen} isOpen={isAdminMenuOpen}
setIsOpen={setIsAdminMenuOpen} setIsOpen={setIsAdminMenuOpen}
/>} />}
tooltip={{ content: !isAdminMenuOpen ? 'Admin Menu' : undefined }} tooltip={{
content: !isAdminMenuOpen ? 'Admin Menu' : undefined,
keyCommand: !isAdminMenuOpen ? 'A' : undefined,
}}
noPadding noPadding
/>} />}
</Switcher> </Switcher>

View File

@ -61,7 +61,10 @@ export default function Modal({
}, },
}); });
useEscapeHandler(onClose, true); useEscapeHandler({
onKeyDown: onClose,
ignoreShouldRespondToKeyboardCommands: true,
});
return ( return (
<motion.div <motion.div

View File

@ -1,6 +1,6 @@
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config'; import { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config';
import { ComponentProps, ReactNode } from 'react'; import { ComponentProps, ReactNode, RefObject } from 'react';
import Spinner from './Spinner'; import Spinner from './Spinner';
import LinkWithIconLoader from './LinkWithIconLoader'; import LinkWithIconLoader from './LinkWithIconLoader';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
@ -11,6 +11,7 @@ export default function SwitcherItem({
icon, icon,
title, title,
href, href,
hrefRef,
className: classNameProp, className: classNameProp,
onClick, onClick,
active, active,
@ -22,6 +23,7 @@ export default function SwitcherItem({
icon: ReactNode icon: ReactNode
title?: string title?: string
href?: string href?: string
hrefRef?: RefObject<HTMLAnchorElement | null>
className?: string className?: string
onClick?: () => void onClick?: () => void
active?: boolean active?: boolean
@ -57,6 +59,7 @@ export default function SwitcherItem({
const content = href const content = href
? <LinkWithIconLoader {...{ ? <LinkWithIconLoader {...{
href, href,
ref: hrefRef,
title, title,
className, className,
prefetch, prefetch,
@ -72,7 +75,7 @@ export default function SwitcherItem({
? <Tooltip ? <Tooltip
{...tooltip} {...tooltip}
classNameTrigger={WIDTH_CLASS} classNameTrigger={WIDTH_CLASS}
delayDuration={300} delayDuration={500}
> >
{content} {content}
</Tooltip> </Tooltip>

View File

@ -75,6 +75,7 @@ export default function MoreMenu({
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content <DropdownMenu.Content
{...props} {...props}
onCloseAutoFocus={e => e.preventDefault()}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={clsx( className={clsx(

View File

@ -12,11 +12,11 @@ export default function PhotoEscapeHandler() {
const escapePath = getEscapePath(pathname); const escapePath = getEscapePath(pathname);
const escapeHandler = useCallback(() => { const onKeyDown = useCallback(() => {
if (escapePath) { router.push(escapePath, { scroll: false }); } if (escapePath) { router.push(escapePath, { scroll: false }); }
}, [escapePath, router]); }, [escapePath, router]);
useEscapeHandler(escapeHandler); useEscapeHandler({ onKeyDown });
return null; return null;
} }

View File

@ -1,12 +1,10 @@
import useKeydownHandler from '@/utility/useKeydownHandler'; import useKeydownHandler from '@/utility/useKeydownHandler';
export default function useEscapeHandler( export default function useEscapeHandler(
onKeyDown?: (e: KeyboardEvent) => void, args: Omit<Parameters<typeof useKeydownHandler>[0], 'keys'>,
ignoreShouldRespondToKeyboardCommands?: boolean,
) { ) {
useKeydownHandler({ useKeydownHandler({
onKeyDown, ...args,
keys: ['ESCAPE'], keys: ['ESCAPE'],
ignoreShouldRespondToKeyboardCommands,
}); });
} }