Skip to content

Commit

Permalink
refactor(KeyboardShortcuts): move code into separate folder
Browse files Browse the repository at this point in the history
  • Loading branch information
maxpatiiuk committed Jul 21, 2024
1 parent ad4c501 commit d854c51
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 125 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { KeyboardShortcuts, ModifierKey } from '../KeyboardContext';
import { exportsForTests, keyJoinSymbol } from '../KeyboardContext';
import type { GenericPreferences } from '../types';
import { userPreferenceDefinitions } from '../UserDefinitions';
import type { KeyboardShortcuts, ModifierKey } from '../context';
import { exportsForTests, keyJoinSymbol } from '../context';
import type { GenericPreferences } from '../../Preferences/types';
import { userPreferenceDefinitions } from '../../Preferences/UserDefinitions';

const { keysToString, modifierKeys, specialKeys } = exportsForTests;

Expand Down
72 changes: 72 additions & 0 deletions specifyweb/frontend/js_src/lib/components/Keyboard/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { preferencesText } from '../../localization/preferences';
import type { RA, RR } from '../../utils/types';

/**
* Because operating systems, browsers and browser extensions define many
* keyboard shortcuts, many of which differ between operating systems, the set
* of free keyboard shortcuts is quite small so it's difficult to have one
* shortcut that works on all 3 platforms.
*
* To provide flexibility, without complicating the UI for people who only use
* Specify on a single platform, we do the following:
* - UI allows you to set keyboard shortcuts for the current platform only
* - If you set keyboard shortcut on any platform, that shortcut is used on all
* platforms, unless you explicitly edited the shortcut on the other platform
* - If keyboard shortcut was not explicitly set, the default shortcut, if any
* will be used
*/
export type KeyboardShortcuts = Partial<
RR<KeyboardPlatform, RA<string> | undefined>
>;

type KeyboardPlatform = 'mac' | 'other' | 'windows';
export const keyboardPlatform: KeyboardPlatform =
navigator.platform.toLowerCase().includes('mac') ||
// Check for iphone || ipad || ipod
navigator.platform.toLowerCase().includes('ip')
? 'mac'
: navigator.platform.toLowerCase().includes('win')
? 'windows'
: 'other';

const modifierKeys = ['Alt', 'Ctrl', 'Meta', 'Shift'] as const;
export type ModifierKey = typeof modifierKeys[number];
export const allModifierKeys = new Set([
...modifierKeys,
'AltGraph',
'CapsLock',
]);

export const keyboardModifierLocalization: RR<ModifierKey, string> = {
Alt:
keyboardPlatform === 'mac'
? preferencesText.macOption()
: preferencesText.alt(),
Ctrl:
keyboardPlatform === 'mac'
? preferencesText.macControl()
: preferencesText.ctrl(),
// This key should never appear in non-mac platforms
Meta: preferencesText.macMeta(),
Shift:
keyboardPlatform === 'mac'
? preferencesText.macShift()
: preferencesText.shift(),
};

/**
* Do not allow binding a keyboard shortcut that includes only one of these
* keys, without any modifier.
*
* For example, do not allow binding keyboard shortcuts to Tab key. That key is
* important for accessibility and for keyboard navigation. Without it
* you won't be able to tab your way to the "Save" button to save the
* keyboard shortcut)
*/
export const specialKeyboardKeys = new Set([
'Enter',
'Tab',
' ',
'Escape',
'Backspace',
]);
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,10 @@
* Allows to register a key listener
*/

import type { RA, RR } from '../../utils/types';

/**
* Because operating systems, browsers and browser extensions define many
* keyboard shortcuts, many of which differ between operating systems, the set
* of free keyboard shortcuts is quite small so it's difficult to have one
* shortcut that works on all 3 platforms.
*
* To provide flexibility, without complicating the UI for people who only use
* Specify on a single platform, we do the following:
* - UI allows you to set keyboard shortcuts for the current platform only
* - If you set keyboard shortcut on any platform, that shortcut is used on all
* platforms, unless you explicitly edited the shortcut on the other platform
* - If keyboard shortcut was not explicitly set, the default shortcut, if any
* will be used
*/
export type KeyboardShortcuts = Partial<
RR<KeyboardPlatform, RA<string> | undefined>
>;

type KeyboardPlatform = 'mac' | 'other' | 'windows';
export const keyboardPlatform: KeyboardPlatform =
navigator.platform.toLowerCase().includes('mac') ||
// Check for iphone || ipad || ipod
navigator.platform.toLowerCase().includes('ip')
? 'mac'
: navigator.platform.toLowerCase().includes('win')
? 'windows'
: 'other';

