Uniformize folder structure (#693)

* Uniformize folder structure

* Fix icons

* Fix icons

* Fix tests

* Fix tests
This commit is contained in:
Charles Bochet
2023-07-16 14:29:28 -07:00
committed by GitHub
parent 900ec5572f
commit 6ced8434bd
462 changed files with 931 additions and 960 deletions

View File

@ -0,0 +1,23 @@
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
export const INITIAL_HOTKEYS_SCOPES: string[] = [AppHotkeyScope.App];
export const ALWAYS_ON_HOTKEYS_SCOPES: string[] = [
AppHotkeyScope.CommandMenu,
AppHotkeyScope.App,
];
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
commandMenu: true,
goto: false,
};
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
scope: AppHotkeyScope.App,
customScopes: {
commandMenu: true,
goto: true,
},
};

View File

@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { currentHotkeyScopeState } from '@/ui/hotkey/states/internal/currentHotkeyScopeState';
import { AppHotkeyScope } from '../../types/AppHotkeyScope';
import { useHotkeyScopes } from './useHotkeyScopes';
export function useHotkeyScopeAutoSync() {
const { setHotkeyScopes } = useHotkeyScopes();
const currentHotkeyScope = useRecoilValue(currentHotkeyScopeState);
useEffect(() => {
const scopesToSet: string[] = [];
if (currentHotkeyScope.customScopes?.commandMenu) {
scopesToSet.push(AppHotkeyScope.CommandMenu);
}
if (currentHotkeyScope?.customScopes?.goto) {
scopesToSet.push(AppHotkeyScope.Goto);
}
scopesToSet.push(currentHotkeyScope.scope);
setHotkeyScopes(scopesToSet);
}, [setHotkeyScopes, currentHotkeyScope]);
}

View File

@ -0,0 +1,103 @@
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '../../states/internal/internalHotkeysEnabledScopesState';
export function useHotkeyScopes() {
const { disableScope, enableScope } = useHotkeysContext();
const disableAllHotkeyScopes = useRecoilCallback(
({ set, snapshot }) => {
return async () => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
for (const enabledScope of enabledScopes) {
disableScope(enabledScope);
}
set(internalHotkeysEnabledScopesState, []);
};
},
[disableScope],
);
const enableHotkeyScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToEnable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
if (!enabledScopes.includes(scopeToEnable)) {
enableScope(scopeToEnable);
set(internalHotkeysEnabledScopesState, [
...enabledScopes,
scopeToEnable,
]);
}
};
},
[enableScope],
);
const disableHotkeyScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToDisable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopeToRemoveIndex = enabledScopes.findIndex(
(scope) => scope === scopeToDisable,
);
if (scopeToRemoveIndex > -1) {
disableScope(scopeToDisable);
enabledScopes.splice(scopeToRemoveIndex);
set(internalHotkeysEnabledScopesState, enabledScopes);
}
};
},
[disableScope],
);
const setHotkeyScopes = useRecoilCallback(
({ set, snapshot }) => {
return async (scopesToSet: string[]) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopesToDisable = enabledScopes.filter(
(enabledScope) => !scopesToSet.includes(enabledScope),
);
const scopesToEnable = scopesToSet.filter(
(scopeToSet) => !enabledScopes.includes(scopeToSet),
);
for (const scopeToDisable of scopesToDisable) {
disableScope(scopeToDisable);
}
for (const scopeToEnable of scopesToEnable) {
enableScope(scopeToEnable);
}
set(internalHotkeysEnabledScopesState, scopesToSet);
};
},
[disableScope, enableScope],
);
return {
disableAllHotkeyScopes,
enableHotkeyScope,
disableHotkeyScope,
setHotkeyScopes,
};
}

View File

@ -0,0 +1,25 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useNavigate } from 'react-router-dom';
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
export function useGoToHotkeys(key: Keys, location: string) {
const navigate = useNavigate();
useSequenceHotkeys(
'g',
key,
() => {
navigate(location);
},
AppHotkeyScope.Goto,
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[navigate],
);
}

View File

@ -0,0 +1,39 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
import { useSetHotkeyScope } from './useSetHotkeyScope';
export function usePreviousHotkeyScope() {
const [previousHotkeyScope, setPreviousHotkeyScope] =
useState<HotkeyScope | null>();
const setHotkeyScope = useSetHotkeyScope();
const currentHotkeyScope = useRecoilValue(currentHotkeyScopeState);
function goBackToPreviousHotkeyScope() {
if (previousHotkeyScope) {
setHotkeyScope(
previousHotkeyScope.scope,
previousHotkeyScope.customScopes,
);
}
}
function setHotkeyScopeAndMemorizePreviousScope(
scope: string,
customScopes?: CustomHotkeyScopes,
) {
setPreviousHotkeyScope(currentHotkeyScope);
setHotkeyScope(scope, customScopes);
}
return {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
};
}

View File

