Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Keyboard shortcuts modal #6112

Merged
merged 41 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
baf320a
Basic component with props added
akshayasalvi Oct 29, 2021
fdf81af
Converted component to class component
akshayasalvi Oct 29, 2021
e00816c
Added mapper function to the keyboardshortcut lib
akshayasalvi Oct 29, 2021
28855fb
Translation files
akshayasalvi Oct 29, 2021
cb54938
Removed modal props from ScreenWrapper
akshayasalvi Oct 29, 2021
a6c28bd
Modified the shortcut trigger logic to modal
akshayasalvi Oct 29, 2021
6bad775
Added keys for existing shortcuts
akshayasalvi Oct 29, 2021
290acb8
Styling updates for container
akshayasalvi Oct 29, 2021
f31f5ed
Key handling for cmd
akshayasalvi Oct 29, 2021
2b6e319
Moved styling to style.js
akshayasalvi Oct 29, 2021
32622ab
Changed border styling
akshayasalvi Oct 29, 2021
2e0d4c7
Added spanish translations
akshayasalvi Oct 29, 2021
2096196
Added styling options for modal
akshayasalvi Nov 3, 2021
18fbadf
Added borderRadius to the table
akshayasalvi Nov 3, 2021
640687d
Fixed border styling for the table
akshayasalvi Nov 3, 2021
c18e2a4
Removed height hardcoding
akshayasalvi Nov 3, 2021
ea2a41f
Allow modal to open in input focus as well
akshayasalvi Nov 7, 2021
e1a8e96
Merge branch 'main' of github-personal:akshayasalvi/App into keyboard…
akshayasalvi Nov 7, 2021
e39b198
Updated copy for the modal subtitle
akshayasalvi Nov 7, 2021
77c9381
Code cleanup
akshayasalvi Nov 8, 2021
111edbc
Moved object to map to Keyboardshortcut lib
akshayasalvi Nov 8, 2021
06e42c4
Reordered params for KeyboardShortcut.subscribe
akshayasalvi Nov 8, 2021
1018fb5
Styling fixes
akshayasalvi Nov 8, 2021
dbfa47e
Changes for code cleanup
akshayasalvi Nov 8, 2021
048c729
Merge branch 'main' of github-personal:akshayasalvi/App into keyboard…
akshayasalvi Nov 8, 2021
666e499
Added check for descriptionKey
akshayasalvi Nov 8, 2021
eeb16d1
Fixed escape key for dialog
akshayasalvi Nov 8, 2021
fa76933
Lint fixes
akshayasalvi Nov 8, 2021
99e7a4f
Made modal bottom docked for small screens and changed function name
akshayasalvi Nov 8, 2021
1f57f37
Moved shortcut modifiers (control/meta) logic to utils
akshayasalvi Nov 9, 2021
6943aa1
Merge branch 'main' of github-personal:akshayasalvi/App into keyboard…
akshayasalvi Nov 14, 2021
4c29fc0
Changed logic for modifiers
akshayasalvi Nov 19, 2021
7de6c54
Fixed typo for KeyboardShortcut
akshayasalvi Nov 19, 2021
e5b034d
Split toggle func to hide and show
akshayasalvi Nov 19, 2021
3cbd1bf
Removed hardcoding of the shortcuts to CONST
akshayasalvi Nov 19, 2021
fc03898
Pick all shortcut configs from CONST
akshayasalvi Nov 19, 2021
eb6ef1c
Added enter configuration for keyboard shortcuts
akshayasalvi Nov 19, 2021
e2e456c
Fixed typo for Button
akshayasalvi Nov 19, 2021
f09b027
Added jsdocs and changed function calls
akshayasalvi Nov 23, 2021
2eeed69
Unified representation of shortcut object
akshayasalvi Nov 23, 2021
06c77ce
Fixed typos in shortcuts
akshayasalvi Nov 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions src/components/KeyboardShortcutsModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';
import {View, Text} from 'react-native';
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
import _ from 'underscore';
import HeaderWithCloseButton from './HeaderWithCloseButton';
import Modal from './Modal';
import CONST from '../CONST';
import styles, {getKeyboardShortcutModalStyle} from '../styles/styles';
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import compose from '../libs/compose';
import KeyboardShortcut from '../libs/KeyboardShortcut';
import getOperatingSystem from '../libs/getOperatingSystem';