const modifierKeys = ['Alt', 'Ctrl', 'Meta', 'Shift'] as const;
export type ModifierKey = typeof modifierKeys[number];
const allModifierKeys = new Set([...modifierKeys, 'AltGraph', 'CapsLock']);

/**
* Do not allow binding a keyboard shortcut that includes only one of these
* keys, without any modifier.
*
* For example, do not allow binding keyboard shortcuts to Tab key. That key is
* important for accessibility and for keyboard navigation. Without it
* you won't be able to tab your way to the "Save" button to save the
* keyboard shortcut)
*/
const specialKeys = new Set(['Enter', 'Tab', ' ', 'Escape', 'Backspace']);
import type { RA } from '../../utils/types';
import type { KeyboardShortcuts, ModifierKey } from './config';
import { allModifierKeys, specialKeyboardKeys } from './config';
import { resolvePlatformShortcuts } from './utils';

/**
* To keep the event listener as fast as possible, we are not looping though all
Expand Down Expand Up @@ -89,41 +49,6 @@ export function bindKeyboardShortcut(
});
}

/**
* If there is a keyboard shortcut defined for current system, use it
* (also, if current system explicitly has empty array of shortcuts, use it).
*
* Otherwise, use the keyboard shortcut from one of the other platforms if set,
* but change meta to ctrl and vice versa as necessary.
*/
export function resolvePlatformShortcuts(
shortcut: KeyboardShortcuts
): RA<string> | undefined {
if ('platform' in shortcut) return shortcut[keyboardPlatform];
else if ('other' in shortcut)
return keyboardPlatform === 'windows'
? shortcut.other
: shortcut.other?.map(replaceCtrlWithMeta);
else if ('windows' in shortcut)
return keyboardPlatform === 'other'
? shortcut.other
: shortcut.other?.map(replaceCtrlWithMeta);
else if ('mac' in shortcut) return shortcut.other?.map(replaceMetaWithCtrl);
else return undefined;
}

const replaceCtrlWithMeta = (shortcut: string): string =>
shortcut
.split(keyJoinSymbol)
.map((key) => (key === 'ctrl' ? 'meta' : key))
.join(keyJoinSymbol);

const replaceMetaWithCtrl = (shortcut: string): string =>
shortcut
.split(keyJoinSymbol)
.map((key) => (key === 'meta' ? 'ctrl' : key))
.join(keyJoinSymbol);

