From 75590017a26884a659f16365a12819bd6296f19b Mon Sep 17 00:00:00 2001 From: Vit Horacek <36083550+mountiny@users.noreply.github.com> Date: Tue, 18 Apr 2023 09:14:11 -0700 Subject: [PATCH] Merge pull request #17592 from Expensify/revert-14767-keycommand-v2 Revert "Allow keyboard shortcuts to work on native devices" (cherry picked from commit 0900da550e21996437a4009a7d367b3ad330b46a) --- __mocks__/react-native-key-command.js | 13 --- .../java/com/expensify/chat/MainActivity.java | 34 +----- ios/Podfile.lock | 10 +- package-lock.json | 53 +++------ package.json | 1 - src/CONST.js | 59 +--------- src/components/{Button/index.js => Button.js} | 31 +++--- .../Button/validateSubmitShortcut/index.js | 19 ---- .../validateSubmitShortcut/index.native.js | 17 --- src/components/KeyboardShortcutsModal.js | 26 ++--- src/components/ScreenWrapper/index.js | 2 +- .../bindHandlerToKeydownEvent/index.js | 54 --------- .../bindHandlerToKeydownEvent/index.native.js | 33 ------ .../KeyboardShortcut/getKeyEventModifiers.js | 27 ----- src/libs/KeyboardShortcut/index.js | 103 ++++++++++++------ src/libs/KeyboardShortcut/index.native.js | 12 ++ src/pages/home/report/ReportActionCompose.js | 2 +- src/pages/settings/AboutPage/AboutPage.js | 10 +- .../getPlatformSpecificMenuItems/index.js | 15 +++ .../index.native.js | 1 + src/styles/styles.js | 4 +- 21 files changed, 149 insertions(+), 377 deletions(-) delete mode 100644 __mocks__/react-native-key-command.js rename src/components/{Button/index.js => Button.js} (93%) delete mode 100644 src/components/Button/validateSubmitShortcut/index.js delete mode 100644 src/components/Button/validateSubmitShortcut/index.native.js delete mode 100644 src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.js delete mode 100644 src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.native.js delete mode 100644 src/libs/KeyboardShortcut/getKeyEventModifiers.js create mode 100644 src/libs/KeyboardShortcut/index.native.js create mode 100644 src/pages/settings/AboutPage/getPlatformSpecificMenuItems/index.js create mode 100644 src/pages/settings/AboutPage/getPlatformSpecificMenuItems/index.native.js diff --git a/__mocks__/react-native-key-command.js b/__mocks__/react-native-key-command.js deleted file mode 100644 index 092ab120a142..000000000000 --- a/__mocks__/react-native-key-command.js +++ /dev/null @@ -1,13 +0,0 @@ -const registerKeyCommands = () => {}; -const unregisterKeyCommands = () => {}; -const constants = {}; -const eventEmitter = () => {}; -const addListener = () => {}; - -export { - registerKeyCommands, - unregisterKeyCommands, - constants, - eventEmitter, - addListener, -}; diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.java b/android/app/src/main/java/com/expensify/chat/MainActivity.java index 3e6381000409..bd90ee9abd02 100644 --- a/android/app/src/main/java/com/expensify/chat/MainActivity.java +++ b/android/app/src/main/java/com/expensify/chat/MainActivity.java @@ -2,9 +2,7 @@ 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; @@ -46,34 +44,4 @@ 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); - } -} +} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 86707162d74c..28de3a7fa527 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -514,8 +514,6 @@ PODS: - React - react-native-image-picker (5.1.0): - React-Core - - react-native-key-command (0.9.1): - - React-Core - react-native-netinfo (8.3.1): - React-Core - react-native-pdf (6.6.2): @@ -767,7 +765,6 @@ 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`) @@ -919,8 +916,6 @@ 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: @@ -1007,7 +1002,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Airship: 19ead2c0bdc791c1b9d6ebb7940aaac99614414e AirshipFrameworkProxy: 037a0ad6491757c45de2c70a6cc47bae5fcfa32b - boost: a7c83b31436843459a1961bfd74b96033dc77234 + boost: 57d2868c099736d80fcd648bf211b4431e51a558 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: ff54429f0110d3c722630a98096ba689c39f6d5f @@ -1050,7 +1045,7 @@ SPEC CHECKSUMS: Permission-LocationWhenInUse: 3ba99e45c852763f730eabecec2870c2382b7bd4 Plaid: 7d340abeadb46c7aa1a91f896c5b22395a31fcf2 PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef - RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda + RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: e9e7b8b45aa9bedb2fdad71740adf07a7265b9be RCTTypeSafety: 9ae0e9206625e995f0df4d5b9ddc94411929fb30 React: a71c8e1380f07e01de721ccd52bcf9c03e81867d @@ -1072,7 +1067,6 @@ SPEC CHECKSUMS: react-native-flipper: dc5290261fbeeb2faec1bdc57ae6dd8d562e1de4 react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56 react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b - react-native-key-command: e49d6e44d44705779696d8d3a5ac6b9e3a198941 react-native-netinfo: 1a6035d3b9780221d407c277ebfb5722ace00658 react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa react-native-performance: 224bd53e6a835fda4353302cf891d088a0af7406 diff --git a/package-lock.json b/package-lock.json index 03ed2190329c..ae529ba4fc0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,6 @@ "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": "^0.9.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", "react-native-onyx": "1.0.38", @@ -34671,21 +34670,6 @@ "integrity": "sha512-jNNpW5byieb7pb/l0HRvmCav4BtzpTzgC+ybT+Wbi2yyroOukveVvnjwWnmoOeuGynsYB4Yt5eGrWZnPnJSwqQ==", "license": "MIT" }, - "node_modules/react-native-key-command": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/react-native-key-command/-/react-native-key-command-0.9.1.tgz", - "integrity": "sha512-di7G5q66eI0xL14B4kcVfm7azGET07henwu21N8hb71sZpDZGsAJ1WFuR32SwbnkLVNhEk7FJAIH/5Sh+dDQoA==", - "dependencies": { - "events": "^3.3.0", - "underscore": "^1.13.4" - }, - "peerDependencies": { - "react": "^18.1.0", - "react-dom": "18.1.0", - "react-native": "^0.70.4", - "react-native-web": "^0.18.1" - } - }, "node_modules/react-native-localize": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-2.2.6.tgz", @@ -34928,22 +34912,22 @@ } }, "node_modules/react-native-web": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.1.tgz", - "integrity": "sha512-kWZKJU/OIvVriC7R7WN2Wpai6QllD9f7RDUuaBj2G9FdXaybSQFgmuhey4n6naqbLnj720im2PtcnH54Cn/UXw==", + "version": "0.18.12", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.12.tgz", + "integrity": "sha512-fboP7yqobJ8InSr4fP+bQ3scOtSQtUoPcR+HWasH8b/fk/RO+mWcJs/8n+lewy9WTZc2D68ha7VwRDviUshEWA==", "peer": true, "dependencies": { + "@babel/runtime": "^7.18.6", "create-react-class": "^15.7.0", - "fbjs": "^3.0.0", - "inline-style-prefixer": "^6.0.0", + "fbjs": "^3.0.4", + "inline-style-prefixer": "^6.0.1", "normalize-css-color": "^1.0.2", "postcss-value-parser": "^4.2.0", - "prop-types": "^15.6.0", "styleq": "^0.1.2" }, "peerDependencies": { - "react": ">=17.0.2", - "react-dom": ">=17.0.2" + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" } }, "node_modules/react-native-web-lottie": { @@ -64350,15 +64334,6 @@ "integrity": "sha512-jNNpW5byieb7pb/l0HRvmCav4BtzpTzgC+ybT+Wbi2yyroOukveVvnjwWnmoOeuGynsYB4Yt5eGrWZnPnJSwqQ==", "from": "react-native-image-size@git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972" }, - "react-native-key-command": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/react-native-key-command/-/react-native-key-command-0.9.1.tgz", - "integrity": "sha512-di7G5q66eI0xL14B4kcVfm7azGET07henwu21N8hb71sZpDZGsAJ1WFuR32SwbnkLVNhEk7FJAIH/5Sh+dDQoA==", - "requires": { - "events": "^3.3.0", - "underscore": "^1.13.4" - } - }, "react-native-localize": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-2.2.6.tgz", @@ -64495,17 +64470,17 @@ } }, "react-native-web": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.1.tgz", - "integrity": "sha512-kWZKJU/OIvVriC7R7WN2Wpai6QllD9f7RDUuaBj2G9FdXaybSQFgmuhey4n6naqbLnj720im2PtcnH54Cn/UXw==", + "version": "0.18.12", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.12.tgz", + "integrity": "sha512-fboP7yqobJ8InSr4fP+bQ3scOtSQtUoPcR+HWasH8b/fk/RO+mWcJs/8n+lewy9WTZc2D68ha7VwRDviUshEWA==", "peer": true, "requires": { + "@babel/runtime": "^7.18.6", "create-react-class": "^15.7.0", - "fbjs": "^3.0.0", - "inline-style-prefixer": "^6.0.0", + "fbjs": "^3.0.4", + "inline-style-prefixer": "^6.0.1", "normalize-css-color": "^1.0.2", "postcss-value-parser": "^4.2.0", - "prop-types": "^15.6.0", "styleq": "^0.1.2" } }, diff --git a/package.json b/package.json index 76c50a8e8453..d4cdc05f140f 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,6 @@ "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": "^0.9.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", "react-native-onyx": "1.0.38", diff --git a/src/CONST.js b/src/CONST.js index 20e28c18e928..047676fa3d13 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -1,6 +1,5 @@ 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'; @@ -8,20 +7,10 @@ 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 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, @@ -234,7 +223,6 @@ const CONST = { CTRL: { DEFAULT: 'control', [PLATFORM_OS_MACOS]: 'meta', - [PLATFORM_IOS]: 'meta', }, SHIFT: { DEFAULT: 'shift', @@ -245,91 +233,46 @@ 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', modifiers: ['CTRL'], - trigger: { - DEFAULT: {input: 'i', modifierFlags: keyModifierControl}, - [PLATFORM_OS_MACOS]: {input: 'i', modifierFlags: keyModifierCommand}, - [PLATFORM_IOS]: {input: 'i', 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, @@ -838,7 +781,7 @@ const CONST = { WINDOWS: 'Windows', MAC_OS: PLATFORM_OS_MACOS, ANDROID: 'Android', - IOS: PLATFORM_IOS, + IOS: 'iOS', LINUX: 'Linux', NATIVE: 'Native', }, diff --git a/src/components/Button/index.js b/src/components/Button.js similarity index 93% rename from src/components/Button/index.js rename to src/components/Button.js index 50165ca98774..817a1c1f9e7d 100644 --- a/src/components/Button/index.js +++ b/src/components/Button.js @@ -1,20 +1,19 @@ 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 validateSubmitShortcut from './validateSubmitShortcut'; +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'; const propTypes = { /** The text for the button label */ @@ -158,10 +157,10 @@ class Button extends Component { // Setup and attach keypress handler for pressing the button with Enter key this.unsubscribe = KeyboardShortcut.subscribe(shortcutConfig.shortcutKey, (e) => { - if (!validateSubmitShortcut(this.props.isFocused, this.props.isDisabled, this.props.isLoading, e)) { + if (!this.props.isFocused || this.props.isDisabled || this.props.isLoading || (e && e.target.nodeName === 'TEXTAREA')) { return; } - + e.preventDefault(); this.props.onPress(); }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true, false, this.props.enterKeyEventListenerPriority, false); } diff --git a/src/components/Button/validateSubmitShortcut/index.js b/src/components/Button/validateSubmitShortcut/index.js deleted file mode 100644 index bfe5c79483fa..000000000000 --- a/src/components/Button/validateSubmitShortcut/index.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 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; diff --git a/src/components/Button/validateSubmitShortcut/index.native.js b/src/components/Button/validateSubmitShortcut/index.native.js deleted file mode 100644 index 2822fa56d590..000000000000 --- a/src/components/Button/validateSubmitShortcut/index.native.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 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 - * @returns {boolean} Returns `true` if the shortcut should be triggered - */ -function validateSubmitShortcut(isFocused, isDisabled, isLoading) { - if (!isFocused || isDisabled || isLoading) { - return false; - } - - return true; -} - -export default validateSubmitShortcut; diff --git a/src/components/KeyboardShortcutsModal.js b/src/components/KeyboardShortcutsModal.js index a5454e280f0d..81ad2f642831 100644 --- a/src/components/KeyboardShortcutsModal.js +++ b/src/components/KeyboardShortcutsModal.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {View, ScrollView} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import HeaderWithCloseButton from './HeaderWithCloseButton'; @@ -34,26 +34,18 @@ const defaultProps = { class KeyboardShortcutsModal extends React.Component { componentDidMount() { - const openShortcutModalConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUT_MODAL; - this.unsubscribeShortcutModal = KeyboardShortcut.subscribe(openShortcutModalConfig.shortcutKey, () => { + const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUT_MODAL; + this.unsubscribeShortcutModal = KeyboardShortcut.subscribe(shortcutConfig.shortcutKey, () => { ModalActions.close(); KeyboardShortcutsActions.showKeyboardShortcutModal(); - }, openShortcutModalConfig.descriptionKey, openShortcutModalConfig.modifiers, true); - - const closeShortcutModalConfig = CONST.KEYBOARD_SHORTCUTS.ESCAPE; - this.unsubscribeEscapeModal = KeyboardShortcut.subscribe(closeShortcutModalConfig.shortcutKey, () => { - ModalActions.close(); - KeyboardShortcutsActions.hideKeyboardShortcutModal(); - }, closeShortcutModalConfig.descriptionKey, closeShortcutModalConfig.modifiers, true, true); + }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true); } componentWillUnmount() { - if (this.unsubscribeShortcutModal) { - this.unsubscribeShortcutModal(); - } - if (this.unsubscribeEscapeModal) { - this.unsubscribeEscapeModal(); + if (!this.unsubscribeShortcutModal) { + return; } + this.unsubscribeShortcutModal(); } /** @@ -93,7 +85,7 @@ class KeyboardShortcutsModal extends React.Component { onClose={KeyboardShortcutsActions.hideKeyboardShortcutModal} > - + {this.props.translate('keyboardShortcutModal.subtitle')} @@ -103,7 +95,7 @@ class KeyboardShortcutsModal extends React.Component { })} - + ); } diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index 28cb56ecbfa2..e47e101c3ea7 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -36,7 +36,7 @@ class ScreenWrapper extends React.Component { } Navigation.dismissModal(); - }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true, true); + }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true); this.unsubscribeTransitionEnd = this.props.navigation.addListener('transitionEnd', (event) => { // Prevent firing the prop callback when user is exiting the page. diff --git a/src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.js b/src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.js deleted file mode 100644 index 338ce921221e..000000000000 --- a/src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.js +++ /dev/null @@ -1,54 +0,0 @@ -import _ from 'underscore'; -import getKeyEventModifiers from '../getKeyEventModifiers'; - -/** - * Checks if an event for that key is configured and if so, runs it. - * @param {Function} getDisplayName - * @param {Object} eventHandlers - * @param {Object} keycommandEvent - * @param {Event} event - * @private - */ -function bindHandlerToKeydownEvent(getDisplayName, eventHandlers, keycommandEvent, event) { - if (!(event instanceof KeyboardEvent)) { - return; - } - - const eventModifiers = getKeyEventModifiers(keycommandEvent); - const displayName = getDisplayName(keycommandEvent.input, eventModifiers); - - // Loop over all the callbacks - _.every(eventHandlers[displayName], (callback) => { - // Early return for excludedNodes - if (_.contains(callback.excludedNodes, event.target.nodeName)) { - return true; - } - - // If configured to do so, prevent input text control to trigger this event - if (!callback.captureOnInputs && ( - event.target.nodeName === 'INPUT' - || event.target.nodeName === 'TEXTAREA' - || event.target.contentEditable === 'true' - )) { - return true; - } - - // Determine if the event should bubble before executing the callback (which may have side-effects) - let shouldBubble = callback.shouldBubble || false; - if (_.isFunction(callback.shouldBubble)) { - shouldBubble = callback.shouldBubble(); - } - - if (_.isFunction(callback.callback)) { - callback.callback(event); - } - if (callback.shouldPreventDefault) { - event.preventDefault(); - } - - // If the event should not bubble, short-circuit the loop - return shouldBubble; - }); -} - -export default bindHandlerToKeydownEvent; diff --git a/src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.native.js b/src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.native.js deleted file mode 100644 index de59c819c504..000000000000 --- a/src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.native.js +++ /dev/null @@ -1,33 +0,0 @@ -import _ from 'underscore'; -import getKeyEventModifiers from '../getKeyEventModifiers'; - -/** - * Checks if an event for that key is configured and if so, runs it. - * @param {Function} getDisplayName - * @param {Object} eventHandlers - * @param {Object} keycommandEvent - * @param {Event} event - * @private - */ -function bindHandlerToKeydownEvent(getDisplayName, eventHandlers, keycommandEvent, event) { - const eventModifiers = getKeyEventModifiers(keycommandEvent); - const displayName = getDisplayName(keycommandEvent.input, eventModifiers); - - // Loop over all the callbacks - _.every(eventHandlers[displayName], (callback) => { - // Determine if the event should bubble before executing the callback (which may have side-effects) - let shouldBubble = callback.shouldBubble || false; - if (_.isFunction(callback.shouldBubble)) { - shouldBubble = callback.shouldBubble(); - } - - if (_.isFunction(callback.callback)) { - callback.callback(event); - } - - // If the event should not bubble, short-circuit the loop - return shouldBubble; - }); -} - -export default bindHandlerToKeydownEvent; diff --git a/src/libs/KeyboardShortcut/getKeyEventModifiers.js b/src/libs/KeyboardShortcut/getKeyEventModifiers.js deleted file mode 100644 index 7865d51a0507..000000000000 --- a/src/libs/KeyboardShortcut/getKeyEventModifiers.js +++ /dev/null @@ -1,27 +0,0 @@ -import * as KeyCommand from 'react-native-key-command'; -import lodashGet from 'lodash/get'; - -/** - * Gets modifiers from a keyboard event. - * - * @param {Event} event - * @returns {Array} - */ -function getKeyEventModifiers(event) { - if (event.modifierFlags === lodashGet(KeyCommand, 'constants.keyModifierControl', 'keyModifierControl')) { - return ['CONTROL']; - } - if (event.modifierFlags === lodashGet(KeyCommand, 'constants.keyModifierCommand', 'keyModifierCommand')) { - return ['META']; - } - if (event.modifierFlags === lodashGet(KeyCommand, 'constants.keyModifierShiftControl', 'keyModifierShiftControl')) { - return ['CONTROL', 'Shift']; - } - if (event.modifierFlags === lodashGet(KeyCommand, 'constants.keyModifierShiftCommand', 'keyModifierShiftCommand')) { - return ['META', 'Shift']; - } - - return []; -} - -export default getKeyEventModifiers; diff --git a/src/libs/KeyboardShortcut/index.js b/src/libs/KeyboardShortcut/index.js index 9ffd01bbc406..39c3a49e0609 100644 --- a/src/libs/KeyboardShortcut/index.js +++ b/src/libs/KeyboardShortcut/index.js @@ -1,13 +1,9 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; -import * as KeyCommand from 'react-native-key-command'; -import bindHandlerToKeydownEvent from './bindHandlerToKeydownEvent'; import getOperatingSystem from '../getOperatingSystem'; import CONST from '../../CONST'; -const operatingSystem = getOperatingSystem(); - // Handlers for the various keyboard listeners we set up const eventHandlers = {}; @@ -21,6 +17,29 @@ function getDocumentedShortcuts() { return _.values(documentedShortcuts); } +/** + * Gets modifiers from a keyboard event. + * + * @param {Event} event + * @returns {Array} + */ +function getKeyEventModifiers(event) { + const modifiers = []; + if (event.shiftKey) { + modifiers.push('SHIFT'); + } + if (event.ctrlKey) { + modifiers.push('CONTROL'); + } + if (event.altKey) { + modifiers.push('ALT'); + } + if (event.metaKey) { + modifiers.push('META'); + } + return modifiers; +} + /** * Generates the normalized display name for keyboard shortcuts. * @@ -29,23 +48,7 @@ function getDocumentedShortcuts() { * @returns {String} */ function getDisplayName(key, modifiers) { - let displayName = (() => { - // Type of key is string and the type of KeyCommand.constants.* is number | string. Use _.isEqual to match different types. - if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputEnter', 'keyInputEnter').toString().toLowerCase())) { - return ['ENTER']; - } - if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputEscape', 'keyInputEscape').toString().toLowerCase())) { - return ['ESCAPE']; - } - if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputUpArrow', 'keyInputUpArrow').toString().toLowerCase())) { - return ['ARROWUP']; - } - if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputDownArrow', 'keyInputDownArrow').toString().toLowerCase())) { - return ['ARROWDOWN']; - } - return [key.toUpperCase()]; - })(); - + let displayName = [key.toUpperCase()]; if (_.isString(modifiers)) { displayName.unshift(modifiers); } else if (_.isArray(modifiers)) { @@ -57,19 +60,56 @@ function getDisplayName(key, modifiers) { return displayName.join(' + '); } -_.each(CONST.KEYBOARD_SHORTCUTS, (shortcut) => { - const shortcutTrigger = lodashGet(shortcut, ['trigger', operatingSystem], lodashGet(shortcut, 'trigger.DEFAULT')); - - // If there is no trigger for the current OS nor a default trigger, then we don't need to do anything - if (!shortcutTrigger) { +/** + * Checks if an event for that key is configured and if so, runs it. + * @param {Event} event + * @private + */ +function bindHandlerToKeydownEvent(event) { + if (!(event instanceof KeyboardEvent)) { return; } - KeyCommand.addListener( - shortcutTrigger, - (keycommandEvent, event) => bindHandlerToKeydownEvent(getDisplayName, eventHandlers, keycommandEvent, event), - ); -}); + const eventModifiers = getKeyEventModifiers(event); + const displayName = getDisplayName(event.key, eventModifiers); + + // Loop over all the callbacks + _.every(eventHandlers[displayName], (callback) => { + // Early return for excludedNodes + if (_.contains(callback.excludedNodes, event.target.nodeName)) { + return true; + } + + // If configured to do so, prevent input text control to trigger this event + if (!callback.captureOnInputs && ( + event.target.nodeName === 'INPUT' + || event.target.nodeName === 'TEXTAREA' + || event.target.contentEditable === 'true' + )) { + return true; + } + + // Determine if the event should bubble before executing the callback (which may have side-effects) + let shouldBubble = callback.shouldBubble || false; + if (_.isFunction(callback.shouldBubble)) { + shouldBubble = callback.shouldBubble(); + } + + if (_.isFunction(callback.callback)) { + callback.callback(event); + } + if (callback.shouldPreventDefault) { + event.preventDefault(); + } + + // If the event should not bubble, short-circuit the loop + return shouldBubble; + }); +} + +// Make sure we don't add multiple listeners +document.removeEventListener('keydown', bindHandlerToKeydownEvent, {capture: true}); +document.addEventListener('keydown', bindHandlerToKeydownEvent, {capture: true}); /** * Unsubscribes a keyboard event handler. @@ -89,6 +129,7 @@ function unsubscribe(displayName, callbackID) { * @returns {Array} */ function getPlatformEquivalentForKeys(keys) { + const operatingSystem = getOperatingSystem(); return _.map(keys, (key) => { if (!_.has(CONST.PLATFORM_SPECIFIC_KEYS, key)) { return key; diff --git a/src/libs/KeyboardShortcut/index.native.js b/src/libs/KeyboardShortcut/index.native.js new file mode 100644 index 000000000000..8c97f2daf343 --- /dev/null +++ b/src/libs/KeyboardShortcut/index.native.js @@ -0,0 +1,12 @@ +/** + * This is a no-op component for native devices because they wouldn't be able to support keyboard shortcuts like + * a website. + */ +const KeyboardShortcut = { + subscribe() { + return () => {}; + }, + getDocumentedShortcuts() { return []; }, +}; + +export default KeyboardShortcut; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index aca688c797c3..b2bd5bf853ab 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -227,7 +227,7 @@ class ReportActionCompose extends React.Component { } this.updateComment('', true); - }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true, true); + }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true); this.setMaxLines(); this.updateComment(this.comment); diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 52ae1f2e0cfc..b4a816ea5740 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -17,8 +17,8 @@ import Logo from '../../../../assets/images/new-expensify.svg'; import pkg from '../../../../package.json'; import * as Report from '../../../libs/actions/Report'; import * as Link from '../../../libs/actions/Link'; +import getPlatformSpecificMenuItems from './getPlatformSpecificMenuItems'; import compose from '../../../libs/compose'; -import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportActionContextMenu'; import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; @@ -30,6 +30,8 @@ const propTypes = { const AboutPage = (props) => { let popoverAnchor; + const platformSpecificMenuItems = getPlatformSpecificMenuItems(props.isSmallScreenWidth); + const menuItems = [ { translationKey: 'initialSettingsPage.aboutPage.appDownloadLinks', @@ -38,11 +40,7 @@ const AboutPage = (props) => { Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS); }, }, - { - translationKey: 'initialSettingsPage.aboutPage.viewKeyboardShortcuts', - icon: Expensicons.Keyboard, - action: KeyboardShortcuts.showKeyboardShortcutModal, - }, + ...platformSpecificMenuItems, { translationKey: 'initialSettingsPage.aboutPage.viewTheCode', icon: Expensicons.Eye, diff --git a/src/pages/settings/AboutPage/getPlatformSpecificMenuItems/index.js b/src/pages/settings/AboutPage/getPlatformSpecificMenuItems/index.js new file mode 100644 index 000000000000..52f8ffa2250f --- /dev/null +++ b/src/pages/settings/AboutPage/getPlatformSpecificMenuItems/index.js @@ -0,0 +1,15 @@ +import * as KeyboardShortcuts from '../../../../libs/actions/KeyboardShortcuts'; +import * as Expensicons from '../../../../components/Icon/Expensicons'; + +export default (isSmallScreenWidth) => { + if (isSmallScreenWidth) { + return []; + } + return [ + { + translationKey: 'initialSettingsPage.aboutPage.viewKeyboardShortcuts', + icon: Expensicons.Keyboard, + action: KeyboardShortcuts.showKeyboardShortcutModal, + }, + ]; +}; diff --git a/src/pages/settings/AboutPage/getPlatformSpecificMenuItems/index.native.js b/src/pages/settings/AboutPage/getPlatformSpecificMenuItems/index.native.js new file mode 100644 index 000000000000..4ba9480748fc --- /dev/null +++ b/src/pages/settings/AboutPage/getPlatformSpecificMenuItems/index.native.js @@ -0,0 +1 @@ +export default () => []; diff --git a/src/styles/styles.js b/src/styles/styles.js index d01703172852..d0e72ea42f17 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -2799,9 +2799,7 @@ const styles = { keyboardShortcutModalContainer: { maxHeight: '100%', - flexShrink: 0, - flexGrow: 0, - flexBasis: 'auto', + flex: '0 0 auto', }, keyboardShortcutTableWrapper: {