1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-11-28 08:58:41 +02:00

Scrolling and hotkey improvements

New: Use Esc/Enter for cancel/accept in confirmation modals
Fixed: Modals focused when opened
Fixed: Scrolling with keyboard unless focus is shifted out of scrollable area
Closes #3291
This commit is contained in:
Mark McDowall 2020-03-01 21:03:38 -08:00
parent 52e5d4d0f1
commit 506023b0f3
7 changed files with 108 additions and 27 deletions

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React, { useEffect } from 'react';
import { kinds, sizes } from 'Helpers/Props'; import { kinds, sizes } from 'Helpers/Props';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
@ -21,9 +22,14 @@ function ConfirmModal(props) {
hideCancelButton, hideCancelButton,
isSpinning, isSpinning,
onConfirm, onConfirm,
onCancel onCancel,
bindShortcut
} = props; } = props;
useEffect(() => {
bindShortcut('enter', onConfirm);
}, [onConfirm]);
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
@ -49,7 +55,7 @@ function ConfirmModal(props) {
} }
<SpinnerButton <SpinnerButton
data-autofocus={true} autoFocus={true}
kind={kind} kind={kind}
isSpinning={isSpinning} isSpinning={isSpinning}
onPress={onConfirm} onPress={onConfirm}
@ -74,7 +80,8 @@ ConfirmModal.propTypes = {
hideCancelButton: PropTypes.bool, hideCancelButton: PropTypes.bool,
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired onCancel: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired
}; };
ConfirmModal.defaultProps = { ConfirmModal.defaultProps = {
@ -85,4 +92,4 @@ ConfirmModal.defaultProps = {
isSpinning: false isSpinning: false
}; };
export default ConfirmModal; export default keyboardShortcuts(ConfirmModal);

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock';
import classNames from 'classnames'; import classNames from 'classnames';
import elementClass from 'element-class'; import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
@ -181,31 +182,33 @@ class Modal extends Component {
} }
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div <FocusLock disabled={false}>
className={styles.modalContainer}
>
<div <div
ref={this._setBackgroundRef} className={styles.modalContainer}
className={backdropClassName}
onMouseDown={this.onBackdropBeginPress}
onMouseUp={this.onBackdropEndPress}
> >
<div <div
className={classNames( ref={this._setBackgroundRef}
className, className={backdropClassName}
styles[size] onMouseDown={this.onBackdropBeginPress}
)} onMouseUp={this.onBackdropEndPress}
style={style}
> >
<ErrorBoundary <div
errorComponent={ModalError} className={classNames(
onModalClose={onModalClose} className,
styles[size]
)}
style={style}
> >
{children} <ErrorBoundary
</ErrorBoundary> errorComponent={ModalError}
onModalClose={onModalClose}
>
{children}
</ErrorBoundary>
</div>
</div> </div>
</div> </div>
</div>, </FocusLock>,
this._node this._node
); );
} }

View File

@ -23,6 +23,8 @@ class Scroller extends Component {
if (this.props.scrollTop != null) { if (this.props.scrollTop != null) {
this._scroller.scrollTop = scrollTop; this._scroller.scrollTop = scrollTop;
} }
this._scroller.focus({ preventScroll: true });
} }
// //
@ -58,6 +60,7 @@ class Scroller extends Component {
styles[scrollDirection], styles[scrollDirection],
autoScroll && styles.autoScroll autoScroll && styles.autoScroll
)} )}
tabIndex={-1}
{...otherProps} {...otherProps}
> >
{children} {children}

View File