/**
* Assumes keys and modifiers are sorted
*/
Expand All @@ -148,7 +73,7 @@ document.addEventListener('keydown', (event) => {
// Ignore shortcuts that result in printed characters when in an input field
const ignore = isPrintable && isEntering;
if (ignore) return;
if (modifiers.length === 0 && specialKeys.has(event.key)) return;
if (modifiers.length === 0 && specialKeyboardKeys.has(event.key)) return;

const keyString = keysToString(modifiers, pressedKeys);
const handler = interceptor ?? listeners.get(keyString);
Expand Down Expand Up @@ -243,6 +168,4 @@ document.addEventListener('visibilitychange', () => {

export const exportsForTests = {
keysToString,
modifierKeys,
specialKeys,
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,36 @@
*/

import React from 'react';
import type { LocalizedString } from 'typesafe-i18n';

import { useTriggerState } from '../../hooks/useTriggerState';
import { commonText } from '../../localization/common';
import { preferencesText } from '../../localization/preferences';
import { listen } from '../../utils/events';
import type { RA, RR } from '../../utils/types';
import { localized } from '../../utils/types';
import type { RA } from '../../utils/types';
import { removeItem, replaceItem, replaceKey } from '../../utils/utils';
import { Key } from '../Atoms';
import { Button } from '../Atoms/Button';
import type { KeyboardShortcuts, ModifierKey } from './KeyboardContext';
import type { PreferenceRendererProps } from '../Preferences/types';
import type { KeyboardShortcuts } from './config';
import { keyboardPlatform } from './config';
import {
keyboardPlatform,
keyJoinSymbol,
resolveModifiers,
resolvePlatformShortcuts,
setKeyboardEventInterceptor,
} from './KeyboardContext';
import type { PreferenceRendererProps } from './types';
} from './context';
import { localizeKeyboardShortcut, resolvePlatformShortcuts } from './utils';

/*
* FIXME: create a mechanism for setting shortcuts for a page, and then displaying
* those in the UI if present on the page
*
* FIXME: open key shortcut viewer on cmd+/
*
* FIXME: localize some key shortcuts (arrow keys, home, etc)
*
* FIXME: add a mapping of allowed default keyboard shortcuts and use that in tests
*/

const modifierLocalization: RR<ModifierKey, string> = {
Alt:
keyboardPlatform === 'mac'
? preferencesText.macOption()
: preferencesText.alt(),
Ctrl:
keyboardPlatform === 'mac'
? preferencesText.macControl()
: preferencesText.ctrl(),
// This key should never appear in non-mac platforms
Meta: preferencesText.macMeta(),
Shift:
keyboardPlatform === 'mac'
? preferencesText.macShift()
: preferencesText.shift(),
};

const localizedKeyJoinSymbol = ' + ';
export const localizeKeyboardShortcut = (shortcut: string): LocalizedString =>
localized(
shortcut
.split(keyJoinSymbol)
.map((key) => modifierLocalization[key as ModifierKey] ?? key)
.join(localizedKeyJoinSymbol)
);

export function KeyboardShortcutPreferenceItem({
value,
onChange: handleChange,
Expand Down
51 changes: 51 additions & 0 deletions specifyweb/frontend/js_src/lib/components/Keyboard/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { LocalizedString } from 'typesafe-i18n';

import type { RA } from '../../utils/types';
import { localized } from '../../utils/types';
import type { KeyboardShortcuts, ModifierKey } from './config';
import { keyboardModifierLocalization, keyboardPlatform } from './config';
import { keyJoinSymbol } from './context';

const localizedKeyJoinSymbol = ' + ';
export const localizeKeyboardShortcut = (shortcut: string): LocalizedString =>
localized(
shortcut
.split(keyJoinSymbol)
.map((key) => keyboardModifierLocalization[key as ModifierKey] ?? key)
.join(localizedKeyJoinSymbol)
);

/**
* If there is a keyboard shortcut defined for current system, use it
* (also, if current system explicitly has empty array of shortcuts, use it).
*
* Otherwise, use the keyboard shortcut from one of the other platforms if set,
* but change meta to ctrl and vice versa as necessary.
*/
export function resolvePlatformShortcuts(
shortcut: KeyboardShortcuts
): RA<string> | undefined {
if ('platform' in shortcut) return shortcut[keyboardPlatform];
else if ('other' in shortcut)
return keyboardPlatform === 'windows'
? shortcut.other
: shortcut.other?.map(replaceCtrlWithMeta);
else if ('windows' in shortcut)
return keyboardPlatform === 'other'
? shortcut.other
: shortcut.other?.map(replaceCtrlWithMeta);
else if ('mac' in shortcut) return shortcut.other?.map(replaceMetaWithCtrl);
else return undefined;
}

const replaceCtrlWithMeta = (shortcut: string): string =>
shortcut
.split(keyJoinSymbol)
.map((key) => (key === 'ctrl' ? 'meta' : key))
.join(keyJoinSymbol);

const replaceMetaWithCtrl = (shortcut: string): string =>
shortcut
.split(keyJoinSymbol)
.map((key) => (key === 'meta' ? 'ctrl' : key))
.join(keyJoinSymbol);
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import {
contextUnlockedPromise,
foreverFetch,
} from '../InitialContext';
import { formatUrl } from '../Router/queryString';
import {
bindKeyboardShortcut,
resolvePlatformShortcuts,
} from './KeyboardContext';
import { localizeKeyboardShortcut } from './KeyboardShortcut';
} from '../Keyboard/context';
import { localizeKeyboardShortcut } from '../Keyboard/shortcuts';
import { formatUrl } from '../Router/queryString';
import type { GenericPreferences, PreferenceItem } from './types';

/* eslint-disable functional/no-this-expression */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ import type { TableFields } from '../DataModel/helperTypes';
import { genericTables } from '../DataModel/tables';
import type { Collection, Tables } from '../DataModel/types';
import { error, softError } from '../Errors/assert';
import type { KeyboardShortcuts } from '../Keyboard/context';
import { KeyboardShortcutPreferenceItem } from '../Keyboard/shortcuts';
import type { StatLayout } from '../Statistics/types';
import {
LanguagePreferencesItem,
SchemaLanguagePreferenceItem,
} from '../Toolbar/Language';
import type { KeyboardShortcuts } from './KeyboardContext';
import { KeyboardShortcutPreferenceItem } from './KeyboardShortcut';
import type { MenuPreferences, WelcomePageMode } from './Renderers';
import {
CollectionSortOrderPreferenceItem,
Expand Down

0 comments on commit d854c51

Please sign in to comment.