Skip to content
Open
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
6 changes: 5 additions & 1 deletion src/components/SidePanel/SidePanelModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,16 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi

// Web back button: push history state and close Side Panel on popstate
useEffect(() => {
// Side Panel is not a normal modal on ExtraLargeScreenWidth.
if (isExtraLargeScreenWidth) {
return;
}
ComposerFocusManager.resetReadyToFocus(uniqueModalId);
return () => {
ComposerFocusManager.setReadyToFocus(uniqueModalId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [isExtraLargeScreenWidth]);

return (
<ModalPortal>
Expand Down
5 changes: 5 additions & 0 deletions src/libs/ReportActionComposeFocusManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ type ComposerType = 'main' | 'edit';
type FocusCallback = (shouldFocusForNonBlurInputOnTapOutside?: boolean) => void;

const composerRef: RefObject<TextInput | null> = React.createRef<TextInput>();
/**
* There can be 2 composers present at the same time. This ref is for the side panel.
*/
const sidePanelComposerRef: RefObject<TextInput | null> = React.createRef<TextInput>();

// There are two types of composer: general composer (edit composer) and main composer.
// The general composer callback will take priority if it exists.
Expand Down Expand Up @@ -104,6 +108,7 @@ function preventEditComposerFocusOnFirstResponderOnce() {

export default {
composerRef,
sidePanelComposerRef,
onComposerFocus,
focus,
clear,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Composer from '@components/Composer';
import type {CustomSelectionChangeEvent, TextSelection} from '@components/Composer/types';
import {useWideRHPState} from '@components/WideRHPContextProvider';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useIsInSidePanel from '@hooks/useIsInSidePanel';
import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
Expand Down Expand Up @@ -263,6 +264,7 @@ function ComposerWithSuggestions({
const mobileInputScrollPosition = useRef(0);
const cursorPositionValue = useSharedValue({x: 0, y: 0});
const tag = useSharedValue(-1);
const isInSidePanel = useIsInSidePanel();
const [draftComment = ''] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`);
const [value, setValue] = useState(() => {
if (draftComment) {
Expand Down Expand Up @@ -683,21 +685,33 @@ function ComposerWithSuggestions({
};
}, [focus, route.key, shouldAutoFocus, shouldDelayAutoFocus]);

/**
* Tracks whether there is a composer input inside the side panel on the screen.
*/
const handleSidePanelFocus = useCallback(() => {
if (!isInSidePanel) {
ReportActionComposeFocusManager.sidePanelComposerRef.current = null;
} else {
ReportActionComposeFocusManager.sidePanelComposerRef.current = textInputRef.current;
}
}, [isInSidePanel]);

/**
* Set focus callback
* @param shouldTakeOverFocus - Whether this composer should gain focus priority
*/
const setUpComposeFocusManager = useCallback(
(shouldTakeOverFocus = false) => {
ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNonBlurInputOnTapOutside = false) => {
handleSidePanelFocus();
if ((!willBlurTextInputOnTapOutside && !shouldFocusForNonBlurInputOnTapOutside) || !isFocused || !isSidePanelHiddenOrLargeScreen) {
return;
}

focus(true);
}, shouldTakeOverFocus);
},
[focus, isFocused, isSidePanelHiddenOrLargeScreen],
[focus, isFocused, isSidePanelHiddenOrLargeScreen, handleSidePanelFocus],
);

/**
Expand Down Expand Up @@ -797,6 +811,11 @@ function ComposerWithSuggestions({
return;
}

// Do not focus side panels composer if it wasn't focused before
if (isInSidePanel && !ReportActionComposeFocusManager.sidePanelComposerRef.current) {
return;
}

// Do not focus the composer if the Side Panel is visible
if (!isSidePanelHiddenOrLargeScreen) {
return;
Expand All @@ -814,7 +833,7 @@ function ComposerWithSuggestions({
return;
}
focus(true);
}, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal?.isVisible, isNextModalWillOpenRef, shouldAutoFocus, isSidePanelHiddenOrLargeScreen]);
}, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal?.isVisible, isNextModalWillOpenRef, shouldAutoFocus, isSidePanelHiddenOrLargeScreen, isInSidePanel]);

useEffect(() => {
// Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit
Expand Down Expand Up @@ -913,10 +932,10 @@ function ComposerWithSuggestions({
);

const handleFocus = useCallback(() => {
// The last composer that had focus should re-gain focus
setUpComposeFocusManager(true);
handleSidePanelFocus();
setUpComposeFocusManager(!isInSidePanel);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve priority callback for focused side-panel composer

In handleFocus, passing !isInSidePanel to setUpComposeFocusManager means a side-panel composer never becomes the priority focus callback. ReportActionComposeFocusManager.focus() always executes priorityFocusCallback first, and report actions still call focus() after menus/popovers close (for example in ContextMenuActions.tsx), so when a user is actively composing in Concierge Anywhere those flows can refocus the main report composer instead of the side-panel input.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is intentional, stops side panel from stealing focus when we switch reports

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear priority focus callback when main composer unmounts

Registering the main composer with setUpComposeFocusManager(!isInSidePanel) makes it a priority callback, but the unmount cleanup in this component still calls ReportActionComposeFocusManager.clear() (non-priority). That leaves a stale priorityFocusCallback behind after route/report switches, and ReportActionComposeFocusManager.focus() always invokes priority first and returns, so later focus requests can be swallowed by an unmounted composer instead of reaching the active one. This is reproducible when a report composer was focused, then unmounted, and a subsequent flow triggers focus() before the new composer is manually focused.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It was true before (always priority) so I think it's ok

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow side-panel focus to replace stale priority callback

New in this commit, side-panel focus now always registers as non-priority via setUpComposeFocusManager(!isInSidePanel). Because unmount cleanup still calls ReportActionComposeFocusManager.clear() (which only clears the non-priority callback), any previously-registered main priority callback can persist after report transitions; once that happens, ReportActionComposeFocusManager.focus() returns after invoking the stale priority callback and never reaches the active side-panel callback. This causes focus restoration flows (e.g., after menu/modal close) in Concierge Anywhere to be swallowed until a main composer re-registers priority.

Useful? React with 👍 / 👎.

onFocus();
}, [onFocus, setUpComposeFocusManager]);
}, [onFocus, setUpComposeFocusManager, handleSidePanelFocus, isInSidePanel]);

// When using the suggestions box (Suggestions) we need to imperatively
// set the cursor to the end of the suggestion/mention after it's selected.
Expand Down
Loading