Create global keyboard shortcut handler (#10145)

This commit is contained in:
DS Boyce
2026-04-03 10:06:51 -07:00
committed by GitHub
parent e0ea778e58
commit cca3326b21
16 changed files with 257 additions and 182 deletions
+18 -8
View File
@@ -7,6 +7,7 @@ import {
useState,
} from 'react'
import {useHotkeysContext} from '#/lib/hotkeys'
import {type DialogControlRefProps} from '#/components/Dialog'
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
import {IS_WEB} from '#/env'
@@ -62,6 +63,7 @@ export function useDialogFullyExpandedCountContext() {
export function Provider({children}: React.PropsWithChildren<{}>) {
const [fullyExpandedCount, setFullyExpandedCount] = useState(0)
const {disableScope, enableScope} = useHotkeysContext()
const activeDialogs = useRef<
Map<string, React.MutableRefObject<DialogControlRefProps>>
@@ -77,18 +79,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return openDialogs.current.size > 0
} else {
BottomSheetNativeComponent.dismissAll()
void BottomSheetNativeComponent.dismissAll()
return false
}
}, [])
const setDialogIsOpen = useCallback((id: string, isOpen: boolean) => {
if (isOpen) {
openDialogs.current.add(id)
} else {
openDialogs.current.delete(id)
}
}, [])
const setDialogIsOpen = useCallback(
(id: string, isOpen: boolean) => {
if (isOpen) {
openDialogs.current.add(id)
} else {
openDialogs.current.delete(id)
}
if (openDialogs.current.size > 0) {
disableScope('global')
} else {
enableScope('global')
}
},
[disableScope, enableScope],
)
const context = useMemo<IDialogContext>(
() => ({
+8
View File
@@ -45,3 +45,11 @@ export function listenPostCreated(fn: () => void): UnlistenFn {
emitter.on('post-created', fn)
return () => emitter.off('post-created', fn)
}
export function emitFocusSearch() {
emitter.emit('focus-search')
}
export function listenFocusSearch(fn: () => void): UnlistenFn {
emitter.on('focus-search', fn)
return () => emitter.off('focus-search', fn)
}
+11 -1
View File
@@ -1,7 +1,8 @@
import {createContext, useContext, useMemo, useState} from 'react'
import {createContext, useContext, useEffect, useMemo, useState} from 'react'
import {nanoid} from 'nanoid/non-secure'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useHotkeysContext} from '#/lib/hotkeys'
import {type ImageSource} from '#/view/com/lightbox/ImageViewing/@types'
export type Lightbox = {
@@ -28,6 +29,15 @@ LightboxControlContext.displayName = 'LightboxControlContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [activeLightbox, setActiveLightbox] = useState<Lightbox | null>(null)
const {disableScope, enableScope} = useHotkeysContext()
useEffect(() => {
if (activeLightbox) {
disableScope('global')
} else {
enableScope('global')
}
}, [activeLightbox, disableScope, enableScope])
const openLightbox = useNonReactiveCallback(
(lightbox: Omit<Lightbox, 'id'>) => {
+11 -1
View File
@@ -1,6 +1,7 @@
import {createContext, useContext, useMemo, useState} from 'react'
import {createContext, useContext, useEffect, useMemo, useState} from 'react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useHotkeysContext} from '#/lib/hotkeys'
export interface UserAddRemoveListsModal {
name: 'user-add-remove-lists'
@@ -47,6 +48,15 @@ ModalControlContext.displayName = 'ModalControlContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [activeModals, setActiveModals] = useState<Modal[]>([])
const {disableScope, enableScope} = useHotkeysContext()
useEffect(() => {
if (activeModals.length > 0) {
disableScope('global')
} else {
enableScope('global')
}
}, [activeModals.length, disableScope, enableScope])
const openModal = useNonReactiveCallback((modal: Modal) => {
setActiveModals(modals => [...modals, modal])
@@ -1,77 +0,0 @@
import {useEffect} from 'react'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useDialogStateContext} from '#/state/dialogs'
import {useLightbox} from '#/state/lightbox'
import {useModals} from '#/state/modals'
import {useSession} from '#/state/session'
import {useIsDrawerOpen} from '#/state/shell/drawer-open'
/**
* Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2}
*/
function shouldIgnore(event: KeyboardEvent) {
const target: any = event.target || event.srcElement
if (!target) return false
const {tagName} = target
if (!tagName) return false
const isInput =
tagName === 'INPUT' &&
![
'checkbox',
'radio',
'range',
'button',
'file',
'reset',
'submit',
'color',
].includes(target.type)
// ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
if (
target.isContentEditable ||
((isInput || tagName === 'TEXTAREA' || tagName === 'SELECT') &&
!target.readOnly)
) {
return true
}
return false
}
export function useComposerKeyboardShortcut() {
const {openComposer} = useOpenComposer()
const {openDialogs} = useDialogStateContext()
const {isModalActive} = useModals()
const {activeLightbox} = useLightbox()
const isDrawerOpen = useIsDrawerOpen()
const {hasSession} = useSession()
useEffect(() => {
if (!hasSession) {
return
}
function handler(event: KeyboardEvent) {
if (shouldIgnore(event)) return
if (
openDialogs?.current.size > 0 ||
isModalActive ||
activeLightbox ||
isDrawerOpen
)
return
if (event.key === 'n' || event.key === 'N') {
openComposer({logContext: 'Other'})
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [
openComposer,
isModalActive,
openDialogs,
activeLightbox,
isDrawerOpen,
hasSession,
])
}
+15 -1
View File
@@ -1,5 +1,7 @@
import {createContext, useContext, useState} from 'react'
import {useHotkeysContext} from '#/lib/hotkeys'
type StateContext = boolean
type SetContext = (v: boolean) => void
@@ -10,10 +12,22 @@ setContext.displayName = 'DrawerOpenSetContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = useState(false)
const {disableScope, enableScope} = useHotkeysContext()
const setDrawerOpen = (open: boolean) => {
if (open) {
disableScope('global')
} else {
enableScope('global')
}
setState(open)
}
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setState}>{children}</setContext.Provider>
<setContext.Provider value={setDrawerOpen}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}