From 7ba88e0930c6bf429cb3051ee8b1bd2ee8aca041 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Sun, 4 Oct 2026 00:27:58 +0800 Subject: [PATCH 1/5] Content overlaps with Custom instructions field in landscape mode --- src/pages/settings/Agents/AddAgentPage.tsx | 1 + src/pages/settings/Agents/Fields/EditPromptPage.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pages/settings/Agents/AddAgentPage.tsx b/src/pages/settings/Agents/AddAgentPage.tsx index d9bc458715ac..9f74b5c1e9c9 100644 --- a/src/pages/settings/Agents/AddAgentPage.tsx +++ b/src/pages/settings/Agents/AddAgentPage.tsx @@ -255,6 +255,7 @@ function AddAgentPageContent({route, template}: AddAgentPageContentProps) { }} > Date: Mon, 19 Oct 2026 23:30:54 +0800 Subject: [PATCH 2/5] fix: issue --- .../settings/Agents/DynamicAddAgentPage.tsx | 7 ++-- .../Agents/Fields/DynamicEditPromptPage.tsx | 7 ++-- .../pages/settings/EditPromptPageTest.tsx | 35 +++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index 237928f0aa29..00a9b261ea26 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -73,6 +73,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten const shouldAutoGrowPromptInput = Platform.OS !== 'web' && !isInLandscapeMode; const shouldUseScrollableLayout = shouldAutoGrowPromptInput || isInLandscapeMode; const shouldShrinkPromptInput = isInLandscapeMode && isKeyboardActive; + const landscapePromptHeight = shouldShrinkPromptInput ? PROMPT_MAX_HEIGHT_ON_KEYBOARD_OPEN_LANDSCAPE_MODE : styles.h42.height; const {accountID: ownerAccountID, login: ownerLogin, displayName} = useCurrentUserPersonalDetails(); const defaultAgentName = template?.name ?? (displayName ? translate('addAgentPage.defaultAgentName', displayName) : undefined); const defaultPrompt = template?.prompt ?? translate('addAgentPage.defaultPrompt'); @@ -258,7 +259,6 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten }} > (null); @@ -126,7 +127,6 @@ function DynamicEditPromptPage({route}: DynamicEditPromptPageProps) { }} > jest.fn(() => false)); + jest.mock('@userActions/Agent', () => ({ updateAgentPrompt: jest.fn(), })); @@ -76,7 +82,11 @@ jest.mock('@components/Form/FormProvider', () => { }); jest.mock('@components/Form/InputWrapper', () => { + const {useEffect} = jest.requireActual('react'); function MockInputWrapper({inputID, defaultValue}: {inputID: string; defaultValue?: string}) { + useEffect(() => { + mockPromptMount(); + }, []); return `${inputID}::${defaultValue ?? ''}`; } return MockInputWrapper; @@ -98,6 +108,31 @@ describe('EditPromptPage', () => { mockUseOnyx.mockReturnValue([undefined, {status: 'loaded'}]); }); + it('keeps the prompt mounted when rotating on native', () => { + // Given a native prompt editor in portrait, remounting would discard the input's focus and selection. + const platform = jest.replaceProperty(Platform, 'OS', 'ios'); + const mockLandscape = jest.mocked(useIsInLandscapeMode); + mockLandscape.mockReturnValue(false); + const page = ( + + ); + const {rerender} = render(page); + expect(mockPromptMount).toHaveBeenCalledTimes(1); + + // When the device rotates to landscape and back while the editor is open. + mockLandscape.mockReturnValue(true); + rerender(React.cloneElement(page)); + mockLandscape.mockReturnValue(false); + rerender(React.cloneElement(page)); + + // Then the original input survives both layout changes, preserving native editing state. + expect(mockPromptMount).toHaveBeenCalledTimes(1); + platform.restore(); + }); + it('renders page title', () => { const {toJSON} = render( Date: Thu, 22 Oct 2026 16:23:40 +0800 Subject: [PATCH 3/5] fix: overlap --- src/pages/settings/Agents/DynamicAddAgentPage.tsx | 15 ++++++++------- .../Agents/Fields/DynamicEditPromptPage.tsx | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index fef1e7865f0e..a725f5f5e18d 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -75,9 +75,9 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten const {windowWidth, windowHeight} = useWindowDimensions(); const {isKeyboardActive} = useKeyboardState(); const isInLandscapeMode = isInLandscapeModeUtil(windowWidth, windowHeight); - // On native portrait the prompt grows with its content up to a max height instead of filling the screen, so the open + // On native the prompt grows with its content up to a max height instead of filling the screen, so the open // keyboard can't squeeze it or hide the line being edited, and the form scrolls to fit it above the keyboard. - const shouldAutoGrowPromptInput = Platform.OS !== 'web' && !isInLandscapeMode; + const shouldAutoGrowPromptInput = Platform.OS !== 'web'; const shouldUseScrollableLayout = shouldAutoGrowPromptInput || isInLandscapeMode; const shouldShrinkPromptInput = isInLandscapeMode && isKeyboardActive; const landscapePromptHeight = shouldShrinkPromptInput ? PROMPT_MAX_HEIGHT_ON_KEYBOARD_OPEN_LANDSCAPE_MODE : styles.h42.height; @@ -310,11 +310,12 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten )} { promptTopOffsetRef.current = event.nativeEvent.layout.y; }} diff --git a/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx b/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx index 6161dc7d6ad8..259699fdb403 100644 --- a/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx +++ b/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx @@ -42,9 +42,9 @@ function DynamicEditPromptPage({route}: DynamicEditPromptPageProps) { const styles = useThemeStyles(); const {isKeyboardActive} = useKeyboardState(); const isInLandscapeMode = useIsInLandscapeMode(); - // On native portrait the prompt grows with its content up to a max height instead of filling the screen, so the open + // On native the prompt grows with its content up to a max height instead of filling the screen, so the open // keyboard can't squeeze it or hide the line being edited, and the form scrolls to fit it above the keyboard. - const shouldAutoGrowPromptInput = Platform.OS !== 'web' && !isInLandscapeMode; + const shouldAutoGrowPromptInput = Platform.OS !== 'web'; const shouldUseScrollableLayout = shouldAutoGrowPromptInput || isInLandscapeMode; const shouldShrinkPromptInput = isInLandscapeMode && isKeyboardActive; const landscapePromptHeight = shouldShrinkPromptInput ? PROMPT_MAX_HEIGHT_ON_KEYBOARD_OPEN_LANDSCAPE_MODE : styles.h42.height; @@ -117,11 +117,12 @@ function DynamicEditPromptPage({route}: DynamicEditPromptPageProps) { > { promptTopOffsetRef.current = event.nativeEvent.layout.y; }} From 49f1d78e8b8584df0fa40a720dce73efa84de642 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Thu, 22 Oct 2026 16:36:13 +0800 Subject: [PATCH 4/5] fix: rotate issue --- .../settings/Agents/DynamicAddAgentPage.tsx | 13 +++++- .../Agents/Fields/DynamicEditPromptPage.tsx | 13 +++++- .../pages/settings/EditPromptPageTest.tsx | 46 +++++++++++++++++-- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index a725f5f5e18d..164a438b041f 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -238,7 +238,11 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten }; const promptTopOffsetRef = useRef(0); - const handleInputFocus = () => scrollToMultilineInput(formRef, shouldUseScrollableLayout, promptTopOffsetRef.current); + const isPromptFocusedRef = useRef(false); + const handleInputFocus = () => { + isPromptFocusedRef.current = true; + scrollToMultilineInput(formRef, shouldUseScrollableLayout, promptTopOffsetRef.current); + }; const agentAvatar = avatarSource ? ( { promptTopOffsetRef.current = event.nativeEvent.layout.y; + // Rotation preserves focus, so reveal the prompt again after its new layout is measured. + if (isPromptFocusedRef.current) { + scrollToMultilineInput(formRef, shouldUseScrollableLayout, promptTopOffsetRef.current); + } }} > { + isPromptFocusedRef.current = false; + }} /> {`${translate('addAgentPage.copilotNote')} ${translate('workspace.rules.agentRules.disclaimer')}`} diff --git a/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx b/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx index 259699fdb403..cca0ae0e3e51 100644 --- a/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx +++ b/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx @@ -52,7 +52,11 @@ function DynamicEditPromptPage({route}: DynamicEditPromptPageProps) { const [agentPrompt] = useOnyx(`${ONYXKEYS.COLLECTION.AGENT}${accountID}`); const formRef = useRef(null); const promptTopOffsetRef = useRef(0); - const scrollToInput = () => scrollToMultilineInput(formRef, shouldUseScrollableLayout, promptTopOffsetRef.current); + const isPromptFocusedRef = useRef(false); + const scrollToInput = () => { + isPromptFocusedRef.current = true; + scrollToMultilineInput(formRef, shouldUseScrollableLayout, promptTopOffsetRef.current); + }; const backPath = useDynamicBackPath(DYNAMIC_ROUTES.AGENT_EDIT_PROMPT.path); const validate = (values: FormOnyxValues): FormInputErrors => { @@ -125,6 +129,10 @@ function DynamicEditPromptPage({route}: DynamicEditPromptPageProps) { ]} onLayout={(event) => { promptTopOffsetRef.current = event.nativeEvent.layout.y; + // Rotation preserves focus, so reveal the prompt again after its new layout is measured. + if (isPromptFocusedRef.current) { + scrollToMultilineInput(formRef, shouldUseScrollableLayout, promptTopOffsetRef.current); + } }} > { + isPromptFocusedRef.current = false; + }} /> {translate('workspace.rules.agentRules.disclaimer')} diff --git a/tests/unit/pages/settings/EditPromptPageTest.tsx b/tests/unit/pages/settings/EditPromptPageTest.tsx index 7f86f1726241..b127d6f166a0 100644 --- a/tests/unit/pages/settings/EditPromptPageTest.tsx +++ b/tests/unit/pages/settings/EditPromptPageTest.tsx @@ -1,4 +1,4 @@ -import {render} from '@testing-library/react-native'; +import {act, fireEvent, render} from '@testing-library/react-native'; import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode'; import useOnyx from '@hooks/useOnyx'; @@ -7,16 +7,20 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import EditPromptPage from '@pages/settings/Agents/Fields/DynamicEditPromptPage'; +import scrollToMultilineInput from '@pages/settings/Agents/scrollToMultilineInput'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import React from 'react'; -import {Platform} from 'react-native'; +import {Platform, View} from 'react-native'; import createMock from '../../../utils/createMock'; const mockPromptMount = jest.fn(); +const mockPromptProps = jest.fn void; onBlur?: () => void}]>(); + +jest.mock('@pages/settings/Agents/scrollToMultilineInput', () => jest.fn()); jest.mock('@hooks/useIsInLandscapeMode', () => jest.fn(() => false)); @@ -83,7 +87,8 @@ jest.mock('@components/Form/FormProvider', () => { jest.mock('@components/Form/InputWrapper', () => { const {useEffect} = jest.requireActual('react'); - function MockInputWrapper({inputID, defaultValue}: {inputID: string; defaultValue?: string}) { + function MockInputWrapper({inputID, defaultValue, ...props}: {inputID: string; defaultValue?: string; onFocus?: () => void; onBlur?: () => void}) { + mockPromptProps(props); useEffect(() => { mockPromptMount(); }, []); @@ -133,6 +138,41 @@ describe('EditPromptPage', () => { platform.restore(); }); + it('reveals the focused prompt after rotation changes its layout', () => { + // Given an iOS prompt that is being edited, focus will survive rotation. + const platform = jest.replaceProperty(Platform, 'OS', 'ios'); + const {UNSAFE_getAllByType: getAllByType} = render( + , + ); + act(() => mockPromptProps.mock.lastCall?.[0].onFocus?.()); + jest.mocked(scrollToMultilineInput).mockClear(); + const promptContainer = getAllByType(View).find((view) => { + const child: unknown = view.props.children; + return React.isValidElement<{inputID?: string}>(child) && child.props.inputID === 'prompt'; + }); + if (!promptContainer) { + throw new Error('Prompt container not found'); + } + + // When rotation moves the still-focused field, the updated offset must be used without another focus event. + fireEvent(promptContainer, 'layout', {nativeEvent: {layout: {x: 0, y: 120, width: 600, height: 76}}}); + + // Then the field is revealed using the newly measured position. + expect(scrollToMultilineInput).toHaveBeenLastCalledWith(expect.anything(), true, 120); + + // When editing ends, later layout changes should preserve the user's scroll position. + act(() => mockPromptProps.mock.lastCall?.[0].onBlur?.()); + jest.mocked(scrollToMultilineInput).mockClear(); + fireEvent(promptContainer, 'layout', {nativeEvent: {layout: {x: 0, y: 180, width: 300, height: 200}}}); + + // Then the page does not scroll to an unfocused field. + expect(scrollToMultilineInput).not.toHaveBeenCalled(); + platform.restore(); + }); + it('renders page title', () => { const {toJSON} = render( Date: Sun, 25 Oct 2026 22:18:14 +0800 Subject: [PATCH 5/5] fix: use shouldAutoGrowPromptInput --- src/pages/settings/Agents/DynamicAddAgentPage.tsx | 2 +- src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index 164a438b041f..e185049f7436 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -340,7 +340,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten defaultValue={defaultPrompt} multiline // Keep native markdown sizing enabled across rotation so the input retains focus while its height limit changes. - autoGrowHeight={Platform.OS !== 'web'} + autoGrowHeight={shouldAutoGrowPromptInput} maxAutoGrowHeight={isInLandscapeMode ? landscapePromptHeight : PROMPT_MAX_AUTO_GROW_HEIGHT} containerStyles={shouldAutoGrowPromptInput ? undefined : [styles.h100]} touchableInputWrapperStyle={shouldAutoGrowPromptInput ? undefined : [styles.flex1]} diff --git a/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx b/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx index cca0ae0e3e51..0ac450616c7d 100644 --- a/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx +++ b/src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx @@ -146,7 +146,7 @@ function DynamicEditPromptPage({route}: DynamicEditPromptPageProps) { defaultValue={Str.htmlDecode(agentPrompt?.prompt ?? '')} multiline // Keep native markdown sizing enabled across rotation so the input retains focus while its height limit changes. - autoGrowHeight={Platform.OS !== 'web'} + autoGrowHeight={shouldAutoGrowPromptInput} maxAutoGrowHeight={isInLandscapeMode ? landscapePromptHeight : PROMPT_MAX_AUTO_GROW_HEIGHT} containerStyles={shouldAutoGrowPromptInput ? undefined : [styles.h100]} touchableInputWrapperStyle={shouldAutoGrowPromptInput ? undefined : [styles.flex1]}