[Canvas] Expose shortcuts in tooltips (#36482) (#36938)

* Added shortcuts to tooltips

    * Added stories and tests

    * Added snapshots

    * Updated copy

    * Updated snapshots

    * Pulled out OS in KeyboardShortcutsDoc

    * Updated snapshot
This commit is contained in:
Catherine Liu 2019-05-22 21:39:56 -07:00 committed by GitHub
parent 4e3e9c1431
commit 87b909397e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1865 additions and 237 deletions

View file

@ -19,6 +19,7 @@ import {
} from '@elastic/eui';
import { Shortcuts } from 'react-shortcuts';
import { ExpressionInput } from '../expression_input';
import { ToolTipShortcut } from '../tool_tip_shortcut';
const { useRef } = React;
@ -131,14 +132,22 @@ export const Expression = ({
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
disabled={!!error}
onClick={() => setExpression(formState.expression)}
size="s"
<EuiToolTip
content={
<span>
Run the expression <ToolTipShortcut namespace="EXPRESSION" action="RUN" />
</span>
}
>
Run
</EuiButton>
<EuiButton
fill
disabled={!!error}
onClick={() => setExpression(formState.expression)}
size="s"
>
Run
</EuiButton>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
@ -156,4 +165,5 @@ Expression.propTypes = {
error: PropTypes.string,
isAutocompleteEnabled: PropTypes.bool,
toggleAutocompleteEnabled: PropTypes.func,
runExpressionShortcut: PropTypes.string.isRequired,
};

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { storiesOf } from '@storybook/react';
import React from 'react';
import { action } from '@storybook/addon-actions';
import { KeyboardShortcutsDoc } from '../keyboard_shortcuts_doc';
storiesOf('components/KeyboardShortcutsDoc', module).add('default', () => (
<KeyboardShortcutsDoc onClose={action('onClose')} />
));

View file

@ -1,92 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiDescriptionList,
EuiHorizontalRule,
EuiCode,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { keymap } from '../../lib/keymap';
import { getClientPlatform } from '../../lib/get_client_platform';
import { getId } from '../../lib/get_id';
const getPrettyShortcut = shortcut => {
if (!shortcut) {
return '';
}
let result = shortcut.replace(/command/i, '⌘');
result = result.replace(/option/i, '⌥');
result = result.replace(/left/i, '←');
result = result.replace(/right/i, '→');
result = result.replace(/up/i, '↑');
result = result.replace(/down/i, '↓');
return (
<span key={getId('span')}>
{result
.split(/(\+)/g) //splits the array by '+' and keeps the '+'s as elements in the array
.map(key => (key === '+' ? ` ${key} ` : <EuiCode key={getId('shortcut')}>{key}</EuiCode>))}
</span>
);
};
const getDescriptionListItems = shortcuts =>
Object.values(shortcuts).map(shortcutKeyMap => {
const os = getClientPlatform();
const osShortcuts = shortcutKeyMap[os];
return {
title: shortcutKeyMap.help,
description: osShortcuts.reduce((acc, shortcut, i) => {
if (i !== 0) {
acc.push(' or ');
}
acc.push(getPrettyShortcut(shortcut));
return acc;
}, []),
};
});
export const KeyboardShortcutsDoc = props => (
<EuiFlyout closeButtonAriaLabel="Closes keyboard shortcuts reference" size="s" {...props}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2>Keyboard Shortcuts</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{Object.values(keymap).map(namespace => {
const { displayName, ...shortcuts } = namespace;
return (
<div key={getId('shortcuts')} className="canvasKeyboardShortcut">
<EuiTitle size="xs">
<h4>{displayName}</h4>
</EuiTitle>
<EuiHorizontalRule margin="s" />
<EuiDescriptionList
textStyle="reverse"
type="column"
listItems={getDescriptionListItems(shortcuts)}
compressed
/>
<EuiSpacer />
</div>
);
})}
</EuiFlyoutBody>
</EuiFlyout>
);
KeyboardShortcutsDoc.propTypes = {
onClose: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiDescriptionList,
EuiHorizontalRule,
EuiCode,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { keymap, ShortcutMap, ShortcutNameSpace } from '../../lib/keymap';
import { getClientPlatform } from '../../lib/get_client_platform';
import { getId } from '../../lib/get_id';
import { getPrettyShortcut } from '../../lib/get_pretty_shortcut';
interface DescriptionListItem {
title: string;
description: JSX.Element[];
}
interface Props {
/**
* click handler for closing flyout
*/
onClose: () => void;
}
const os = getClientPlatform();
const getDescriptionListItems = (shortcuts: ShortcutMap[]): DescriptionListItem[] =>
shortcuts.map(
(shortcutKeyMap: ShortcutMap): DescriptionListItem => {
const osShortcuts = shortcutKeyMap[os];
return {
title: shortcutKeyMap.help,
description: osShortcuts.reduce((acc: JSX.Element[], shortcut, i): JSX.Element[] => {
if (i !== 0) {
acc.push(<span key={getId('span')}> or </span>);
}
acc.push(
<span key={getId('span')}>
{getPrettyShortcut(shortcut)
.split(/(\+)/g) // splits the array by '+' and keeps the '+'s as elements in the array
.map(key => (key === '+' ? ` ` : <EuiCode key={getId('shortcut')}>{key}</EuiCode>))}
</span>
);
return acc;
}, []),
};
}
);
export const KeyboardShortcutsDoc: FunctionComponent<Props> = ({ onClose }) => (
<EuiFlyout closeButtonAriaLabel="Closes keyboard shortcuts reference" size="s" onClose={onClose}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2>Keyboard Shortcuts</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{Object.values(keymap).map((namespace: ShortcutNameSpace) => {
const { displayName, ...shortcuts } = namespace;
return (
<div key={getId('shortcuts')} className="canvasKeyboardShortcut">
<EuiTitle size="xs">
<h4>{displayName}</h4>
</EuiTitle>
<EuiHorizontalRule margin="s" />
<EuiDescriptionList
textStyle="reverse"
type="column"
listItems={getDescriptionListItems(Object.values(shortcuts) as ShortcutMap[])}
compressed
/>
<EuiSpacer />
</div>
);
})}
</EuiFlyoutBody>
</EuiFlyout>
);
KeyboardShortcutsDoc.propTypes = {
onClose: PropTypes.func.isRequired,
};

View file

@ -20,6 +20,7 @@ import {
// @ts-ignore unconverted component
import { Popover } from '../popover';
import { CustomElementModal } from '../custom_element_modal';
import { ToolTipShortcut } from '../tool_tip_shortcut/';
const topBorderClassName = 'canvasContextMenu--topBorder';
@ -153,7 +154,15 @@ export class SidebarHeader extends Component<Props, State> {
return (
<Fragment>
<EuiFlexItem grow={false}>
<EuiToolTip position="bottom" content="Move element to top layer">
<EuiToolTip
position="bottom"
content={
<span>
Bring to front
<ToolTipShortcut namespace="ELEMENT" action="BRING_TO_FRONT" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="sortUp"
@ -163,7 +172,15 @@ export class SidebarHeader extends Component<Props, State> {
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip position="bottom" content="Move element up one layer">
<EuiToolTip
position="bottom"
content={
<span>
Bring forward
<ToolTipShortcut namespace="ELEMENT" action="BRING_FORWARD" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="arrowUp"
@ -173,7 +190,15 @@ export class SidebarHeader extends Component<Props, State> {
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip position="bottom" content="Move element down one layer">
<EuiToolTip
position="bottom"
content={
<span>
Send backward
<ToolTipShortcut namespace="ELEMENT" action="SEND_BACKWARD" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="arrowDown"
@ -183,7 +208,15 @@ export class SidebarHeader extends Component<Props, State> {
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip position="bottom" content="Move element to bottom layer">
<EuiToolTip
position="bottom"
content={
<span>
Send to back
<ToolTipShortcut namespace="ELEMENT" action="SEND_TO_BACK" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="sortDown"

View file

@ -0,0 +1,183 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/ToolTipShortcut with alt 1`] = `
<div
style={
Object {
"backgroundColor": "#343741",
"padding": "5px",
"width": "100px",
}
}
>
<div
className="euiText euiText--extraSmall"
>
<div
className="euiTextAlign euiTextAlign--center"
>
<div
className="euiTextColor euiTextColor--ghost"
>
⌥ + P
</div>
</div>
</div>
</div>
`;
exports[`Storyshots components/ToolTipShortcut with cmd 1`] = `
<div
style={
Object {
"backgroundColor": "#343741",
"padding": "5px",
"width": "100px",
}
}
>
<div
className="euiText euiText--extraSmall"
>
<div
className="euiTextAlign euiTextAlign--center"
>
<div
className="euiTextColor euiTextColor--ghost"
>
⌘ + D
</div>
</div>
</div>
</div>
`;
exports[`Storyshots components/ToolTipShortcut with down arrow 1`] = `
<div
style={
Object {
"backgroundColor": "#343741",
"padding": "5px",
"width": "100px",
}
}
>
<div
className="euiText euiText--extraSmall"
>
<div
className="euiTextAlign euiTextAlign--center"
>
<div
className="euiTextColor euiTextColor--ghost"
>
⌘ + SHIFT + ↓
</div>
</div>
</div>
</div>
`;
exports[`Storyshots components/ToolTipShortcut with left arrow 1`] = `
<div
style={
Object {
"backgroundColor": "#343741",
"padding": "5px",
"width": "100px",
}
}
>
<div
className="euiText euiText--extraSmall"
>
<div
className="euiTextAlign euiTextAlign--center"
>
<div
className="euiTextColor euiTextColor--ghost"
>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots components/ToolTipShortcut with right arrow 1`] = `
<div
style={
Object {
"backgroundColor": "#343741",
"padding": "5px",
"width": "100px",
}
}
>
<div
className="euiText euiText--extraSmall"
>
<div
className="euiTextAlign euiTextAlign--center"
>
<div
className="euiTextColor euiTextColor--ghost"
>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots components/ToolTipShortcut with shortcut 1`] = `
<div
style={
Object {
"backgroundColor": "#343741",
"padding": "5px",
"width": "100px",
}
}
>
<div
className="euiText euiText--extraSmall"
>
<div
className="euiTextAlign euiTextAlign--center"
>
<div
className="euiTextColor euiTextColor--ghost"
>
G
</div>
</div>
</div>
</div>
`;
exports[`Storyshots components/ToolTipShortcut with up arrow 1`] = `
<div
style={
Object {
"backgroundColor": "#343741",
"padding": "5px",
"width": "100px",
}
}
>
<div
className="euiText euiText--extraSmall"
>
<div
className="euiTextAlign euiTextAlign--center"
>
<div
className="euiTextColor euiTextColor--ghost"
>
⌘ + SHIFT + ↑
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { storiesOf } from '@storybook/react';
import React from 'react';
import { ToolTipShortcut } from '../tool_tip_shortcut';
storiesOf('components/ToolTipShortcut', module)
.addDecorator(story => (
<div style={{ width: '100px', backgroundColor: '#343741', padding: '5px' }}>{story()}</div>
))
.add('with shortcut', () => <ToolTipShortcut shortcut="G" />)
.add('with cmd', () => <ToolTipShortcut shortcut="⌘ + D" />)
.add('with alt', () => <ToolTipShortcut shortcut="⌥ + P" />)
.add('with left arrow', () => <ToolTipShortcut shortcut="←" />)
.add('with right arrow', () => <ToolTipShortcut shortcut="→" />)
.add('with up arrow', () => <ToolTipShortcut shortcut="⌘ + SHIFT + ↑" />)
.add('with down arrow', () => <ToolTipShortcut shortcut="⌘ + SHIFT + ↓" />);

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { compose, mapProps } from 'recompose';
import { ToolTipShortcut as Component, Props as ComponentProps } from './tool_tip_shortcut';
import { getClientPlatform } from '../../lib/get_client_platform';
import { keymap } from '../../lib/keymap';
import { getPrettyShortcut } from '../../lib/get_pretty_shortcut';
const os = getClientPlatform();
interface Props {
/**
* namespace defined in the keymap to look for shortcut in
*/
namespace: keyof typeof keymap;
/**
* key of the shortcut defined in the keymap
*/
action: string;
}
export const ToolTipShortcut = compose<ComponentProps, Props>(
mapProps(
({ namespace, action }: Props): ComponentProps => {
const shortcutMap = keymap[namespace][action];
if (typeof shortcutMap === 'string') {
return { shortcut: '' };
}
const shortcuts = shortcutMap[os] || [];
return { shortcut: getPrettyShortcut(shortcuts[0]) };
}
)
)(Component);

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { EuiText } from '@elastic/eui';
import { FunctionComponent } from 'react';
export interface Props {
/**
* keyboard shortcut to display in a tooltip
*/
shortcut: string;
}
export const ToolTipShortcut: FunctionComponent<Props> = ({ shortcut }) => (
<EuiText size="xs" textAlign="center" color="ghost">
{shortcut.replace(/\+/g, ' + ')}
</EuiText>
);
ToolTipShortcut.propTypes = {
shortcut: PropTypes.string.isRequired,
};

View file

@ -7,9 +7,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { ToolTipShortcut } from '../../tool_tip_shortcut';
export const RefreshControl = ({ doRefresh, inFlight }) => (
<EuiToolTip position="bottom" content="Refresh data">
<EuiToolTip
position="bottom"
content={
<span>
Refresh data
<ToolTipShortcut namespace="EDITOR" action="REFRESH" />
</span>
}
>
<EuiButtonIcon
disabled={inFlight}
iconType="refresh"

View file

@ -19,6 +19,7 @@ import {
} from '@elastic/eui';
import { AssetManager } from '../asset_manager';
import { ElementTypes } from '../element_types';
import { ToolTipShortcut } from '../tool_tip_shortcut/';
import { ControlSettings } from './control_settings';
import { RefreshControl } from './refresh_control';
import { FullscreenControl } from './fullscreen_control';
@ -33,7 +34,14 @@ export class WorkpadHeader extends React.PureComponent {
state = { isModalVisible: false };
_fullscreenButton = ({ toggleFullscreen }) => (
<EuiToolTip position="bottom" content="Enter fullscreen mode">
<EuiToolTip
position="bottom"
content={
<span>
Enter fullscreen mode <ToolTipShortcut namespace="PRESENTATION" action="FULLSCREEN" />
</span>
}
>
<EuiButtonIcon
iconType="fullScreen"
aria-label="View fullscreen"
@ -73,7 +81,12 @@ export class WorkpadHeader extends React.PureComponent {
if (!this.props.canUserWrite) {
return "You don't have permission to edit this workpad";
} else {
return this.props.isWriteable ? 'Hide editing controls' : 'Show editing controls';
const content = this.props.isWriteable ? `Hide editing controls` : `Show editing controls`;
return (
<span>
{content} <ToolTipShortcut namespace="EDITOR" action="EDITING" />
</span>
);
}
};

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getPrettyShortcut } from '../get_pretty_shortcut';
describe('getPrettyShortcut', () => {
test('uppercases shortcuts', () => {
expect(getPrettyShortcut('g')).toBe('G');
expect(getPrettyShortcut('shift+click')).toBe('SHIFT+CLICK');
expect(getPrettyShortcut('backspace')).toBe('BACKSPACE');
});
test('preserves shortcut order', () => {
expect(getPrettyShortcut('command+c')).toBe('⌘+C');
expect(getPrettyShortcut('c+command')).toBe('C+⌘');
});
test(`replaces 'command' with ⌘`, () => {
expect(getPrettyShortcut('command')).toBe('⌘');
expect(getPrettyShortcut('command+c')).toBe('⌘+C');
expect(getPrettyShortcut('command+shift+b')).toBe('⌘+SHIFT+B');
});
test(`replaces 'option' with ⌥`, () => {
expect(getPrettyShortcut('option')).toBe('⌥');
expect(getPrettyShortcut('option+f')).toBe('⌥+F');
expect(getPrettyShortcut('option+shift+G')).toBe('⌥+SHIFT+G');
expect(getPrettyShortcut('command+option+shift+G')).toBe('⌘+⌥+SHIFT+G');
});
test(`replaces 'left' with ←`, () => {
expect(getPrettyShortcut('left')).toBe('←');
expect(getPrettyShortcut('command+left')).toBe('⌘+←');
expect(getPrettyShortcut('option+left')).toBe('⌥+←');
expect(getPrettyShortcut('option+shift+left')).toBe('⌥+SHIFT+←');
expect(getPrettyShortcut('command+shift+left')).toBe('⌘+SHIFT+←');
expect(getPrettyShortcut('command+option+shift+left')).toBe('⌘+⌥+SHIFT+←');
});
test(`replaces 'right' with →`, () => {
expect(getPrettyShortcut('right')).toBe('→');
expect(getPrettyShortcut('command+right')).toBe('⌘+→');
expect(getPrettyShortcut('option+right')).toBe('⌥+→');
expect(getPrettyShortcut('option+shift+right')).toBe('⌥+SHIFT+→');
expect(getPrettyShortcut('command+shift+right')).toBe('⌘+SHIFT+→');
expect(getPrettyShortcut('command+option+shift+right')).toBe('⌘+⌥+SHIFT+→');
});
test(`replaces 'up' with ←`, () => {
expect(getPrettyShortcut('up')).toBe('↑');
expect(getPrettyShortcut('command+up')).toBe('⌘+↑');
expect(getPrettyShortcut('option+up')).toBe('⌥+↑');
expect(getPrettyShortcut('option+shift+up')).toBe('⌥+SHIFT+↑');
expect(getPrettyShortcut('command+shift+up')).toBe('⌘+SHIFT+↑');
expect(getPrettyShortcut('command+option+shift+up')).toBe('⌘+⌥+SHIFT+↑');
});
test(`replaces 'down' with ↓`, () => {
expect(getPrettyShortcut('down')).toBe('↓');
expect(getPrettyShortcut('command+down')).toBe('⌘+↓');
expect(getPrettyShortcut('option+down')).toBe('⌥+↓');
expect(getPrettyShortcut('option+shift+down')).toBe('⌥+SHIFT+↓');
expect(getPrettyShortcut('command+shift+down')).toBe('⌘+SHIFT+↓');
expect(getPrettyShortcut('command+option+shift+down')).toBe('⌘+⌥+SHIFT+↓');
});
});

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const getClientPlatform = () => {
export const getClientPlatform = (): 'osx' | 'windows' | 'linux' | 'other' => {
const platform = navigator.platform.toLowerCase();
if (platform.indexOf('mac') >= 0) {
return 'osx';

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const getPrettyShortcut = (shortcut: string): string => {
if (!shortcut) {
return '';
}
let result = shortcut.toUpperCase();
result = result.replace(/command/i, '⌘');
result = result.replace(/option/i, '⌥');
result = result.replace(/left/i, '←');
result = result.replace(/right/i, '→');
result = result.replace(/up/i, '↑');
result = result.replace(/down/i, '↓');
return result;
};

View file

@ -1,130 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { mapValues } from 'lodash';
// maps key for all OS's with optional modifiers
const getShortcuts = (shortcuts, modifiers = []) => {
// normalize shortcut values
if (!Array.isArray(shortcuts)) {
shortcuts = [shortcuts];
}
// normalize modifier values
if (!Array.isArray(modifiers)) {
modifiers = [modifiers];
}
let macShortcuts = shortcuts;
// handle shift modifier
if (modifiers.includes('shift')) {
macShortcuts = shortcuts.map(shortcut => `shift+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `shift+${shortcut}`);
}
// handle alt modifier
if (modifiers.includes('alt') || modifiers.includes('option')) {
macShortcuts = shortcuts.map(shortcut => `option+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `alt+${shortcut}`);
}
// handle ctrl modifier
if (modifiers.includes('ctrl') || modifiers.includes('command')) {
macShortcuts = shortcuts.map(shortcut => `command+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `ctrl+${shortcut}`);
}
return {
osx: macShortcuts,
windows: shortcuts,
linux: shortcuts,
other: shortcuts,
};
};
const refreshShortcut = { ...getShortcuts('r', ['alt']), help: 'Refresh workpad' };
const previousPageShortcut = { ...getShortcuts('[', ['alt']), help: 'Go to previous page' };
const nextPageShortcut = { ...getShortcuts(']', ['alt']), help: 'Go to next page' };
const deleteElementShortcuts = ['del', 'backspace'];
const groupShortcut = ['g'];
const ungroupShortcut = ['u'];
const fullscreentExitShortcut = ['esc'];
const fullscreenPageCycle = ['p'];
export const keymap = {
ELEMENT: {
displayName: 'Element controls',
CUT: { ...getShortcuts('x', ['ctrl']), help: 'Cut' },
COPY: { ...getShortcuts('c', ['ctrl']), help: 'Copy' },
PASTE: { ...getShortcuts('v', ['ctrl']), help: 'Paste' },
CLONE: { ...getShortcuts('d', ['ctrl']), help: 'Clone' },
DELETE: {
osx: deleteElementShortcuts,
windows: deleteElementShortcuts,
linux: deleteElementShortcuts,
other: deleteElementShortcuts,
help: 'Delete',
},
BRING_FORWARD: {
...getShortcuts('up', ['ctrl']),
help: 'Bring to front',
},
BRING_TO_FRONT: {
...getShortcuts('up', ['ctrl', 'shift']),
help: 'Bring forward',
},
SEND_BACKWARD: {
...getShortcuts('down', ['ctrl']),
help: 'Send backward',
},
SEND_TO_BACK: {
...getShortcuts('down', ['ctrl', 'shift']),
help: 'Send to back',
},
GROUP: {
osx: groupShortcut,
windows: groupShortcut,
linux: groupShortcut,
other: groupShortcut,
help: 'Group',
},
UNGROUP: {
osx: ungroupShortcut,
windows: ungroupShortcut,
linux: ungroupShortcut,
other: ungroupShortcut,
help: 'Ungroup',
},
},
EDITOR: {
displayName: 'Editor controls',
UNDO: { ...getShortcuts('z', ['ctrl']), help: 'Undo last action' },
REDO: { ...getShortcuts('z', ['ctrl', 'shift']), help: 'Redo last action' },
PREV: previousPageShortcut,
NEXT: nextPageShortcut,
EDITING: { ...getShortcuts('e', ['alt']), help: 'Toggle edit mode' },
GRID: { ...getShortcuts('g', ['alt']), help: 'Show grid' },
REFRESH: refreshShortcut,
},
PRESENTATION: {
displayName: 'Presentation controls',
FULLSCREEN: { ...getShortcuts(['p', 'f'], ['alt']), help: 'Enter presentation mode' },
FULLSCREEN_EXIT: { ...getShortcuts(fullscreentExitShortcut), help: 'Exit presentation mode' },
PREV: mapValues(previousPageShortcut, (osShortcuts, key) =>
key === 'help' ? osShortcuts : osShortcuts.concat(['backspace', 'left'])
),
NEXT: mapValues(nextPageShortcut, (osShortcuts, key) =>
key === 'help' ? osShortcuts : osShortcuts.concat(['space', 'right'])
),
REFRESH: refreshShortcut,
PAGE_CYCLE_TOGGLE: { ...getShortcuts(fullscreenPageCycle), help: 'Toggle page cycling' },
},
EXPRESSION: {
displayName: 'Expression controls',
RUN: { ...getShortcuts('enter', ['ctrl']), help: 'Run whole expression' },
},
};

View file

@ -0,0 +1,136 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { mapValues } from 'lodash';
export interface ShortcutMap {
osx: string[];
windows: string[];
linux: string[];
other: string[];
help: string;
}
export interface ShortcutNameSpace {
displayName: string;
[shortcut: string]: string | ShortcutMap;
}
interface KeyMap {
[category: string]: ShortcutNameSpace;
}
type Modifier = 'ctrl' | 'command' | 'shift' | 'alt' | 'option';
// maps key for all OS's with optional modifiers
const getShortcuts = (
shortcuts: string | string[],
{ modifiers = [], help }: { modifiers?: Modifier | Modifier[]; help: string }
): ShortcutMap => {
// normalize shortcut values
if (!Array.isArray(shortcuts)) {
shortcuts = [shortcuts];
}
// normalize modifier values
if (!Array.isArray(modifiers)) {
modifiers = [modifiers];
}
let macShortcuts = shortcuts;
// handle shift modifier
if (modifiers.includes('shift')) {
macShortcuts = shortcuts.map(shortcut => `shift+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `shift+${shortcut}`);
}
// handle alt modifier
if (modifiers.includes('alt') || modifiers.includes('option')) {
macShortcuts = shortcuts.map(shortcut => `option+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `alt+${shortcut}`);
}
// handle ctrl modifier
if (modifiers.includes('ctrl') || modifiers.includes('command')) {
macShortcuts = shortcuts.map(shortcut => `command+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `ctrl+${shortcut}`);
}
return {
osx: macShortcuts,
windows: shortcuts,
linux: shortcuts,
other: shortcuts,
help,
};
};
const refreshShortcut = getShortcuts('r', { modifiers: 'alt', help: 'Refresh workpad' });
const previousPageShortcut = getShortcuts('[', { modifiers: 'alt', help: 'Go to previous page' });
const nextPageShortcut = getShortcuts(']', { modifiers: 'alt', help: 'Go to next page' });
export const keymap: KeyMap = {
ELEMENT: {
displayName: 'Element controls',
CUT: getShortcuts('x', { modifiers: 'ctrl', help: 'Cut' }),
COPY: getShortcuts('c', { modifiers: 'ctrl', help: 'Copy' }),
PASTE: getShortcuts('v', { modifiers: 'ctrl', help: 'Paste' }),
CLONE: getShortcuts('d', { modifiers: 'ctrl', help: 'Clone' }),
DELETE: getShortcuts(['del', 'backspace'], { help: 'Delete' }),
BRING_FORWARD: getShortcuts('up', { modifiers: 'ctrl', help: 'Bring to front' }),
BRING_TO_FRONT: getShortcuts('up', { modifiers: ['ctrl', 'shift'], help: 'Bring forward' }),
SEND_BACKWARD: getShortcuts('down', { modifiers: 'ctrl', help: 'Send backward' }),
SEND_TO_BACK: getShortcuts('down', { modifiers: ['ctrl', 'shift'], help: 'Send to back' }),
GROUP: getShortcuts('g', { help: 'Group' }),
UNGROUP: getShortcuts('u', { help: 'Ungroup' }),
},
EXPRESSION: {
displayName: 'Expression controls',
RUN: getShortcuts('enter', { modifiers: 'ctrl', help: 'Run whole expression' }),
},
EDITOR: {
displayName: 'Editor controls',
// added for documentation purposes, not handled by `react-shortcuts`
MULTISELECT: getShortcuts('click', { modifiers: 'shift', help: 'Select multiple elements' }),
// added for documentation purposes, not handled by `react-shortcuts`
RESIZE_FROM_CENTER: getShortcuts('drag', {
modifiers: 'alt',
help: 'Resize from center',
}),
// added for documentation purposes, not handled by `react-shortcuts`
IGNORE_SNAP: getShortcuts('drag', {
modifiers: 'ctrl',
help: 'Move, resize, and rotate without snapping',
}),
// added for documentation purposes, not handled by `react-shortcuts`
SELECT_BEHIND: getShortcuts('click', {
modifiers: 'ctrl',
help: 'Select element below',
}),
UNDO: getShortcuts('z', { modifiers: 'ctrl', help: 'Undo last action' }),
REDO: getShortcuts('z', { modifiers: ['ctrl', 'shift'], help: 'Redo last action' }),
PREV: previousPageShortcut,
NEXT: nextPageShortcut,
EDITING: getShortcuts('e', { modifiers: 'alt', help: 'Toggle edit mode' }),
GRID: getShortcuts('g', { modifiers: 'alt', help: 'Show grid' }),
REFRESH: refreshShortcut,
},
PRESENTATION: {
displayName: 'Presentation controls',
FULLSCREEN: getShortcuts(['f', 'p'], { modifiers: 'alt', help: 'Enter presentation mode' }),
FULLSCREEN_EXIT: getShortcuts('esc', { help: 'Exit presentation mode' }),
PREV: mapValues(previousPageShortcut, (osShortcuts: string[], key?: string) =>
// adds 'backspace' and 'left' to list of shortcuts per OS
key === 'help' ? osShortcuts : osShortcuts.concat(['backspace', 'left'])
),
NEXT: mapValues(nextPageShortcut, (osShortcuts: string[], key?: string) =>
// adds 'space' and 'right' to list of shortcuts per OS
key === 'help' ? osShortcuts : osShortcuts.concat(['space', 'right'])
),
REFRESH: refreshShortcut,
PAGE_CYCLE_TOGGLE: getShortcuts('p', { help: 'Toggle page cycling' }),
},
};