mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: shortcut for focusing search input
This commit is contained in:
parent
3476ed1072
commit
b8866aa415
11 changed files with 232 additions and 127 deletions
|
|
@ -1,8 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import * as DOMHelper from 'utils/DOMHelper'
|
||||
|
||||
export function useFocusFileExplorerOnFirstRender() {
|
||||
React.useEffect(() => {
|
||||
DOMHelper.focusFileExplorer()
|
||||
}, [])
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Label, Text } from '@primer/react'
|
||||
import { useFocusOnPendingTarget } from 'components/FocusTarget'
|
||||
import { LoadingIndicator } from 'components/LoadingIndicator'
|
||||
import { SearchBar } from 'components/SearchBar'
|
||||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
|
|
@ -14,7 +15,6 @@ import { useLoadedContext } from 'utils/hooks/useLoadedContext'
|
|||
import { useOnLocationChange } from 'utils/hooks/useOnLocationChange'
|
||||
import { VisibleNodes, VisibleNodesGenerator } from 'utils/VisibleNodesGenerator'
|
||||
import { SideBarStateContext } from '../../containers/SideBarState'
|
||||
import { useFocusFileExplorerOnFirstRender } from './hooks/useFocusFileExplorerOnFirstRender'
|
||||
import { useGetCurrentPath } from './hooks/useGetCurrentPath'
|
||||
import { useHandleKeyDown } from './hooks/useHandleKeyDown'
|
||||
import {
|
||||
|
|
@ -23,7 +23,7 @@ import {
|
|||
useRenderFileCommentAmounts,
|
||||
useRenderFileStatus,
|
||||
useRenderFindInFolderButton,
|
||||
useRenderGoToButton
|
||||
useRenderGoToButton,
|
||||
} from './hooks/useNodeRenderers'
|
||||
import { useHandleNodeClick } from './hooks/useOnNodeClick'
|
||||
import { useOnSearch } from './hooks/useOnSearch'
|
||||
|
|
@ -153,8 +153,6 @@ function LoadedFileExplorer({
|
|||
])
|
||||
const renderLabelText = useRenderLabelText(searchKey)
|
||||
|
||||
useFocusFileExplorerOnFirstRender()
|
||||
|
||||
const goToCurrentItem = React.useCallback(() => {
|
||||
const targetPath = platform.getCurrentPath(metaData.branchName)
|
||||
if (targetPath) expandTo(targetPath)
|
||||
|
|
@ -163,8 +161,14 @@ function LoadedFileExplorer({
|
|||
useOnLocationChange(goToCurrentItem)
|
||||
useAfterRedirect(goToCurrentItem)
|
||||
|
||||
const ref = React.useRef<HTMLDivElement | null>(null)
|
||||
useFocusOnPendingTarget(
|
||||
'files',
|
||||
React.useCallback(() => ref.current?.focus(), []),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`file-explorer`} tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
<div ref={ref} className={`file-explorer`} tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
{visibleNodesGenerator?.defer && (
|
||||
<div className={'status'}>
|
||||
<Label
|
||||
|
|
|
|||
14
src/components/FocusTarget.tsx
Normal file
14
src/components/FocusTarget.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import * as React from 'react'
|
||||
import { SidebarContext } from './SidebarContext'
|
||||
|
||||
export type FocusTarget = 'files' | 'search' | null
|
||||
|
||||
export function useFocusOnPendingTarget(target: FocusTarget, method: () => void) {
|
||||
const { pendingFocusTarget } = React.useContext(SidebarContext)
|
||||
React.useEffect(() => {
|
||||
if (pendingFocusTarget.value === target) {
|
||||
method()
|
||||
pendingFocusTarget.onChange(null)
|
||||
}
|
||||
}, [target, method, pendingFocusTarget])
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@ import { SearchIcon } from '@primer/octicons-react'
|
|||
import { TextInput, TextInputProps } from '@primer/react'
|
||||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
import * as React from 'react'
|
||||
import { isValidRegexpSource } from 'utils/general'
|
||||
import { formatWithShortcut, isValidRegexpSource } from 'utils/general'
|
||||
import { useFocusOnPendingTarget } from './FocusTarget'
|
||||
import { SearchMode } from './searchModes'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -11,8 +12,14 @@ type Props = {
|
|||
} & Required<Pick<TextInputProps, 'onFocus'>>
|
||||
|
||||
export function SearchBar({ onSearch, onFocus, value }: Props) {
|
||||
const ref = React.useRef<HTMLInputElement | null>(null)
|
||||
useFocusOnPendingTarget(
|
||||
'search',
|
||||
React.useCallback(() => ref.current?.focus(), []),
|
||||
)
|
||||
|
||||
const configs = useConfigs()
|
||||
const { searchMode } = configs.value
|
||||
const { searchMode, focusSearchInputShortcut } = configs.value
|
||||
|
||||
const toggleButtonDescription =
|
||||
searchMode === 'regex'
|
||||
|
|
@ -26,6 +33,7 @@ export function SearchBar({ onSearch, onFocus, value }: Props) {
|
|||
|
||||
return (
|
||||
<TextInput
|
||||
ref={ref}
|
||||
leadingVisual={SearchIcon}
|
||||
onFocus={e => {
|
||||
onFocus(e)
|
||||
|
|
@ -35,7 +43,7 @@ export function SearchBar({ onSearch, onFocus, value }: Props) {
|
|||
sx={{ borderRadius: 0 }}
|
||||
className={'search-input'}
|
||||
aria-label="search files"
|
||||
placeholder={`Search files`}
|
||||
placeholder={formatWithShortcut(`Search files`, focusSearchInputShortcut)}
|
||||
onChange={({ target: { value } }) => onSearch(value, searchMode)}
|
||||
value={value}
|
||||
validationStatus={validationStatus}
|
||||
|
|
|
|||
|
|
@ -14,18 +14,21 @@ import { Config } from 'utils/config/helper'
|
|||
import { cx } from 'utils/cx'
|
||||
import * as DOMHelper from 'utils/DOMHelper'
|
||||
import * as features from 'utils/features'
|
||||
import { detectBrowser } from 'utils/general'
|
||||
import { detectBrowser, formatWithShortcut } from 'utils/general'
|
||||
import { useConditionalHook } from 'utils/hooks/useConditionalHook'
|
||||
import { useAfterRedirect, usePJAXAPI } from 'utils/hooks/useFastRedirect'
|
||||
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
|
||||
import { ResizeState } from 'utils/hooks/useResizeHandler'
|
||||
import * as keyHelper from 'utils/keyHelper'
|
||||
import { useStateIO } from 'utils/hooks/useStateIO'
|
||||
import { SideBarErrorContext } from '../containers/ErrorContext'
|
||||
import { SideBarStateContext } from '../containers/SideBarState'
|
||||
import { Theme } from '../containers/Theme'
|
||||
import { useOnShortcutPressed } from '../utils/hooks/useOnShortcutPressed'
|
||||
import { FocusTarget } from './FocusTarget'
|
||||
import { LoadingIndicator } from './LoadingIndicator'
|
||||
import { RoundIconButton } from './RoundIconButton'
|
||||
import { SettingsBarContent } from './settings/SettingsBar'
|
||||
import { SidebarContext } from './SidebarContext'
|
||||
import { SideBarResizeHandler } from './SideBarResizeHandler'
|
||||
|
||||
export function SideBar() {
|
||||
|
|
@ -36,11 +39,19 @@ export function SideBar() {
|
|||
const error = useLoadedContext(SideBarErrorContext).value
|
||||
|
||||
const [shouldExpand, setShouldExpand, toggleShowSideBar] = useShouldExpand()
|
||||
useFocusSidebarOnExpand(shouldExpand)
|
||||
const pendingFocusTarget = useStateIO<FocusTarget>(null)
|
||||
useShowSidebarKeyboard(
|
||||
shouldExpand,
|
||||
setShouldExpand,
|
||||
toggleShowSideBar,
|
||||
pendingFocusTarget.onChange,
|
||||
)
|
||||
|
||||
const configContext = useConfigs()
|
||||
|
||||
const blockLeaveRef = React.useRef(false)
|
||||
const { sidebarToggleMode } = configContext.value
|
||||
const { sidebarToggleMode, shortcut, focusSearchInputShortcut } = configContext.value
|
||||
const onResizeStateChange = React.useCallback((state: ResizeState) => {
|
||||
blockLeaveRef.current = state === 'resizing'
|
||||
}, [])
|
||||
|
|
@ -50,110 +61,126 @@ export function SideBar() {
|
|||
() => useWindowSize().height, // eslint-disable-line react-hooks/rules-of-hooks
|
||||
)
|
||||
|
||||
const sidebarContextValue = React.useMemo(() => ({ pendingFocusTarget }), [pendingFocusTarget])
|
||||
|
||||
return (
|
||||
<Theme>
|
||||
<IIFC>
|
||||
{() => {
|
||||
const logoContainerElement = useLogoContainerElement()
|
||||
return (
|
||||
<Portal into={logoContainerElement}>
|
||||
<ToggleShowButton
|
||||
error={error}
|
||||
className={cx({
|
||||
hidden: shouldExpand,
|
||||
})}
|
||||
onHover={sidebarToggleMode === 'float' ? () => setShouldExpand(true) : undefined}
|
||||
onClick={toggleShowSideBar}
|
||||
/>
|
||||
</Portal>
|
||||
)
|
||||
}}
|
||||
</IIFC>
|
||||
<div className={'gitako-side-bar'}>
|
||||
<div
|
||||
className={cx('gitako-side-bar-body-wrapper', `toggle-mode-${sidebarToggleMode}`, {
|
||||
collapsed: error || !shouldExpand,
|
||||
})}
|
||||
style={{ height: heightForSafari }}
|
||||
onMouseLeave={() => {
|
||||
if (blockLeaveRef.current) return
|
||||
if (sidebarToggleMode === 'float') setShouldExpand(false)
|
||||
<SidebarContext.Provider value={sidebarContextValue}>
|
||||
<IIFC>
|
||||
{() => {
|
||||
const logoContainerElement = useLogoContainerElement()
|
||||
return (
|
||||
<Portal into={logoContainerElement}>
|
||||
<ToggleShowButton
|
||||
error={error}
|
||||
className={cx({
|
||||
hidden: shouldExpand,
|
||||
})}
|
||||
onHover={sidebarToggleMode === 'float' ? () => setShouldExpand(true) : undefined}
|
||||
onClick={toggleShowSideBar}
|
||||
/>
|
||||
</Portal>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div className={'gitako-side-bar-body'}>
|
||||
<div className={'gitako-side-bar-content'}>
|
||||
<div className={'header'}>
|
||||
<div className={'side-bar-position-controls'}>
|
||||
{sidebarToggleMode === 'persistent' && (
|
||||
</IIFC>
|
||||
<div className={'gitako-side-bar'}>
|
||||
<div
|
||||
className={cx('gitako-side-bar-body-wrapper', `toggle-mode-${sidebarToggleMode}`, {
|
||||
collapsed: error || !shouldExpand,
|
||||
})}
|
||||
style={{ height: heightForSafari }}
|
||||
onMouseLeave={() => {
|
||||
if (blockLeaveRef.current) return
|
||||
if (sidebarToggleMode === 'float') setShouldExpand(false)
|
||||
}}
|
||||
>
|
||||
<div className={'gitako-side-bar-body'}>
|
||||
<div className={'gitako-side-bar-content'}>
|
||||
<div className={'header'}>
|
||||
<div className={'side-bar-position-controls'}>
|
||||
{sidebarToggleMode === 'persistent' && (
|
||||
<RoundIconButton
|
||||
icon={TabIcon}
|
||||
aria-label={formatWithShortcut('Collapse sidebar', shortcut)}
|
||||
sx={{
|
||||
transform: 'rotateY(180deg)',
|
||||
}}
|
||||
onClick={toggleShowSideBar}
|
||||
/>
|
||||
)}
|
||||
<RoundIconButton
|
||||
icon={TabIcon}
|
||||
aria-label={'Collapse sidebar'}
|
||||
icon={PinIcon}
|
||||
aria-label={'Toggle sidebar dock mode between float and persistent'}
|
||||
iconColor={sidebarToggleMode === 'persistent' ? 'fg.default' : undefined}
|
||||
sx={{
|
||||
transform: 'rotateY(180deg)',
|
||||
}}
|
||||
onClick={toggleShowSideBar}
|
||||
onClick={() =>
|
||||
configContext.onChange({
|
||||
sidebarToggleMode:
|
||||
sidebarToggleMode === 'persistent' ? 'float' : 'persistent',
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<RoundIconButton
|
||||
icon={PinIcon}
|
||||
aria-label={'Toggle sidebar dock mode between float and persistent'}
|
||||
iconColor={sidebarToggleMode === 'persistent' ? 'fg.default' : undefined}
|
||||
sx={{
|
||||
transform: 'rotateY(180deg)',
|
||||
}}
|
||||
onClick={() =>
|
||||
configContext.onChange({
|
||||
sidebarToggleMode:
|
||||
sidebarToggleMode === 'persistent' ? 'float' : 'persistent',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<MetaBar />
|
||||
</div>
|
||||
<MetaBar />
|
||||
<IIFC>
|
||||
{() => {
|
||||
switch (useLoadedContext(SideBarStateContext).value) {
|
||||
case 'getting-access-token':
|
||||
return <LoadingIndicator text={'Getting access token...'} />
|
||||
case 'after-getting-access-token':
|
||||
case 'meta-loading':
|
||||
return <LoadingIndicator text={'Fetching repo meta...'} />
|
||||
case 'error-due-to-auth':
|
||||
return <AccessDeniedDescription />
|
||||
case 'meta-loaded':
|
||||
case 'tree-loading':
|
||||
case 'tree-rendering':
|
||||
case 'tree-rendered':
|
||||
return <FileExplorer />
|
||||
}
|
||||
}}
|
||||
</IIFC>
|
||||
</div>
|
||||
<IIFC>
|
||||
{() => {
|
||||
switch (useLoadedContext(SideBarStateContext).value) {
|
||||
case 'getting-access-token':
|
||||
return <LoadingIndicator text={'Getting access token...'} />
|
||||
case 'after-getting-access-token':
|
||||
case 'meta-loading':
|
||||
return <LoadingIndicator text={'Fetching repo meta...'} />
|
||||
case 'error-due-to-auth':
|
||||
return <AccessDeniedDescription />
|
||||
case 'meta-loaded':
|
||||
case 'tree-loading':
|
||||
case 'tree-rendering':
|
||||
case 'tree-rendered':
|
||||
return <FileExplorer />
|
||||
}
|
||||
const [showSettings, setShowSettings] = React.useState(false)
|
||||
const toggleShowSettings = React.useCallback(
|
||||
() => setShowSettings(show => !show),
|
||||
[],
|
||||
)
|
||||
|
||||
useOnShortcutPressed(
|
||||
focusSearchInputShortcut,
|
||||
React.useCallback(() => setShowSettings(false), []),
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSettings && <SettingsBarContent toggleShow={toggleShowSettings} />}
|
||||
<Footer toggleShowSettings={toggleShowSettings} />
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</IIFC>
|
||||
</div>
|
||||
<IIFC>
|
||||
{() => {
|
||||
const [showSettings, setShowSettings] = React.useState(false)
|
||||
const toggleShowSettings = React.useCallback(
|
||||
() => setShowSettings(show => !show),
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSettings && <SettingsBarContent toggleShow={toggleShowSettings} />}
|
||||
<Footer toggleShowSettings={toggleShowSettings} />
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</IIFC>
|
||||
{features.resize && <SideBarResizeHandler onResizeStateChange={onResizeStateChange} />}
|
||||
</div>
|
||||
{features.resize && <SideBarResizeHandler onResizeStateChange={onResizeStateChange} />}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarContext.Provider>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
|
||||
function useFocusSidebarOnExpand(shouldExpand: boolean) {
|
||||
React.useEffect(() => {
|
||||
// prevent keeping focus within Gitako
|
||||
if (!shouldExpand) document.body.focus()
|
||||
}, [shouldExpand])
|
||||
}
|
||||
|
||||
function useMarkGitakoReadyState() {
|
||||
React.useEffect(() => {
|
||||
DOMHelper.markGitakoReadyState(true)
|
||||
|
|
@ -221,25 +248,6 @@ function useSaveExpandStateOnToggle(shouldExpand: boolean) {
|
|||
}, [shouldExpand, intelligentToggle]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}
|
||||
|
||||
function useToggleSideBarWithKeyboard(toggleShowSideBar: () => void) {
|
||||
const { shortcut } = useConfigs().value
|
||||
const state = useLoadedContext(SideBarStateContext).value
|
||||
const isDisabled = state === 'disabled' || !shortcut
|
||||
React.useEffect(
|
||||
function attachKeyDown() {
|
||||
if (isDisabled) return
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const keys = keyHelper.parseEvent(e)
|
||||
if (keys === shortcut) toggleShowSideBar()
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
},
|
||||
[toggleShowSideBar, isDisabled, shortcut],
|
||||
)
|
||||
}
|
||||
|
||||
function useCollapseOnNoPermissionWhenTokenHasBeenSet(
|
||||
setShowSideBar: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
) {
|
||||
|
|
@ -266,12 +274,40 @@ function useShouldExpand() {
|
|||
useSaveExpandStateOnToggle(shouldExpand)
|
||||
useUpdateBodyIndentOnStateUpdate(shouldExpand)
|
||||
useUpdateBodyIndentAfterRedirect(setShouldExpand)
|
||||
useToggleSideBarWithKeyboard(toggleShowSideBar)
|
||||
useCollapseOnNoPermissionWhenTokenHasBeenSet(setShouldExpand)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldExpand) DOMHelper.focusFileExplorer() // TODO: verify if it works
|
||||
}, [shouldExpand])
|
||||
|
||||
return [shouldExpand, setShouldExpand, toggleShowSideBar] as const
|
||||
}
|
||||
|
||||
function useShowSidebarKeyboard(
|
||||
shouldExpand: boolean,
|
||||
setShouldExpand: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
toggleShowSideBar: () => void,
|
||||
setFocusTarget: React.Dispatch<React.SetStateAction<FocusTarget>>,
|
||||
) {
|
||||
const config = useConfigs().value
|
||||
|
||||
useOnShortcutPressed(
|
||||
config.shortcut,
|
||||
React.useCallback(
|
||||
e => {
|
||||
DOMHelper.cancelEvent(e)
|
||||
toggleShowSideBar()
|
||||
if (!shouldExpand) setFocusTarget('files')
|
||||
},
|
||||
[shouldExpand, toggleShowSideBar, setFocusTarget],
|
||||
),
|
||||
)
|
||||
|
||||
useOnShortcutPressed(
|
||||
config.focusSearchInputShortcut,
|
||||
React.useCallback(
|
||||
e => {
|
||||
DOMHelper.cancelEvent(e)
|
||||
if (!shouldExpand) setShouldExpand(true)
|
||||
setFocusTarget('search')
|
||||
},
|
||||
[shouldExpand, setShouldExpand, setFocusTarget],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
10
src/components/SidebarContext.tsx
Normal file
10
src/components/SidebarContext.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react'
|
||||
import { noop } from 'utils/general'
|
||||
import { FocusTarget } from './FocusTarget'
|
||||
|
||||
// Use this to pass state across components under Sidebar
|
||||
export const SidebarContext = React.createContext<{
|
||||
pendingFocusTarget: IO<FocusTarget>
|
||||
}>({
|
||||
pendingFocusTarget: { onChange: noop, value: null },
|
||||
})
|
||||
|
|
@ -14,6 +14,10 @@ export function SidebarSettings() {
|
|||
label={'Keyboard shortcut to toggle visibility'}
|
||||
{...subIO(useConfigs(), 'shortcut')}
|
||||
/>
|
||||
<KeyboardShortcutSetting
|
||||
label={'Keyboard shortcut to focus search input'}
|
||||
{...subIO(useConfigs(), 'focusSearchInputShortcut')}
|
||||
/>
|
||||
<SimpleConfigFieldCheckbox
|
||||
field={{
|
||||
key: 'intelligentToggle',
|
||||
|
|
|
|||
|
|
@ -179,3 +179,8 @@ export function formatClass(className: string) {
|
|||
export function parseIntFromElement(e: HTMLElement): number {
|
||||
return parseInt((e.innerText || '').replace(/[^0-9]/g, ''))
|
||||
}
|
||||
|
||||
export function cancelEvent(e: KeyboardEvent): void {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { migrateConfig } from './migrations'
|
|||
|
||||
export type Config = {
|
||||
sideBarWidth: number
|
||||
shortcut: string | undefined
|
||||
shortcut: string | undefined // shortcut for toggling sidebar
|
||||
focusSearchInputShortcut: string | undefined // shortcut for focusing search input
|
||||
accessToken: string | undefined
|
||||
compressSingletonFolder: boolean
|
||||
copyFileButton: boolean
|
||||
|
|
@ -29,6 +30,7 @@ export type ConfigKeys = keyof Config
|
|||
enum configKeys {
|
||||
sideBarWidth = 'sideBarWidth',
|
||||
shortcut = 'shortcut',
|
||||
focusSearchInputShortcut = 'focusSearchInputShortcut',
|
||||
accessToken = 'accessToken',
|
||||
compressSingletonFolder = 'compressSingletonFolder',
|
||||
copyFileButton = 'copyFileButton',
|
||||
|
|
@ -54,6 +56,7 @@ const isInGitHub = platformStorageKey === 'platform_github.com'
|
|||
export const getDefaultConfigs: () => Config = () => ({
|
||||
sideBarWidth: 260,
|
||||
shortcut: undefined,
|
||||
focusSearchInputShortcut: undefined,
|
||||
accessToken: '',
|
||||
compressSingletonFolder: true,
|
||||
copyFileButton: !isInGitHub, // disable on github.com
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ export function friendlyFormatShortcut(shortcut?: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export function formatWithShortcut(prefix: string, shortcut?: string) {
|
||||
return shortcut ? `${prefix} (${friendlyFormatShortcut(shortcut)})` : prefix
|
||||
}
|
||||
|
||||
export async function traverse<T>(
|
||||
range: T[] = [],
|
||||
conditionAndEffect: (node: T) => Async<boolean>,
|
||||
|
|
|
|||
25
src/utils/hooks/useOnShortcutPressed.tsx
Normal file
25
src/utils/hooks/useOnShortcutPressed.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from 'react'
|
||||
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
|
||||
import * as keyHelper from 'utils/keyHelper'
|
||||
import { SideBarStateContext } from '../../containers/SideBarState'
|
||||
|
||||
export function useOnShortcutPressed(
|
||||
shortcut: string | undefined,
|
||||
onPressed: (e: KeyboardEvent) => void,
|
||||
) {
|
||||
const state = useLoadedContext(SideBarStateContext).value
|
||||
const isDisabled = state === 'disabled' || !shortcut
|
||||
React.useEffect(
|
||||
function attachKeyDown() {
|
||||
if (isDisabled) return
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const keys = keyHelper.parseEvent(e)
|
||||
if (keys === shortcut) onPressed(e)
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
},
|
||||
[onPressed, isDisabled, shortcut],
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue