このコミットが含まれているのは:
2026-07-05 03:43:37 +09:00
コミット 4e3fb6bef7
5個のファイルの変更276行の追加84行の削除
+63 -26
ファイルの表示
@@ -9,10 +9,9 @@ import {
} from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { toast } from '@/components/ui/use-toast'
import ShortcutHelpDialog from '@/components/users/ShortcutHelpDialog'
import {
DEFAULT_KEY_BINDINGS,
formatKeyBinding,
getConflictingShortcutActionIds,
isEditableEventTarget,
keyBindingFromKeyboardEvent,
@@ -46,6 +45,7 @@ type KeyboardShortcutsContextValue = {
keyboardSettings: ClientKeyboardSettings
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>
conflictActionIds: Set<ShortcutActionId>
saveKeyboardSettings: (settings: ClientKeyboardSettings) => void
setEnabled: (enabled: boolean) => void
setBinding: (actionId: ShortcutActionId, binding: KeyBinding | null) => void
resetBinding: (actionId: ShortcutActionId) => void
@@ -57,17 +57,47 @@ const KeyboardShortcutsContext =
createContext<KeyboardShortcutsContextValue | null> (null)
const buildShortcutHelpText = (
activeScope: ShortcutScope,
effectiveBindings: Record<ShortcutActionId, KeyBinding | null>,
): string =>
SHORTCUT_DEFINITIONS
.filter (definition =>
definition.scope === 'global' || definition.scope === activeScope)
.filter (definition => effectiveBindings[definition.id] != null)
.map (definition =>
`${ definition.label }: ${ formatKeyBinding (effectiveBindings[definition.id]) }`)
.join (' / ')
const isFocusableShortcutTarget = (element: HTMLElement): boolean => {
if (element.closest ('[hidden], [aria-hidden="true"]') != null)
return false
if (
(element instanceof HTMLInputElement
|| element instanceof HTMLButtonElement
|| element instanceof HTMLSelectElement
|| element instanceof HTMLTextAreaElement)
&& element.disabled
)
return false
const style = window.getComputedStyle (element)
if (
style.display === 'none'
|| style.visibility === 'hidden'
|| style.pointerEvents === 'none'
)
return false
return element.getClientRects ().length > 0
}
const focusSearchTarget = (): void => {
const targets = [
...document.querySelectorAll<HTMLElement> (
'[data-shortcut-focus-search="true"]',
),
]
const target = targets.find (isFocusableShortcutTarget)
if (target == null)
return
target.focus ()
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
target.select ()
}
export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
@@ -78,6 +108,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
useState<ClientKeyboardSettings> (() => getClientKeyboardSettings ())
const [registeredHandlers, setRegisteredHandlers] =
useState<Record<number, ShortcutHandlers>> ({ })
const [shortcutHelpOpen, setShortcutHelpOpen] = useState (false)
const registrationIdRef = useRef (0)
const activeScope = useMemo<ShortcutScope> (
@@ -108,6 +139,11 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
setKeyboardSettingsState (next)
}, [keyboardSettings])
const saveKeyboardSettings = useCallback ((settings: ClientKeyboardSettings) => {
const next = setClientKeyboardSettings (settings)
setKeyboardSettingsState (next)
}, [])
const setBinding = useCallback ((
actionId: ShortcutActionId,
binding: KeyBinding | null,
@@ -157,6 +193,9 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
if (isEditableEventTarget (event.target))
return
if (shortcutHelpOpen)
return
const binding = keyBindingFromKeyboardEvent (event)
if (binding == null)
return
@@ -183,21 +222,10 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
const builtinHandler = (
{
'global.openShortcutHelp': () => {
const description =
buildShortcutHelpText (activeScope, effectiveBindings)
|| 'この画面で使えるショートカットはありません.'
toast ({
title: 'キーボード・ショートカット',
description,
})
setShortcutHelpOpen (true)
},
'global.focusSearch': () => {
const target = document.querySelector<HTMLElement> (
'[data-shortcut-focus-search="true"]',
)
target?.focus ()
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
target.select ()
focusSearchTarget ()
},
'navigation.posts': () => navigate ('/posts'),
'navigation.tags': () => navigate ('/tags'),
@@ -226,6 +254,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
keyboardSettings.enabled,
mergedRegisteredHandlers,
navigate,
shortcutHelpOpen,
])
const value = useMemo<KeyboardShortcutsContextValue> (() => ({
@@ -234,6 +263,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
effectiveBindings,
conflictActionIds,
setEnabled,
saveKeyboardSettings,
setBinding,
resetBinding,
resetAllBindings,
@@ -246,6 +276,7 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
registerHandlers,
resetAllBindings,
resetBinding,
saveKeyboardSettings,
setBinding,
setEnabled,
])
@@ -253,6 +284,12 @@ export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
return (
<KeyboardShortcutsContext.Provider value={value}>
{children}
<ShortcutHelpDialog
open={shortcutHelpOpen}
onOpenChange={setShortcutHelpOpen}
activeScope={activeScope}
effectiveBindings={effectiveBindings}
conflictActionIds={conflictActionIds}/>
</KeyboardShortcutsContext.Provider>)
}