Skip to content

Commit

Permalink
Merge pull request #17708 from azimgd/keycommand-v3
Browse files Browse the repository at this point in the history
Allow keyboard shortcuts to work on native devices
  • Loading branch information
luacmartins authored Apr 24, 2023
2 parents d8622b0 + e63cf1a commit d9047e0
Show file tree
Hide file tree
Showing 22 changed files with 373 additions and 136 deletions.
13 changes: 13 additions & 0 deletions __mocks__/react-native-key-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const registerKeyCommands = () => {};
const unregisterKeyCommands = () => {};
const constants = {};
const eventEmitter = () => {};
const addListener = () => {};

export {
registerKeyCommands,
unregisterKeyCommands,
constants,
eventEmitter,
addListener,
};
32 changes: 32 additions & 0 deletions android/app/src/main/java/com/expensify/chat/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import android.os.Bundle;
import android.content.pm.ActivityInfo;
import android.view.KeyEvent;
import com.expensify.chat.bootsplash.BootSplash;
import com.expensify.reactnativekeycommand.KeyCommandModule;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
Expand Down Expand Up @@ -44,4 +46,34 @@ protected void onCreate(Bundle savedInstanceState) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}

/**
* This method is called when a key down event has occurred.
* Forwards the event to the KeyCommandModule
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Disabling hardware ESCAPE support which is handled by Android
if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) {
return false;
}
KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event);
return super.onKeyDown(keyCode, event);
}

@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
// Disabling hardware ESCAPE support which is handled by Android
if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { return false; }
KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event);
return super.onKeyLongPress(keyCode, event);
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Disabling hardware ESCAPE support which is handled by Android
if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { return false; }
KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event);
return super.onKeyUp(keyCode, event);
}
}
10 changes: 10 additions & 0 deletions ios/NewExpensify/AppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#import "RCTBootSplash.h"
#import "RCTStartupTimer.h"
#import <HardwareShortcuts.h>

@interface AppDelegate () <UNUserNotificationCenterDelegate>

Expand Down Expand Up @@ -89,4 +90,13 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
#endif
}

// This methods is needed to support the hardware keyboard shortcuts
- (NSArray *)keyCommands {
return [HardwareShortcuts sharedInstance].keyCommands;
}

- (void)handleKeyCommand:(UIKeyCommand *)keyCommand {
[[HardwareShortcuts sharedInstance] handleKeyCommand:keyCommand];
}

@end
12 changes: 9 additions & 3 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,8 @@ PODS:
- React
- react-native-image-picker (5.1.0):
- React-Core
- react-native-key-command (1.0.0):
- React-Core
- react-native-netinfo (8.3.1):
- React-Core
- react-native-pdf (6.6.2):
Expand Down Expand Up @@ -769,6 +771,7 @@ DEPENDENCIES:
- react-native-flipper (from `../node_modules/react-native-flipper`)
- "react-native-image-manipulator (from `../node_modules/@oguzhnatly/react-native-image-manipulator`)"
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-key-command (from `../node_modules/react-native-key-command`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-pdf (from `../node_modules/react-native-pdf`)
- react-native-performance (from `../node_modules/react-native-performance`)
Expand Down Expand Up @@ -921,6 +924,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@oguzhnatly/react-native-image-manipulator"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-key-command:
:path: "../node_modules/react-native-key-command"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-pdf:
Expand Down Expand Up @@ -1009,7 +1014,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Airship: c70eed50e429f97f5adb285423c7291fb7a032ae
AirshipFrameworkProxy: 2eefb77bb77b5120b0f48814b0d44439aa3ad415
boost: 57d2868c099736d80fcd648bf211b4431e51a558
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: ff54429f0110d3c722630a98096ba689c39f6d5f
Expand Down Expand Up @@ -1052,7 +1057,7 @@ SPEC CHECKSUMS:
Permission-LocationWhenInUse: 3ba99e45c852763f730eabecec2870c2382b7bd4
Plaid: 7d340abeadb46c7aa1a91f896c5b22395a31fcf2
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: e9e7b8b45aa9bedb2fdad71740adf07a7265b9be
RCTTypeSafety: 9ae0e9206625e995f0df4d5b9ddc94411929fb30
React: a71c8e1380f07e01de721ccd52bcf9c03e81867d
Expand All @@ -1074,6 +1079,7 @@ SPEC CHECKSUMS:
react-native-flipper: dc5290261fbeeb2faec1bdc57ae6dd8d562e1de4
react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56
react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b
react-native-key-command: 0b3aa7c9f5c052116413e81dce33a3b2153a6c5d
react-native-netinfo: 1a6035d3b9780221d407c277ebfb5722ace00658
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
react-native-performance: 224bd53e6a835fda4353302cf891d088a0af7406
Expand Down Expand Up @@ -1123,4 +1129,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: cd132e281e9e3d7e6ec2c99c08e6ec32b37886f8

COCOAPODS: 1.12.0
COCOAPODS: 1.11.3
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"react-native-image-pan-zoom": "^2.1.12",
"react-native-image-picker": "^5.1.0",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972",
"react-native-key-command": "^1.0.0",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.39",
Expand Down
61 changes: 59 additions & 2 deletions src/CONST.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import lodashGet from 'lodash/get';
import Config from 'react-native-config';
import * as KeyCommand from 'react-native-key-command';
import * as Url from './libs/Url';

const CLOUDFRONT_DOMAIN = 'cloudfront.net';
const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`;
const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(lodashGet(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com'));
const USE_EXPENSIFY_URL = 'https://use.expensify.com';
const PLATFORM_OS_MACOS = 'Mac OS';
const PLATFORM_IOS = 'iOS';
const ANDROID_PACKAGE_NAME = 'com.expensify.chat';
const USA_COUNTRY_NAME = 'United States';
const CURRENT_YEAR = new Date().getFullYear();
const PULL_REQUEST_NUMBER = lodashGet(Config, 'PULL_REQUEST_NUMBER', '');

const keyModifierControl = lodashGet(KeyCommand, 'constants.keyModifierControl', 'keyModifierControl');
const keyModifierCommand = lodashGet(KeyCommand, 'constants.keyModifierCommand', 'keyModifierCommand');
const keyModifierShiftControl = lodashGet(KeyCommand, 'constants.keyModifierShiftControl', 'keyModifierShiftControl');
const keyModifierShiftCommand = lodashGet(KeyCommand, 'constants.keyModifierShiftCommand', 'keyModifierShiftCommand');
const keyInputEscape = lodashGet(KeyCommand, 'constants.keyInputEscape', 'keyInputEscape');
const keyInputEnter = lodashGet(KeyCommand, 'constants.keyInputEnter', 'keyInputEnter');
const keyInputUpArrow = lodashGet(KeyCommand, 'constants.keyInputUpArrow', 'keyInputUpArrow');
const keyInputDownArrow = lodashGet(KeyCommand, 'constants.keyInputDownArrow', 'keyInputDownArrow');

const CONST = {
ANDROID_PACKAGE_NAME,
ANIMATED_TRANSITION: 300,
Expand Down Expand Up @@ -227,6 +238,7 @@ const CONST = {
CTRL: {
DEFAULT: 'control',
[PLATFORM_OS_MACOS]: 'meta',
[PLATFORM_IOS]: 'meta',
},
SHIFT: {
DEFAULT: 'shift',
Expand All @@ -237,46 +249,91 @@ const CONST = {
descriptionKey: 'search',
shortcutKey: 'K',
modifiers: ['CTRL'],
trigger: {
DEFAULT: {input: 'k', modifierFlags: keyModifierControl},
[PLATFORM_OS_MACOS]: {input: 'k', modifierFlags: keyModifierCommand},
[PLATFORM_IOS]: {input: 'k', modifierFlags: keyModifierCommand},
},
},
NEW_GROUP: {
descriptionKey: 'newGroup',
shortcutKey: 'K',
modifiers: ['CTRL', 'SHIFT'],
trigger: {
DEFAULT: {input: 'k', modifierFlags: keyModifierShiftControl},
[PLATFORM_OS_MACOS]: {input: 'k', modifierFlags: keyModifierShiftCommand},
[PLATFORM_IOS]: {input: 'k', modifierFlags: keyModifierShiftCommand},
},
},
SHORTCUT_MODAL: {
descriptionKey: 'openShortcutDialog',
shortcutKey: 'I',
shortcutKey: 'J',
modifiers: ['CTRL'],
trigger: {
DEFAULT: {input: 'j', modifierFlags: keyModifierControl},
[PLATFORM_OS_MACOS]: {input: 'j', modifierFlags: keyModifierCommand},
[PLATFORM_IOS]: {input: 'j', modifierFlags: keyModifierCommand},
},
},
ESCAPE: {
descriptionKey: 'escape',
shortcutKey: 'Escape',
modifiers: [],
trigger: {
DEFAULT: {input: keyInputEscape},
[PLATFORM_OS_MACOS]: {input: keyInputEscape},
[PLATFORM_IOS]: {input: keyInputEscape},
},
},
ENTER: {
descriptionKey: null,
shortcutKey: 'Enter',
modifiers: [],
trigger: {
DEFAULT: {input: keyInputEnter},
[PLATFORM_OS_MACOS]: {input: keyInputEnter},
[PLATFORM_IOS]: {input: keyInputEnter},
},
},
CTRL_ENTER: {
descriptionKey: null,
shortcutKey: 'Enter',
modifiers: ['CTRL'],
trigger: {
DEFAULT: {input: keyInputEnter, modifierFlags: keyModifierControl},
[PLATFORM_OS_MACOS]: {input: keyInputEnter, modifierFlags: keyModifierCommand},
[PLATFORM_IOS]: {input: keyInputEnter, modifierFlags: keyModifierCommand},
},
},
COPY: {
descriptionKey: 'copy',
shortcutKey: 'C',
modifiers: ['CTRL'],
trigger: {
DEFAULT: {input: 'c', modifierFlags: keyModifierControl},
[PLATFORM_OS_MACOS]: {input: 'c', modifierFlags: keyModifierCommand},
[PLATFORM_IOS]: {input: 'c', modifierFlags: keyModifierCommand},
},
},
ARROW_UP: {
descriptionKey: null,
shortcutKey: 'ArrowUp',
modifiers: [],
trigger: {
DEFAULT: {input: keyInputUpArrow},
[PLATFORM_OS_MACOS]: {input: keyInputUpArrow},
[PLATFORM_IOS]: {input: keyInputUpArrow},
},
},
ARROW_DOWN: {
descriptionKey: null,
shortcutKey: 'ArrowDown',
modifiers: [],
trigger: {
DEFAULT: {input: keyInputDownArrow},
[PLATFORM_OS_MACOS]: {input: keyInputDownArrow},
[PLATFORM_IOS]: {input: keyInputDownArrow},
},
},
TAB: {
descriptionKey: null,
Expand Down Expand Up @@ -790,7 +847,7 @@ const CONST = {
WINDOWS: 'Windows',
MAC_OS: PLATFORM_OS_MACOS,
ANDROID: 'Android',
IOS: 'iOS',
IOS: PLATFORM_IOS,
LINUX: 'Linux',
NATIVE: 'Native',
},
Expand Down
30 changes: 15 additions & 15 deletions src/components/Button.js → src/components/Button/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import React, {Component} from 'react';
import {Pressable, ActivityIndicator, View} from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
import OpacityView from './OpacityView';
import Text from './Text';
import KeyboardShortcut from '../libs/KeyboardShortcut';
import Icon from './Icon';
import CONST from '../CONST';
import * as StyleUtils from '../styles/StyleUtils';
import HapticFeedback from '../libs/HapticFeedback';
import withNavigationFallback from './withNavigationFallback';
import compose from '../libs/compose';
import * as Expensicons from './Icon/Expensicons';
import withNavigationFocus from './withNavigationFocus';
import styles from '../../styles/styles';
import themeColors from '../../styles/themes/default';
import OpacityView from '../OpacityView';
import Text from '../Text';
import KeyboardShortcut from '../../libs/KeyboardShortcut';
import Icon from '../Icon';
import CONST from '../../CONST';
import * as StyleUtils from '../../styles/StyleUtils';
import HapticFeedback from '../../libs/HapticFeedback';
import withNavigationFallback from '../withNavigationFallback';
import compose from '../../libs/compose';
import * as Expensicons from '../Icon/Expensicons';
import withNavigationFocus from '../withNavigationFocus';
import validateSubmitShortcut from './validateSubmitShortcut';

const propTypes = {
/** The text for the button label */
Expand Down Expand Up @@ -157,10 +158,9 @@ class Button extends Component {

// Setup and attach keypress handler for pressing the button with Enter key
this.unsubscribe = KeyboardShortcut.subscribe(shortcutConfig.shortcutKey, (e) => {
if (!this.props.isFocused || this.props.isDisabled || this.props.isLoading || (e && e.target.nodeName === 'TEXTAREA')) {
if (!validateSubmitShortcut(this.props.isFocused, this.props.isDisabled, this.props.isLoading, e)) {
return;
}
e.preventDefault();
this.props.onPress();
}, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true, false, this.props.enterKeyEventListenerPriority, false);
}
Expand Down
19 changes: 19 additions & 0 deletions src/components/Button/validateSubmitShortcut/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Validate if the submit shortcut should be triggered depending on the button state
*
* @param {boolean} isFocused Whether Button is on active screen
* @param {boolean} isDisabled Indicates whether the button should be disabled
* @param {boolean} isLoading Indicates whether the button should be disabled and in the loading state
* @param {Object} event Focused input event
* @returns {boolean} Returns `true` if the shortcut should be triggered
*/
function validateSubmitShortcut(isFocused, isDisabled, isLoading, event) {
if (!isFocused || isDisabled || isLoading || (event && event.target.nodeName === 'TEXTAREA')) {
return false;
}

event.preventDefault();
return true;
}

export default validateSubmitShortcut;
Loading

0 comments on commit d9047e0

Please sign in to comment.