const propTypes = {

roryabraham marked this conversation as resolved.
Show resolved Hide resolved
/** prop to fetch screen width */
...windowDimensionsPropTypes,

/** props to fetch translation functions */
...withLocalizePropTypes,

roryabraham marked this conversation as resolved.
Show resolved Hide resolved
};

class KeyboardShortcutsModal extends React.PureComponent {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
constructor(props) {
super(props);

this.state = {
isOpen: false,
};
}

componentDidMount() {
let shortcutModifiers = ['control'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen this same code in AuthScreens.js, and I think it would be valuable to extract some of this to a separate util.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked too. Couldn't find an simple way to move to utils.

        let searchShortcutModifiers = ['control'];
        let groupShortcutModifiers = ['control', 'shift'];

        if (getOperatingSystem() === CONST.OS.MAC_OS) {
            searchShortcutModifiers = ['meta'];
            groupShortcutModifiers = ['meta', 'shift'];
        }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a simple function in src/libs/KeyboardShortcut/index.js like this to start:

getShortcutModifiers(modifiers) {
    const isMacOS = getOperatingSystem() === CONST.OS.MAC_OS;
    return _.map(modifiers, modifier => isMacOS && modifier === 'control' ? 'meta' : 'control');
}

if (getOperatingSystem() === CONST.OS.MAC_OS) {
shortcutModifiers = ['meta'];
}
this.unsubscribeShortCutModal = KeyboardShortcut.subscribe('?', () => {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
this.toggleKeyboardShortcutModal(true);
}, shortcutModifiers, false, 'openShortcutDialog');
}

componentWillUnmount() {
if (this.unsubscribeShortCutModal) {
this.unsubscribeShortCutModal();
}
}


/**
* Set flag for model visibility
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
* @param {Boolean} flag
*/
toggleKeyboardShortcutModal(flag) {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
this.setState({isOpen: flag});
}

renderRow(shortcut, isFirstRow) {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
return (
<View
style={[
styles.keyboardShortcutTableRow,
isFirstRow && styles.keyboardShortcutTableFirstRow,
]}
key={shortcut.key}
>
<View style={[styles.dFlex, styles.p2, styles.keyboardShortcutTablePrefix]}>
<Text>{shortcut.key}</Text>
</View>
<View style={[styles.flex1, styles.p2, styles.alignSelfStretch]}>
<Text>{this.props.translate(`keyboardShortCutModal.shortcuts.${shortcut.descriptionKey}`)}</Text>
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
</View>
</View>
);
}


render() {
const shortcutMap = KeyboardShortcut.getKeyboardShortcutMap();
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
const shortcuts = [];
_.each(shortcutMap, (descriptionKey, key) => {
shortcuts.push({key, descriptionKey});
});

return (
<Modal
isVisible={this.state.isOpen}
type={CONST.MODAL.MODAL_TYPE.CENTERED}
containerStyle={styles.keyboardShortcutModalContainer}
onClose={() => this.toggleKeyboardShortcutModal(false)}
>
<HeaderWithCloseButton title={this.props.translate('keyboardShortCutModal.title')} onCloseButtonPress={() => this.toggleKeyboardShortcutModal(false)} />
<View style={[styles.p5, styles.pt0]}>
<Text style={styles.mb5}>{this.props.translate('keyboardShortCutModal.subtitle')}</Text>
<View style={[getKeyboardShortcutModalStyle(this.props.isSmallScreenWidth)]}>
<View style={[styles.alignItemsCenter, styles.keyboardShortcutTableContainer]}>
{shortcuts.map((shortcut, index) => {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
const isFirstRow = index === 0;
return this.renderRow(shortcut, isFirstRow);
})}
</View>

</View>
</View>
</Modal>
);
}
}

KeyboardShortcutsModal.propTypes = propTypes;

export default compose(
withWindowDimensions,
withLocalize,
)(KeyboardShortcutsModal);
1 change: 1 addition & 0 deletions src/components/Modal/BaseModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class BaseModal extends PureComponent {
isSmallScreenWidth: this.props.isSmallScreenWidth,
},
this.props.popoverAnchorPosition,
this.props.containerStyle,
);
return (
<ReactNativeModal
Expand Down
5 changes: 5 additions & 0 deletions src/components/Modal/modalPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import _ from 'underscore';
import CONST from '../../CONST';
import {windowDimensionsPropTypes} from '../withWindowDimensions';
import stylePropTypes from '../../styles/stylePropTypes';

const propTypes = {
/** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */
Expand Down Expand Up @@ -54,6 +55,9 @@ const propTypes = {
left: PropTypes.number,
}),

/** Modal container styles */
containerStyle: stylePropTypes,

...windowDimensionsPropTypes,
};

