about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorClaire <claire.github-309c@sitedethib.com>2022-02-25 00:51:01 +0100
committerGitHub <noreply@github.com>2022-02-25 00:51:01 +0100
commit2cd31b31778cec3b282a44f03a03844d92a4e8cc (patch)
tree4b0d299e6ea1f064c80ab5bd62ae61ea7ade197a /app/javascript
parentd4592bbfcd091c4eaef8c8f24c47d5c2ce1bacd3 (diff)
Fix reply button on media modal not giving focus to compose form (#17626)
* Avoid compose form and modal management fighting for focus

* Fix reply button on media modal footer not giving focus to compose form
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/mastodon/actions/modal.js3
-rw-r--r--app/javascript/mastodon/components/modal_root.js5
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js9
-rw-r--r--app/javascript/mastodon/features/picture_in_picture/components/footer.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.js9
-rw-r--r--app/javascript/mastodon/features/ui/containers/modal_container.js11
-rw-r--r--app/javascript/mastodon/reducers/modal.js30
7 files changed, 50 insertions, 19 deletions
diff --git a/app/javascript/mastodon/actions/modal.js b/app/javascript/mastodon/actions/modal.js
index 3d0299db5..3e576fab8 100644
--- a/app/javascript/mastodon/actions/modal.js
+++ b/app/javascript/mastodon/actions/modal.js
@@ -9,9 +9,10 @@ export function openModal(type, props) {
   };
 };
 
-export function closeModal(type) {
+export function closeModal(type, options = { ignoreFocus: false }) {
   return {
     type: MODAL_CLOSE,
     modalType: type,
+    ignoreFocus: options.ignoreFocus,
   };
 };
diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js
index 755c46fd6..b894aeaf9 100644
--- a/app/javascript/mastodon/components/modal_root.js
+++ b/app/javascript/mastodon/components/modal_root.js
@@ -18,6 +18,7 @@ export default class ModalRoot extends React.PureComponent {
       g: PropTypes.number,
       b: PropTypes.number,
     }),
+    ignoreFocus: PropTypes.bool,
   };
 
   activeElement = this.props.children ? document.activeElement : null;
