diff --git a/src-web/components/EnvironmentActionsDropdown.tsx b/src-web/components/EnvironmentActionsDropdown.tsx index 565bffd3..6d04f44d 100644 --- a/src-web/components/EnvironmentActionsDropdown.tsx +++ b/src-web/components/EnvironmentActionsDropdown.tsx @@ -4,7 +4,6 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useAppRoutes } from '../hooks/useAppRoutes'; import { useCreateEnvironment } from '../hooks/useCreateEnvironment'; import { useEnvironments } from '../hooks/useEnvironments'; -import { useHotkey } from '../hooks/useHotkey'; import type { ButtonProps } from './core/Button'; import { Button } from './core/Button'; import type { DropdownItem } from './core/Dropdown'; @@ -35,8 +34,6 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo }); }, [dialog, activeEnvironment]); - useHotkey('environmentEditor.toggle', showEnvironmentDialog, { enable: environments.length > 0 }); - const items: DropdownItem[] = useMemo( () => [ ...environments.map( diff --git a/src-web/components/KeyboardShortcutsDialog.tsx b/src-web/components/KeyboardShortcutsDialog.tsx index d380e980..323d07f3 100644 --- a/src-web/components/KeyboardShortcutsDialog.tsx +++ b/src-web/components/KeyboardShortcutsDialog.tsx @@ -1,4 +1,4 @@ -import { hotkeyActions } from '../hooks/useHotkey'; +import { hotkeyActions } from '../hooks/useHotKey'; import { HotKeyList } from './core/HotKeyList'; export const KeyboardShortcutsDialog = () => { diff --git a/src-web/components/SettingsDropdown.tsx b/src-web/components/SettingsDropdown.tsx index 5bc8ca2e..06f2be1c 100644 --- a/src-web/components/SettingsDropdown.tsx +++ b/src-web/components/SettingsDropdown.tsx @@ -2,7 +2,6 @@ import { invoke, shell } from '@tauri-apps/api'; import { useRef } from 'react'; import { useAppVersion } from '../hooks/useAppVersion'; import { useExportData } from '../hooks/useExportData'; -import { useHotkey } from '../hooks/useHotkey'; import { useImportData } from '../hooks/useImportData'; import { useTheme } from '../hooks/useTheme'; import { useUpdateMode } from '../hooks/useUpdateMode'; @@ -24,17 +23,6 @@ export function SettingsDropdown() { const dropdownRef = useRef(null); const dialog = useDialog(); - const showHotkeyHelp = () => { - dialog.show({ - id: 'hotkey-help', - title: 'Keyboard Shortcuts', - size: 'sm', - render: () => , - }); - }; - - useHotkey('hotkeys.showHelp', showHotkeyHelp); - return ( { + dialog.show({ + id: 'hotkey-help', + title: 'Keyboard Shortcuts', + size: 'sm', + render: () => , + }); + }, leftSlot: , }, { type: 'separator', label: `Yaak v${appVersion.data}` }, diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 5912beef..89f20f10 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -16,7 +16,7 @@ import { useDeleteFolder } from '../hooks/useDeleteFolder'; import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useDuplicateRequest } from '../hooks/useDuplicateRequest'; import { useFolders } from '../hooks/useFolders'; -import { useHotkey } from '../hooks/useHotkey'; +import { useHotKey } from '../hooks/useHotKey'; import { useKeyValue } from '../hooks/useKeyValue'; import { useLatestResponse } from '../hooks/useLatestResponse'; import { usePrompt } from '../hooks/usePrompt'; @@ -55,7 +55,6 @@ export function Sidebar({ className }: Props) { const { hidden } = useSidebarHidden(); const sidebarRef = useRef(null); const activeRequestId = useActiveRequestId(); - const duplicateRequest = useDuplicateRequest({ id: activeRequestId ?? '', navigateAfter: true }); const activeEnvironmentId = useActiveEnvironmentId(); const requests = useRequests(); const folders = useFolders(); @@ -76,8 +75,6 @@ export function Sidebar({ className }: Props) { namespace: NAMESPACE_NO_SYNC, }); - useHotkey('request.duplicate', () => duplicateRequest.mutate()); - const isCollapsed = useCallback( (id: string) => collapsed.value?.[id] ?? false, [collapsed.value], @@ -209,7 +206,7 @@ export function Sidebar({ className }: Props) { useKeyPressEvent('Backspace', handleDeleteKey); useKeyPressEvent('Delete', handleDeleteKey); - useHotkey('sidebar.focus', () => { + useHotKey('sidebar.focus', () => { if (hidden || hasFocus) return; // Select 0 index on focus if none selected focusActiveRequest( @@ -649,10 +646,7 @@ const SidebarItem = forwardRef(function SidebarItem( label: 'Duplicate', hotkeyAction: 'request.duplicate', leftSlot: , - onSelect: () => { - console.log('DUPLICATE'); - duplicateRequest.mutate(); - }, + onSelect: () => duplicateRequest.mutate(), }, { key: 'deleteRequest', diff --git a/src-web/components/SidebarActions.tsx b/src-web/components/SidebarActions.tsx index e47aa997..a56a0fa1 100644 --- a/src-web/components/SidebarActions.tsx +++ b/src-web/components/SidebarActions.tsx @@ -1,7 +1,6 @@ import { memo } from 'react'; import { useCreateFolder } from '../hooks/useCreateFolder'; import { useCreateRequest } from '../hooks/useCreateRequest'; -import { useHotkey } from '../hooks/useHotkey'; import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { Dropdown } from './core/Dropdown'; import { IconButton } from './core/IconButton'; @@ -12,8 +11,6 @@ export const SidebarActions = memo(function SidebarActions() { const createFolder = useCreateFolder(); const { hidden, toggle } = useSidebarHidden(); - useHotkey('request.create', () => createRequest.mutate({})); - return ( { + useHotKey('urlBar.focus', () => { const head = inputRef.current?.state.doc.length ?? 0; inputRef.current?.dispatch({ selection: { anchor: 0, head }, diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index f6a3deae..aad93023 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -1,8 +1,8 @@ import classNames from 'classnames'; import type { HTMLAttributes, ReactNode } from 'react'; import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'; -import type { HotkeyAction } from '../../hooks/useHotkey'; -import { useFormattedHotkey, useHotkey } from '../../hooks/useHotkey'; +import type { HotkeyAction } from '../../hooks/useHotKey'; +import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey'; import { Icon } from './Icon'; const colorStyles = { @@ -80,7 +80,7 @@ export const Button = forwardRef(function Button () => buttonRef.current, ); - useHotkey(hotkeyAction ?? null, () => { + useHotKey(hotkeyAction ?? null, () => { buttonRef.current?.click(); }); diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index b481828b..21971f49 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -20,7 +20,8 @@ import React, { useState, } from 'react'; import { useKey, useKeyPressEvent, useWindowSize } from 'react-use'; -import type { HotkeyAction } from '../../hooks/useHotkey'; +import type { HotkeyAction } from '../../hooks/useHotKey'; +import { useHotKey } from '../../hooks/useHotKey'; import { Overlay } from '../Overlay'; import { Button } from './Button'; import { HotKey } from './HotKey'; @@ -50,6 +51,7 @@ export type DropdownItem = DropdownItemDefault | DropdownItemSeparator; export interface DropdownProps { children: ReactElement>; items: DropdownItem[]; + openOnHotKeyAction?: HotkeyAction; } export interface DropdownRef { @@ -63,20 +65,24 @@ export interface DropdownRef { } export const Dropdown = forwardRef(function Dropdown( - { children, items }: DropdownProps, + { children, items, openOnHotKeyAction }: DropdownProps, ref, ) { - const [open, setOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); const [defaultSelectedIndex, setDefaultSelectedIndex] = useState(); const buttonRef = useRef(null); const menuRef = useRef>(null); + useHotKey(openOnHotKeyAction ?? null, () => { + setIsOpen(true); + }); + useImperativeHandle(ref, () => ({ ...menuRef.current, - isOpen: open, + isOpen: isOpen, toggle(activeIndex?: number) { - if (!open) this.open(activeIndex); - else setOpen(false); + if (!isOpen) this.open(activeIndex); + else setIsOpen(false); }, open(activeIndex?: number) { if (activeIndex === undefined) { @@ -84,7 +90,7 @@ export const Dropdown = forwardRef(function Dropdown } else { setDefaultSelectedIndex(activeIndex >= 0 ? activeIndex : items.length + activeIndex); } - setOpen(true); + setIsOpen(true); }, })); @@ -101,41 +107,40 @@ export const Dropdown = forwardRef(function Dropdown e.preventDefault(); e.stopPropagation(); setDefaultSelectedIndex(undefined); - setOpen((o) => !o); + setIsOpen((o) => !o); }), }; return cloneElement(existingChild, props); }, [children]); const handleClose = useCallback(() => { - setOpen(false); + setIsOpen(false); buttonRef.current?.focus(); }, []); useEffect(() => { - buttonRef.current?.setAttribute('aria-expanded', open.toString()); - }, [open]); + buttonRef.current?.setAttribute('aria-expanded', isOpen.toString()); + }, [isOpen]); const windowSize = useWindowSize(); const triggerRect = useMemo(() => { if (!windowSize) return null; // No-op to TS happy with this dep - if (!open) return null; + if (!isOpen) return null; return buttonRef.current?.getBoundingClientRect(); - }, [open, windowSize]); + }, [isOpen, windowSize]); return ( <> {child} - {open && triggerRect && ( - - )} + ); }); @@ -161,15 +166,12 @@ export const ContextMenu = forwardRef(function Co [show], ); - if (show === null) { - return null; - } - return ( @@ -180,13 +182,22 @@ interface MenuProps { className?: string; defaultSelectedIndex?: number; items: DropdownProps['items']; - triggerShape: Pick; + triggerShape: Pick | null; onClose: () => void; showTriangle?: boolean; + isOpen: boolean; } const Menu = forwardRef, MenuProps>(function Menu( - { className, items, onClose, triggerShape, defaultSelectedIndex, showTriangle }: MenuProps, + { + className, + isOpen, + items, + onClose, + triggerShape, + defaultSelectedIndex, + showTriangle, + }: MenuProps, ref, ) { const containerRef = useRef(null); @@ -291,6 +302,8 @@ const Menu = forwardRef, MenuPro containerStyles: CSSProperties; triangleStyles: CSSProperties | null; }>(() => { + if (triggerShape == null) return { containerStyles: {}, triangleStyles: null }; + const docRect = document.documentElement.getBoundingClientRect(); const width = triggerShape.right - triggerShape.left; const hSpaceRemaining = docRect.width - triggerShape.left; @@ -322,61 +335,76 @@ const Menu = forwardRef, MenuPro if (items.length === 0) return null; return ( - -
-
- - {triangleStyles && showTriangle && ( - + {items.map( + (item) => + item.type !== 'separator' && ( + - )} - {containerStyles && ( - +
+
+ - {items.map((item, i) => { - if (item.type === 'separator') { - return ; - } - if (item.hidden) { - return null; - } - return ( - - ); - })} - - )} - -
- + {triangleStyles && showTriangle && ( + + )} + {containerStyles && ( + + {items.map((item, i) => { + if (item.type === 'separator') { + return ; + } + if (item.hidden) { + return null; + } + return ( + + ); + })} + + )} + +
+ + )} + ); }); @@ -443,3 +471,19 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men ); } + +interface MenuItemHotKeyProps { + action: HotkeyAction | undefined; + onSelect: MenuItemProps['onSelect']; + item: MenuItemProps['item']; +} + +function MenuItemHotKey({ action, onSelect, item }: MenuItemHotKeyProps) { + if (action) { + console.log('MENU ITEM HOTKEY', action, item); + } + useHotKey(action ?? null, () => { + onSelect(item); + }); + return null; +} diff --git a/src-web/components/core/HotKey.tsx b/src-web/components/core/HotKey.tsx index bacf438a..148d04c0 100644 --- a/src-web/components/core/HotKey.tsx +++ b/src-web/components/core/HotKey.tsx @@ -1,7 +1,8 @@ import classNames from 'classnames'; -import type { HotkeyAction } from '../../hooks/useHotkey'; -import { useFormattedHotkey } from '../../hooks/useHotkey'; +import type { HotkeyAction } from '../../hooks/useHotKey'; +import { useFormattedHotkey } from '../../hooks/useHotKey'; import { useOsInfo } from '../../hooks/useOsInfo'; +import { HStack } from './Stacks'; interface Props { action: HotkeyAction | null; @@ -17,14 +18,18 @@ export function HotKey({ action, className, variant }: Props) { } return ( - - {label} - + {label.split('').map((char, index) => ( +
+ {char} +
+ ))} + ); } diff --git a/src-web/components/core/HotKeyLabel.tsx b/src-web/components/core/HotKeyLabel.tsx index d39b71da..a6e291f0 100644 --- a/src-web/components/core/HotKeyLabel.tsx +++ b/src-web/components/core/HotKeyLabel.tsx @@ -1,8 +1,8 @@ -import type { HotkeyAction } from '../../hooks/useHotkey'; -import { useHotKeyLabel } from '../../hooks/useHotkey'; +import type { HotkeyAction } from '../../hooks/useHotKey'; +import { useHotKeyLabel } from '../../hooks/useHotKey'; interface Props { - action: HotkeyAction | null; + action: HotkeyAction; } export function HotKeyLabel({ action }: Props) { diff --git a/src-web/components/core/HotKeyList.tsx b/src-web/components/core/HotKeyList.tsx index 3e4be281..20bd6f8f 100644 --- a/src-web/components/core/HotKeyList.tsx +++ b/src-web/components/core/HotKeyList.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import type { HotkeyAction } from '../../hooks/useHotkey'; +import type { HotkeyAction } from '../../hooks/useHotKey'; import { HotKey } from './HotKey'; import { HotKeyLabel } from './HotKeyLabel'; +import { HStack, VStack } from './Stacks'; interface Props { hotkeys: HotkeyAction[]; @@ -10,14 +11,14 @@ interface Props { export const HotKeyList = ({ hotkeys }: Props) => { return (
-
+ {hotkeys.map((hotkey) => ( -
+ -
+ ))} -
+
); }; diff --git a/src-web/hooks/useHotkey.ts b/src-web/hooks/useHotKey.ts similarity index 87% rename from src-web/hooks/useHotkey.ts rename to src-web/hooks/useHotKey.ts index 257d69ab..332dbd9d 100644 --- a/src-web/hooks/useHotkey.ts +++ b/src-web/hooks/useHotKey.ts @@ -24,13 +24,24 @@ const hotkeys: Record = { 'hotkeys.showHelp': ['CmdCtrl+/'], }; +const hotkeyLabels: Record = { + 'request.send': 'Send Request', + 'request.create': 'New Request', + 'request.duplicate': 'Duplicate Request', + 'sidebar.toggle': 'Toggle Sidebar', + 'sidebar.focus': 'Focus Sidebar', + 'urlBar.focus': 'Focus URL', + 'environmentEditor.toggle': 'Edit Environments', + 'hotkeys.showHelp': 'Show Hotkeys', +}; + export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[]; interface Options { enable?: boolean; } -export function useHotkey( +export function useHotKey( action: HotkeyAction | null, callback: (e: KeyboardEvent) => void, options: Options = {}, @@ -97,25 +108,8 @@ export function useAnyHotkey( }, [options.enable, os]); } -export function useHotKeyLabel(action: HotkeyAction | null): string { - switch (action) { - case 'request.send': - return 'Send Request'; - case 'request.create': - return 'New Request'; - case 'request.duplicate': - return 'Duplicate Request'; - case 'sidebar.toggle': - return 'Toggle Sidebar'; - case 'sidebar.focus': - return 'Focus Sidebar'; - case 'urlBar.focus': - return 'Focus URL'; - case 'environmentEditor.toggle': - return 'Edit Environments'; - default: - return 'Unknown'; - } +export function useHotKeyLabel(action: HotkeyAction): string { + return hotkeyLabels[action]; } export function useFormattedHotkey(action: HotkeyAction | null): string | null {