Expand All @@ -68,6 +72,7 @@ const defaultProps = {
animationIn: null,
animationOut: null,
popoverAnchorPosition: {},
containerStyle: {},
};

export {propTypes, defaultProps};
6 changes: 5 additions & 1 deletion src/components/ScreenWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
import {withOnyx} from 'react-native-onyx';
import styles, {getSafeAreaPadding} from '../styles/styles';
import HeaderGap from './HeaderGap';
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
import KeyboardShortcut from '../libs/KeyboardShortcut';
import onScreenTransitionEnd from '../libs/onScreenTransitionEnd';
import Navigation from '../libs/Navigation/Navigation';
Expand Down Expand Up @@ -60,6 +61,7 @@ const defaultProps = {
class ScreenWrapper extends React.Component {
constructor(props) {
super(props);

this.state = {
didScreenTransitionEnd: false,
};
Expand All @@ -70,7 +72,7 @@ class ScreenWrapper extends React.Component {
if (!this.props.modal.willAlertModalBecomeVisible) {
Navigation.dismissModal();
}
}, [], true);
}, [], true, 'escape');

this.unsubscribeTransitionEnd = onScreenTransitionEnd(this.props.navigation, () => {
this.setState({didScreenTransitionEnd: true});
Expand Down Expand Up @@ -118,6 +120,8 @@ class ScreenWrapper extends React.Component {
})
: this.props.children
}
<KeyboardShortcutsModal />

</View>
);
}}
Expand Down
10 changes: 10 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,4 +761,14 @@ export default {
emojiPicker: {
skinTonePickerLabel: 'Change default skin tone',
},
keyboardShortCutModal: {
title: 'Keyboard Shortcuts',
subtitle: 'Do you expense reports faster with these handy keyboard shortcuts',
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
shortcuts: {
openShortcutDialog: 'Opens the keyboard shortcuts dialog',
escape: 'Escape Dialogs',
search: 'Open search dialog',
newGroup: 'New group screen',
},
},
};
10 changes: 10 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -763,4 +763,14 @@ export default {
emojiPicker: {
skinTonePickerLabel: 'Elige el tono de piel por defecto',
},
keyboardShortCutModal: {
title: 'Atajos de teclado',
subtitle: '¿Realiza informes de gastos más rápido con estos prácticos atajos de teclado?',
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
shortcuts: {
openShortcutDialog: 'Abre el cuadro de diálogo de métodos abreviados de teclado',
escape: 'Diálogos de escape',
search: 'Abrir diálogo de búsqueda',
newGroup: 'Nueva pantalla de grupo',
},
},
};
46 changes: 45 additions & 1 deletion src/libs/KeyboardShortcut/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import _ from 'underscore';

const events = {};
const keyboardShortcutMap = {};

function getKeyboardShortcutMap() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer returning a map but an array of objects, so we should rename the function to getKeyboardShortcuts

return keyboardShortcutMap;
}

