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: issue 23908 #27190

Merged
merged 21 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
002241b
fix: keep keyboardDidHideListener at most one
sangar-1028 Sep 11, 2023
920d7c8
fix: prevent blurring when buttons are clicked on web
sangar-1028 Sep 11, 2023
09fe747
fix: add clearActiveReportAction for EmojiPicker and ReportActionCont…
sangar-1028 Sep 11, 2023
3951fd7
fix: show composer only when there is no focused composer
sangar-1028 Sep 11, 2023
d0e69fe
fix: update isContextMenuActive dynamically in ReportActionItem
sangar-1028 Sep 11, 2023
1b7e45d
fix: minor bugs
sangar-1028 Sep 11, 2023
b0dcc75
fix: bug because of refocus before unmount
sangar-1028 Sep 11, 2023
164f88d
fix: lint error
sangar-1028 Sep 11, 2023
b887abd
fix: create isActive method, add comments
sangar-1028 Sep 12, 2023
77da6ed
fix: blur input before showing confirm delete modal to prevent auto r…
sangar-1028 Sep 12, 2023
418ddd4
fix: remove console log
sangar-1028 Sep 12, 2023
b79780d
Merge branch 'main' into fix/issue-23908
sangar-1028 Sep 12, 2023
b2f4e3a
fix: restore emoji button
sangar-1028 Sep 12, 2023
98d573e
fix: restore previous code
sangar-1028 Sep 12, 2023
3a246e6
fix: update comments
sangar-1028 Sep 12, 2023
b96510d
fix: conflicts
sangar-1028 Sep 12, 2023
a74b2b2
fix: remove same code from deleteDraft
sangar-1028 Sep 12, 2023
e4a381f
fix: initialize textinput on mount
sangar-1028 Sep 14, 2023
97aaa26
Merge branch 'main' into fix/issue-23908
sangar-1028 Sep 14, 2023
c24df9c
revert wrong changes
sangar-1028 Sep 14, 2023
4e9083f
fix: ReportActionContextMenu.isActiveReportAction
sangar-1028 Sep 14, 2023
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
4 changes: 3 additions & 1 deletion src/components/EmojiPicker/EmojiPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,11 @@ const EmojiPicker = forwardRef((props, ref) => {
*/
const isActive = (id) => Boolean(id) && id === activeID;

const clearActive = () => setActiveID(null);

const resetEmojiPopoverAnchor = () => (emojiPopoverAnchor.current = null);

useImperativeHandle(ref, () => ({showEmojiPicker, isActive, hideEmojiPicker, isEmojiPickerVisible, resetEmojiPopoverAnchor}));
useImperativeHandle(ref, () => ({showEmojiPicker, isActive, clearActive, hideEmojiPicker, isEmojiPickerVisible, resetEmojiPopoverAnchor}));

useEffect(() => {
const emojiPopoverDimensionListener = Dimensions.addEventListener('change', () => {
Expand Down
9 changes: 8 additions & 1 deletion src/libs/actions/EmojiPickerAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ function isActive(id) {
return emojiPickerRef.current.isActive(id);
}

function clearActive() {
if (!emojiPickerRef.current) {
return;
}
return emojiPickerRef.current.clearActive();
}

function isEmojiPickerVisible() {
if (!emojiPickerRef.current) {
return;
Expand All @@ -59,4 +66,4 @@ function resetEmojiPopoverAnchor() {
return emojiPickerRef.current.resetEmojiPopoverAnchor();
}

export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActive, isEmojiPickerVisible, resetEmojiPopoverAnchor};
export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActive, clearActive, isEmojiPickerVisible, resetEmojiPopoverAnchor};

This file was deleted.

This file was deleted.

5 changes: 5 additions & 0 deletions src/libs/setShouldShowComposeInputKeyboardAware/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as Composer from '../actions/Composer';

export default (shouldShow) => {
Composer.setShouldShowComposeInput(shouldShow);
};
26 changes: 26 additions & 0 deletions src/libs/setShouldShowComposeInputKeyboardAware/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Keyboard} from 'react-native';
import * as Composer from '../actions/Composer';

let keyboardDidHideListener = null;
export default (shouldShow) => {
if (keyboardDidHideListener) {
keyboardDidHideListener.remove();
keyboardDidHideListener = null;
}

if (!shouldShow) {
Composer.setShouldShowComposeInput(false);
return;
}

// If keyboard is already hidden, we should show composer immediately because keyboardDidHide event won't be called
if (!Keyboard.isVisible()) {
s-alves10 marked this conversation as resolved.
Show resolved Hide resolved
Composer.setShouldShowComposeInput(true);
return;
}

keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
Composer.setShouldShowComposeInput(true);
keyboardDidHideListener.remove();
});
};
6 changes: 3 additions & 3 deletions src/pages/home/report/ContextMenu/ContextMenuActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as ReportUtils from '../../../../libs/ReportUtils';
import * as ReportActionsUtils from '../../../../libs/ReportActionsUtils';
import * as PersonalDetailsUtils from '../../../../libs/PersonalDetailsUtils';
import ReportActionComposeFocusManager from '../../../../libs/ReportActionComposeFocusManager';
import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu';
import {hideContextMenu, showDeleteModal, clearActiveReportAction} from './ReportActionContextMenu';
import CONST from '../../../../CONST';
import getAttachmentDetails from '../../../../libs/fileDownload/getAttachmentDetails';
import fileDownload from '../../../../libs/fileDownload';
Expand Down Expand Up @@ -321,12 +321,12 @@ export default [
onPress: (closePopover, {reportID, reportAction}) => {
if (closePopover) {
// Hide popover, then call showDeleteConfirmModal
hideContextMenu(false, () => showDeleteModal(reportID, reportAction));
hideContextMenu(false, () => showDeleteModal(reportID, reportAction, true, clearActiveReportAction, clearActiveReportAction));
return;
}

// No popover to hide, call showDeleteConfirmModal immediately
showDeleteModal(reportID, reportAction);
showDeleteModal(reportID, reportAction, true, clearActiveReportAction, clearActiveReportAction);
},
getDescription: () => {},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class PopoverReportActionContextMenu extends React.Component {
this.runAndResetOnPopoverHide = this.runAndResetOnPopoverHide.bind(this);
this.getContextMenuMeasuredLocation = this.getContextMenuMeasuredLocation.bind(this);
this.isActiveReportAction = this.isActiveReportAction.bind(this);
this.clearActiveReportAction = this.clearActiveReportAction.bind(this);

this.dimensionsEventListener = null;

Expand Down Expand Up @@ -116,6 +117,10 @@ class PopoverReportActionContextMenu extends React.Component {
return Boolean(actionID) && this.state.reportActionID === actionID;
}

clearActiveReportAction() {
this.setState({reportID: '0', reportAction: {}});
}

/**
* Show the ReportActionContextMenu modal popover.
*
Expand Down Expand Up @@ -332,10 +337,7 @@ class PopoverReportActionContextMenu extends React.Component {
shouldSetModalVisibility={this.state.shouldSetModalVisibilityForDeleteConfirmation}
onConfirm={this.confirmDeleteAndHideModal}
onCancel={this.hideDeleteModal}
onModalHide={() => {
this.setState({reportID: '0', reportAction: {}});
this.callbackWhenDeleteModalHide();
}}
onModalHide={this.callbackWhenDeleteModalHide}
prompt={this.props.translate('reportActionContextMenu.deleteConfirmation', {action: this.state.reportAction})}
confirmText={this.props.translate('common.delete')}
cancelText={this.props.translate('common.cancel')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,11 @@ function isActiveReportAction(actionID) {
return contextMenuRef.current.isActiveReportAction(actionID);
}

export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, showDeleteModal, hideDeleteModal};
function clearActiveReportAction() {
if (!contextMenuRef.current) {
return;
}
return contextMenuRef.current.clearActiveReportAction();
}

export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, clearActiveReportAction, showDeleteModal, hideDeleteModal};
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ function ReportActionCompose({

const onBlur = useCallback((e) => {
setIsFocused(false);
suggestionsRef.current.resetSuggestions();
if (suggestionsRef.current) {
suggestionsRef.current.resetSuggestions();
}
if (e.relatedTarget && e.relatedTarget === actionButtonRef.current) {
isKeyboardVisibleWhenShowingModalRef.current = true;
}
Expand Down
6 changes: 6 additions & 0 deletions src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ function ReportActionItem(props) {
const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID);

// When active action changes, we need to update the `isContextMenuActive` state
const isActiveReportActionForMenu = ReportActionContextMenu.isActiveReportAction(props.action.reportActionID);
useEffect(() => {
setIsContextMenuActive(isActiveReportActionForMenu);
}, [isActiveReportActionForMenu]);
Comment on lines +141 to +145
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this effect depends on temporary data it is not synced with the lifecycle of other components creating inconsistencies between states of components. It caused #28029


const updateHiddenState = useCallback(
(isHiddenValue) => {
setIsHidden(isHiddenValue);
Expand Down
63 changes: 40 additions & 23 deletions src/pages/home/report/ReportActionItemMessageEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import containerComposeStyles from '../../../styles/containerComposeStyles';
import Composer from '../../../components/Composer';
import * as Report from '../../../libs/actions/Report';
import {withReportActionsDrafts} from '../../../components/OnyxProvider';
import openReportActionComposeViewWhenClosingMessageEdit from '../../../libs/openReportActionComposeViewWhenClosingMessageEdit';
import setShouldShowComposeInputKeyboardAware from '../../../libs/setShouldShowComposeInputKeyboardAware';
import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager';
import EmojiPickerButton from '../../../components/EmojiPicker/EmojiPickerButton';
import Icon from '../../../components/Icon';
Expand All @@ -28,7 +28,6 @@ import ExceededCommentLength from '../../../components/ExceededCommentLength';
import CONST from '../../../CONST';
import refPropTypes from '../../../components/refPropTypes';
import * as ComposerUtils from '../../../libs/ComposerUtils';
import * as ComposerActions from '../../../libs/actions/Composer';
import * as User from '../../../libs/actions/User';
import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback';
import getButtonState from '../../../libs/getButtonState';
Expand Down Expand Up @@ -83,8 +82,6 @@ const defaultProps = {
};

// native ids
const saveButtonID = 'saveButton';
const cancelButtonID = 'cancelButton';
const emojiButtonID = 'emojiButton';
const messageEditInput = 'messageEditInput';

Expand Down Expand Up @@ -116,6 +113,12 @@ function ReportActionItemMessageEdit(props) {
isFocusedRef.current = isFocused;
}, [isFocused]);

// We consider the report action active if it's focused, its emoji picker is open or its context menu is open
const isActive = useCallback(
() => isFocusedRef.current || EmojiPickerAction.isActive(props.action.reportActionID) || ReportActionContextMenu.isActiveReportAction(props.action.reportActionID),
[props.action.reportActionID],
);

useEffect(() => {
// For mobile Safari, updating the selection prop on an unfocused input will cause it to automatically gain focus
// and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style),
Expand All @@ -129,16 +132,23 @@ function ReportActionItemMessageEdit(props) {
});

return () => {
// Skip if this is not the focused message so the other edit composer stays focused.
// In small screen devices, when EmojiPicker is shown, the current edit message will lose focus, we need to check this case as well.
if (!isFocusedRef.current && !EmojiPickerAction.isActive(props.action.reportActionID)) {
// Skip if the current report action is not active
if (!isActive()) {
return;
}

if (EmojiPickerAction.isActive(props.action.reportActionID)) {
EmojiPickerAction.clearActive();
}
if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) {
ReportActionContextMenu.clearActiveReportAction();
}

// Show the main composer when the focused message is deleted from another client
// to prevent the main composer stays hidden until we swtich to another chat.
ComposerActions.setShouldShowComposeInput(true);
setShouldShowComposeInputKeyboardAware(true);
Comment on lines -156 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB. Do we need to make this keyboard aware? Will this introduce any delay?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. I think showing composer after keyboard is closed is good. If keyboard isn't visible, this would show composer immediately

};
// eslint-disable-next-line react-hooks/exhaustive-deps -- this cleanup needs to be called only on unmount
}, [props.action.reportActionID]);

/**
Expand Down Expand Up @@ -211,9 +221,11 @@ function ReportActionItemMessageEdit(props) {
const deleteDraft = useCallback(() => {
debouncedSaveDraft.cancel();
Report.saveReportActionDraft(props.reportID, props.action.reportActionID, '');
ComposerActions.setShouldShowComposeInput(true);
ReportActionComposeFocusManager.clear();
ReportActionComposeFocusManager.focus();

if (isActive()) {
ReportActionComposeFocusManager.clear();
ReportActionComposeFocusManager.focus();
}
Comment on lines -232 to +244
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may be causing a regression. Can we remove the condition here? Currently if we delete a message the focus is not set back to the main composer but it should

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that solution won't work. We still need to find a way to fix the regression though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should focus the main composer if the deleted message is active. Please let me know if I am wrong

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct but apparently this is not the behavior

Screen.Recording.2023-09-14.at.5.29.25.PM.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s77rt

There were some changes recently. This is due to this PR. Let me find a way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the bug of isActiveReportAction. Now this works fine as expected

Copy link
Contributor

@techievivek techievivek Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is resolved, right? Because I too agree the expected behaviour is that composer should get the focus.

Copy link
Contributor Author

@s-alves10 s-alves10 Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, thanks.


// Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
if (props.index === 0) {
Expand All @@ -222,7 +234,7 @@ function ReportActionItemMessageEdit(props) {
keyboardDidHideListener.remove();
});
}
}, [props.action.reportActionID, debouncedSaveDraft, props.index, props.reportID, reportScrollManager]);
}, [props.action.reportActionID, debouncedSaveDraft, props.index, props.reportID, reportScrollManager, isActive]);

/**
* Save the draft of the comment to be the new comment message. This will take the comment out of "edit mode" with
Expand Down Expand Up @@ -257,6 +269,7 @@ function ReportActionItemMessageEdit(props) {

// When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.
if (!trimmedNewDraft) {
textInputRef.current.blur();
ReportActionContextMenu.showDeleteModal(props.reportID, props.action, false, deleteDraft, () => InteractionManager.runAfterInteractions(() => textInputRef.current.focus()));
return;
}
Expand Down Expand Up @@ -309,14 +322,15 @@ function ReportActionItemMessageEdit(props) {
<PressableWithFeedback
onPress={deleteDraft}
style={styles.chatItemSubmitButton}
nativeID={cancelButtonID}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('common.close')}
// disable dimming
hoverDimmingValue={1}
pressDimmingValue={1}
hoverStyle={StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.ACTIVE)}
pressStyle={StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)}
// Keep focus on the composer when cancel button is clicked.
onMouseDown={(e) => e.preventDefault()}
>
{({hovered, pressed}) => (
<Icon
Expand Down Expand Up @@ -353,21 +367,23 @@ function ReportActionItemMessageEdit(props) {
onFocus={() => {
setIsFocused(true);
reportScrollManager.scrollToIndex({animated: true, index: props.index}, true);
ComposerActions.setShouldShowComposeInput(false);
setShouldShowComposeInputKeyboardAware(false);

// Clear active report action when another action gets focused
if (!EmojiPickerAction.isActive(props.action.reportActionID)) {
EmojiPickerAction.clearActive();
}
if (!ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) {
ReportActionContextMenu.clearActiveReportAction();
}
Comment on lines +388 to +394
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we do this on onBlur?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. We should keep active report action until another draft gets focused. This wasn't already confirmed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s-alves10 This is not resolved yet. Can we do this any other way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clear active report action when another action gets focused
We shouldn't clear active action on blur because blurred action can be active.

I think we confirmed this in the proposal

Note: I guess we can clear active in onModalHide of EmojiPickerButton for EmojiPicker, and in confirm/cancel callback of showDeleteModal for ReportActionContextMenu. Please let me know your thoughts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s77rt

I think the new approach can cause a regression.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I don't think this is a blocker for now. Will need to test though

}}
onBlur={(event) => {
setIsFocused(false);
const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id');

// Return to prevent re-render when save/cancel button is pressed which cancels the onPress event by re-rendering
if (_.contains([saveButtonID, cancelButtonID, emojiButtonID], relatedTargetId)) {
return;
}

if (messageEditInput === relatedTargetId) {
if (_.contains([messageEditInput, emojiButtonID], relatedTargetId)) {
return;
}
openReportActionComposeViewWhenClosingMessageEdit();
setShouldShowComposeInputKeyboardAware(true);
}}
selection={selection}
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
Expand All @@ -391,12 +407,13 @@ function ReportActionItemMessageEdit(props) {
<PressableWithFeedback
style={[styles.chatItemSubmitButton, hasExceededMaxCommentLength ? {} : styles.buttonSuccess]}
onPress={publishDraft}
nativeID={saveButtonID}
disabled={hasExceededMaxCommentLength}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('common.saveChanges')}
hoverDimmingValue={1}
pressDimmingValue={0.2}
// Keep focus on the composer when send button is clicked.
onMouseDown={(e) => e.preventDefault()}
>
<Icon
src={Expensicons.Checkmark}
Expand Down
Loading