@@ -72,7 +73,9 @@ export default class ModalRoot extends React.PureComponent {
       // immediately selectable, we have to wait for observers to run, as
       // described in https://github.com/WICG/inert#performance-and-gotchas
       Promise.resolve().then(() => {
-        this.activeElement.focus({ preventScroll: true });
+        if (!this.props.ignoreFocus) {
+          this.activeElement.focus({ preventScroll: true });
+        }
         this.activeElement = null;
       }).catch(console.error);
 
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index d75145a09..d7635da40 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -163,8 +163,13 @@ class ComposeForm extends ImmutablePureComponent {
         selectionStart = selectionEnd;
       }
 
-      this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
-      this.autosuggestTextarea.textarea.focus();
+      // Because of the wicg-inert polyfill, the activeElement may not be
+      // immediately selectable, we have to wait for observers to run, as
+      // described in https://github.com/WICG/inert#performance-and-gotchas
+      Promise.resolve().then(() => {
+        this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
+        this.autosuggestTextarea.textarea.focus();
+      }).catch(console.error);
     } else if(prevProps.isSubmitting && !this.props.isSubmitting) {
       this.autosuggestTextarea.textarea.focus();
     } else if (this.props.spoiler !== prevProps.spoiler) {
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
index 0de562ee1..690a77531 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
@@ -60,7 +60,7 @@ class Footer extends ImmutablePureComponent {
     const { router } = this.context;
 
     if (onClose) {
-      onClose();
+      onClose(true);
     }
 
     dispatch(replyCompose(status, router.history));
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index 7b14fe5ca..3fc235849 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -45,6 +45,7 @@ export default class ModalRoot extends React.PureComponent {
     type: PropTypes.string,
     props: PropTypes.object,
     onClose: PropTypes.func.isRequired,
+    ignoreFocus: PropTypes.bool,
   };
 
   state = {
@@ -79,7 +80,7 @@ export default class ModalRoot extends React.PureComponent {
     return <BundleModalError {...props} onClose={onClose} />;
   }
 
-  handleClose = () => {
+  handleClose = (ignoreFocus = false) => {
     const { onClose } = this.props;
     let message = null;
     try {
@@ -89,7 +90,7 @@ export default class ModalRoot extends React.PureComponent {
       // isn't set.
       // This would be much smoother with react-intl 3+ and `forwardRef`.
     }
-    onClose(message);
+    onClose(message, ignoreFocus);
   }
 
   setModalRef = (c) => {
@@ -97,12 +98,12 @@ export default class ModalRoot extends React.PureComponent {
   }
 
   render () {
-    const { type, props } = this.props;
+    const { type, props, ignoreFocus } = this.props;
     const { backgroundColor } = this.state;
     const visible = !!type;
 
     return (
-      <Base backgroundColor={backgroundColor} onClose={this.handleClose}>
+      <Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}>
         {visible && (
           <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
             {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
diff --git a/app/javascript/mastodon/features/ui/containers/modal_container.js b/app/javascript/mastodon/features/ui/containers/modal_container.js
index 34fec8206..35be26222 100644
--- a/app/javascript/mastodon/features/ui/containers/modal_container.js
+++ b/app/javascript/mastodon/features/ui/containers/modal_container.js
@@ -3,22 +3,23 @@ import { openModal, closeModal } from '../../../actions/modal';
 import ModalRoot from '../components/modal_root';
 
 const mapStateToProps = state => ({
-  type: state.getIn(['modal', 0, 'modalType'], null),
-  props: state.getIn(['modal', 0, 'modalProps'], {}),
+  ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
+  type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
+  props: state.getIn(['modal', 'stack', 0, 'modalProps'], {}),
 });
 
 const mapDispatchToProps = dispatch => ({
-  onClose (confirmationMessage) {
+  onClose (confirmationMessage, ignoreFocus = false) {
     if (confirmationMessage) {
       dispatch(
         openModal('CONFIRM', {
           message: confirmationMessage.message,
           confirm: confirmationMessage.confirm,
-          onConfirm: () => dispatch(closeModal()),
+          onConfirm: () => dispatch(closeModal(undefined, { ignoreFocus })),
         }),
       );
     } else {
-      dispatch(closeModal());
+      dispatch(closeModal(undefined, { ignoreFocus }));
     }
   },
 });
diff --git a/app/javascript/mastodon/reducers/modal.js b/app/javascript/mastodon/reducers/modal.js
index 41161a206..3eab07d9d 100644
--- a/app/javascript/mastodon/reducers/modal.js
+++ b/app/javascript/mastodon/reducers/modal.js
@@ -3,16 +3,36 @@ import { TIMELINE_DELETE } from '../actions/timelines';
 import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
 import { Stack as ImmutableStack, Map as ImmutableMap } from 'immutable';
 
-export default function modal(state = ImmutableStack(), action) {
+const initialState = ImmutableMap({
+  ignoreFocus: false,
+  stack: ImmutableStack(),
+});
+
+const popModal = (state, { modalType, ignoreFocus }) => {
+  if (modalType === undefined || modalType === state.getIn(['stack', 0, 'modalType'])) {
+    return state.set('ignoreFocus', !!ignoreFocus).update('stack', stack => stack.shift());
+  } else {
+    return state;
+  }
+};
+
+const pushModal = (state, modalType, modalProps) => {
+  return state.withMutations(map => {
+    map.set('ignoreFocus', false);
+    map.update('stack', stack => stack.unshift(ImmutableMap({ modalType, modalProps })));
+  });
+};
+
+export default function modal(state = initialState, action) {
   switch(action.type) {
   case MODAL_OPEN:
-    return state.unshift(ImmutableMap({ modalType: action.modalType, modalProps: action.modalProps }));
+    return pushModal(state, action.modalType, action.modalProps);
   case MODAL_CLOSE:
-    return (action.modalType === undefined || action.modalType === state.getIn([0, 'modalType'])) ? state.shift() : state;
+    return popModal(state, action);
   case COMPOSE_UPLOAD_CHANGE_SUCCESS:
-    return state.getIn([0, 'modalType']) === 'FOCAL_POINT' ? state.shift() : state;
+    return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
   case TIMELINE_DELETE:
-    return state.filterNot((modal) => modal.get('modalProps').statusId === action.id);
+    return state.update('stack', stack => stack.filterNot((modal) => modal.get('modalProps').statusId === action.id));
   default:
     return state;
   }