/**
* Checks if an event for that key is configured and if so, runs it.
Expand Down Expand Up @@ -92,6 +97,8 @@ function getKeyCode(key) {
return 13;
case 'Escape':
return 27;
case '?':
return 191;
default:
return key.charCodeAt(0);
}
Expand All @@ -107,20 +114,56 @@ function unsubscribe(key) {
events[keyCode].pop();
}

/**
* Add key to the shortcut map
*
* @param {String} key The key to watch, i.e. 'K' or 'Escape'
* @param {String|String[]} modifiers Can either be shift or control
* @param {String} descriptionKey Translation key for shortcut description
*/
function addKeyToMap(key, modifiers, descriptionKey) {
let shortcutKey = [key];
if (typeof modifiers === 'string') {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
shortcutKey.unshift(modifiers);
} else if (Array.isArray(modifiers)) {
shortcutKey = [...modifiers, ...shortcutKey];
}

shortcutKey = shortcutKey.map((modifierKey) => {
switch (modifierKey) {
case 'control':
return 'Ctrl';
case 'meta':
return 'Cmd';
case 'shift':
return 'Shift';
default:
return modifierKey;
}
});

shortcutKey = shortcutKey.join(' + ');
keyboardShortcutMap[shortcutKey] = descriptionKey;
}


/**
* Subscribes to a keyboard event.
* @param {String} key The key to watch, i.e. 'K' or 'Escape'
* @param {Function} callback The callback to call
* @param {String|Array} modifiers Can either be shift or control
* @param {Boolean} captureOnInputs Should we capture the event on inputs too?
* @param {String} descriptionKey Translation key for shortcut description
* @returns {Function} clean up method
*/
function subscribe(key, callback, modifiers = 'shift', captureOnInputs = false) {
function subscribe(key, callback, modifiers = 'shift', captureOnInputs = false, descriptionKey) {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
const keyCode = getKeyCode(key);
if (events[keyCode] === undefined) {
events[keyCode] = [];
}
events[keyCode].push({callback, modifiers: _.isArray(modifiers) ? modifiers : [modifiers], captureOnInputs});

addKeyToMap(key, modifiers, descriptionKey);
return () => unsubscribe(key);
}

Expand All @@ -137,6 +180,7 @@ function subscribe(key, callback, modifiers = 'shift', captureOnInputs = false)
*/
const KeyboardShortcut = {
subscribe,
getKeyboardShortcutMap,
};

export default KeyboardShortcut;
1 change: 1 addition & 0 deletions src/libs/KeyboardShortcut/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const KeyboardShortcut = {
return () => {};
},
unsubscribe() {},
getKeyboardShortcutMap() {},
};

export default KeyboardShortcut;
4 changes: 2 additions & 2 deletions src/libs/Navigation/AppNavigator/AuthScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,10 @@ class AuthScreens extends React.Component {
// based on the key modifiers pressed and the operating system
this.unsubscribeSearchShortcut = KeyboardShortcut.subscribe('K', () => {
Navigation.navigate(ROUTES.SEARCH);
}, searchShortcutModifiers, true);
}, searchShortcutModifiers, true, 'search');
this.unsubscribeGroupShortcut = KeyboardShortcut.subscribe('K', () => {
Navigation.navigate(ROUTES.NEW_GROUP);
}, groupShortcutModifiers, true);
}, groupShortcutModifiers, true, 'newGroup');
}

shouldComponentUpdate(nextProps) {
Expand Down
4 changes: 3 additions & 1 deletion src/styles/getModalStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import colors from './colors';
import variables from './variables';
import themeColors from './themes/default';

export default (type, windowDimensions, popoverAnchorPosition = {}) => {
export default (type, windowDimensions, popoverAnchorPosition = {}, containerStyle = {}) => {
const {isSmallScreenWidth, windowWidth} = windowDimensions;

let modalStyle = {
Expand Down Expand Up @@ -206,6 +206,8 @@ export default (type, windowDimensions, popoverAnchorPosition = {}) => {
animationOut = 'slideOutDown';
}

modalContainerStyle = {...modalContainerStyle, ...containerStyle};

return {
modalStyle,
modalContainerStyle,
Expand Down
Loading