@ -8,6 +8,16 @@ export const shortcuts = {
name: 'Open This Modal' name: 'Open This Modal'
}, },
CLOSE_MODAL: {
key: 'Esc',
name: 'Close Current Modal'
},
ACCEPT_CONFIRM_MODAL: {
key: 'Enter',
name: 'Accept Confirmation Modal'
},
SERIES_SEARCH_INPUT: { SERIES_SEARCH_INPUT: {
key: 's', key: 's',
name: 'Focus Search Box' name: 'Focus Search Box'

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React, { useEffect } from 'react';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
@ -12,9 +13,14 @@ function PendingChangesModal(props) {
const { const {
isOpen, isOpen,
onConfirm, onConfirm,
onCancel onCancel,
bindShortcut
} = props; } = props;
useEffect(() => {
bindShortcut('enter', onConfirm);
}, [onConfirm]);
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
@ -36,6 +42,7 @@ function PendingChangesModal(props) {
</Button> </Button>
<Button <Button
autoFocus={true}
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onConfirm} onPress={onConfirm}
> >
@ -52,11 +59,12 @@ PendingChangesModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
kind: PropTypes.oneOf(kinds.all), kind: PropTypes.oneOf(kinds.all),
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired onCancel: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired
}; };
PendingChangesModal.defaultProps = { PendingChangesModal.defaultProps = {
kind: kinds.PRIMARY kind: kinds.PRIMARY
}; };
export default PendingChangesModal; export default keyboardShortcuts(PendingChangesModal);

View File

@ -96,6 +96,7 @@
"react-dnd-html5-backend": "9.3.2", "react-dnd-html5-backend": "9.3.2",
"react-document-title": "2.0.3", "react-document-title": "2.0.3",
"react-dom": "16.8.6", "react-dom": "16.8.6",
"react-focus-lock": "2.2.1",
"react-google-recaptcha": "1.1.0", "react-google-recaptcha": "1.1.0",
"react-lazyload": "2.6.2", "react-lazyload": "2.6.2",
"react-measure": "1.4.7", "react-measure": "1.4.7",

View File

@ -825,6 +825,13 @@
"@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-self" "^7.0.0"
"@babel/plugin-transform-react-jsx-source" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0"
"@babel/runtime@^7.0.0":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308"
integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5": "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5":
version "7.5.5" version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
@ -2857,6 +2864,11 @@ detect-newline@2.X:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
detect-node@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
diff@^1.3.2: diff@^1.3.2:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf"
@ -3817,6 +3829,11 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.3.6" readable-stream "^2.3.6"
focus-lock@^0.6.6:
version "0.6.6"
resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.6.tgz#98119a755a38cfdbeda0280eaa77e307eee850c7"
integrity sha512-Dx69IXGCq1qsUExWuG+5wkiMqVM/zGx/reXSJSLogECwp3x6KeNQZ+NAetgxEFpnC41rD8U3+jRCW68+LNzdtw==
for-in@^1.0.1, for-in@^1.0.2: for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -7268,6 +7285,13 @@ react-autowhatever@^10.1.2:
react-themeable "^1.1.0" react-themeable "^1.1.0"
section-iterator "^2.0.0" section-iterator "^2.0.0"
react-clientside-effect@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"
integrity sha512-nRmoyxeok5PBO6ytPvSjKp9xwXg9xagoTK1mMjwnQxqM9Hd7MNPl+LS1bOSOe+CV2+4fnEquc7H/S8QD3q697A==
dependencies:
"@babel/runtime" "^7.0.0"
react-custom-scrollbars@4.2.1: react-custom-scrollbars@4.2.1:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db" resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db"
@ -7313,6 +7337,18 @@ react-dom@16.8.6:
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.13.6" scheduler "^0.13.6"
react-focus-lock@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.2.1.tgz#1d12887416925dc53481914b7cedd39494a3b24a"
integrity sha512-47g0xYcCTZccdzKRGufepY8oZ3W1Qg+2hn6u9SHZ0zUB6uz/4K4xJe7yYFNZ1qT6m+2JDm82F6QgKeBTbjW4PQ==
dependencies:
"@babel/runtime" "^7.0.0"
focus-lock "^0.6.6"
prop-types "^15.6.2"
react-clientside-effect "^1.2.2"
use-callback-ref "^1.2.1"
use-sidecar "^1.0.1"
react-google-recaptcha@1.1.0: react-google-recaptcha@1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-1.1.0.tgz#f33bef3e22e8c016820e80da48d573f516bb99e8" resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-1.1.0.tgz#f33bef3e22e8c016820e80da48d573f516bb99e8"
@ -9210,6 +9246,19 @@ url@^0.11.0:
punycode "1.3.2" punycode "1.3.2"
querystring "0.2.0" querystring "0.2.0"
use-callback-ref@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.1.tgz#898759ccb9e14be6c7a860abafa3ffbd826c89bb"
integrity sha512-C3nvxh0ZpaOxs9RCnWwAJ+7bJPwQI8LHF71LzbQ3BvzH5XkdtlkMadqElGevg5bYBDFip4sAnD4m06zAKebg1w==
use-sidecar@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"
integrity sha512-287RZny6m5KNMTb/Kq9gmjafi7lQL0YHO1lYolU6+tY1h9+Z3uCtkJJ3OSOq3INwYf2hBryCcDh4520AhJibMA==
dependencies:
detect-node "^2.0.4"
tslib "^1.9.3"
use@^3.1.0: use@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"