@ -0,0 +1,43 @@
import { useHotkeys } from 'react-hotkeys-hook';
import {
Hotkey,
HotkeyCallback,
Keys,
Options,
OptionsOrDependencyArray,
} from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
export function useScopedHotkeys(
keys: Keys,
callback: HotkeyCallback,
scope: string,
dependencies?: OptionsOrDependencyArray,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
function callbackIfDirectKey(
keyboardEvent: KeyboardEvent,
hotkeysEvent: Hotkey,
) {
if (!pendingHotkey) {
callback(keyboardEvent, hotkeysEvent);
return;
}
setPendingHotkey(null);
}
return useHotkeys(
keys,
callbackIfDirectKey,
{ ...options, scopes: [scope] },
dependencies,
);
}

View File

@ -0,0 +1,42 @@
import { Options, useHotkeys } from 'react-hotkeys-hook';
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
export function useSequenceHotkeys(
firstKey: Keys,
secondKey: Keys,
callback: () => void,
scope: string,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
deps: any[] = [],
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
useHotkeys(
firstKey,
() => {
setPendingHotkey(firstKey);
},
{ ...options, scopes: [scope] },
[setPendingHotkey],
);
useHotkeys(
secondKey,
() => {
if (pendingHotkey !== firstKey) {
return;
}
setPendingHotkey(null);
callback();
},
{ ...options, scopes: [scope] },
[pendingHotkey, setPendingHotkey, ...deps],
);
}

View File

@ -0,0 +1,59 @@
import { useRecoilCallback } from 'recoil';
import { isDefined } from '~/utils/isDefined';
import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
function isCustomScopesEqual(
customScopesA: CustomHotkeyScopes | undefined,
customScopesB: CustomHotkeyScopes | undefined,
) {
return (
customScopesA?.commandMenu === customScopesB?.commandMenu &&
customScopesA?.goto === customScopesB?.goto
);
}
export function useSetHotkeyScope() {
return useRecoilCallback(
({ snapshot, set }) =>
async (HotkeyScopeToSet: string, customScopes?: CustomHotkeyScopes) => {
const currentHotkeyScope = await snapshot.getPromise(
currentHotkeyScopeState,
);
if (currentHotkeyScope.scope === HotkeyScopeToSet) {
if (!isDefined(customScopes)) {
if (
isCustomScopesEqual(
currentHotkeyScope?.customScopes,
DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES,
)
) {
return;
}
} else {
if (
isCustomScopesEqual(
currentHotkeyScope?.customScopes,
customScopes,
)
) {
return;
}
}
}
set(currentHotkeyScopeState, {
scope: HotkeyScopeToSet,
customScopes: {
commandMenu: customScopes?.commandMenu ?? true,
goto: customScopes?.goto ?? false,
},
});
},
[],
);
}

View File

@ -0,0 +1,9 @@
import { atom } from 'recoil';
import { INITIAL_HOTKEYS_SCOPE } from '../../constants';
import { HotkeyScope } from '../../types/HotkeyScope';
export const currentHotkeyScopeState = atom<HotkeyScope>({
key: 'currentHotkeyScopeState',
default: INITIAL_HOTKEYS_SCOPE,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const internalHotkeysEnabledScopesState = atom<string[]>({
key: 'internalHotkeysEnabledScopesState',
default: [],
});

View File

@ -0,0 +1,7 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { atom } from 'recoil';
export const pendingHotkeyState = atom<Keys | null>({
key: 'pendingHotkeyState',
default: null,
});

View File

@ -0,0 +1,5 @@
export enum AppHotkeyScope {
App = 'app',
Goto = 'goto',
CommandMenu = 'command-menu',
}

View File

@ -0,0 +1,4 @@
export type CustomHotkeyScopes = {
goto?: boolean;
commandMenu?: boolean;
};

View File

@ -0,0 +1,6 @@
import { CustomHotkeyScopes } from './CustomHotkeyScope';
export type HotkeyScope = {
scope: string;
customScopes?: CustomHotkeyScopes;
};

View File

@ -0,0 +1,57 @@
export function isNonTextWritingKey(key: string) {
const nonTextWritingKeys = [
'Enter',
'Tab',
'Shift',
'Escape',
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'Delete',
'Backspace',
'F1',
'F2',
'F3',
'F4',
'F5',
'F6',
'F7',
'F8',
'F9',
'F10',
'F11',
'F12',
'Meta',
'Alt',
'Control',
'CapsLock',
'NumLock',
'ScrollLock',
'Pause',
'Insert',
'Home',
'PageUp',
'Delete',
'End',
'PageDown',
'ContextMenu',
'PrintScreen',
'BrowserBack',
'BrowserForward',
'BrowserRefresh',
'BrowserStop',
'BrowserSearch',
'BrowserFavorites',
'BrowserHome',
'VolumeMute',
'VolumeDown',
'VolumeUp',
'MediaTrackNext',
'MediaTrackPrevious',
'MediaStop',
'MediaPlayPause',
];
return nonTextWritingKeys.includes(key);
}