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

Fix reply button on media modal not giving focus to compose form #17626

Merged
merged 2 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion app/javascript/mastodon/actions/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
5 changes: 4 additions & 1 deletion app/javascript/mastodon/components/modal_root.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class Footer extends ImmutablePureComponent {
const { router } = this.context;

if (onClose) {
onClose();
onClose(true);
}

dispatch(replyCompose(status, router.history));
Expand Down
9 changes: 5 additions & 4 deletions app/javascript/mastodon/features/ui/components/modal_root.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 {
Expand All @@ -89,20 +90,20 @@ 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) => {
this._modal = c;
}

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} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
}
},
});
Expand Down
30 changes: 25 additions & 5 deletions app/javascript/mastodon/reducers/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down