Skip to content

Commit af15e77

Browse files
committed
🐛(frontend) keep editor mounted when resize window
When resizing the window and crossing the desktop breakpoint, the editor was unmounted. It could lead to loss of data if there were unsaved changes, and tiptap crash if the toolbar was used while the editor was unmounted. It was caused by the ResizableLeftPanel component which was rerendering the editor. We now keep the editor mounted when resizing the window, by keeping the ResizableLeftPanel component rendered but setting its size to 0 and disabling the resize handle.
1 parent 99131dc commit af15e77

File tree

6 files changed

+129
-65
lines changed

6 files changed

+129
-65
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to
2121
### Fixed
2222

2323
- 🐛(nginx) fix / location to handle new static pages
24+
- 🐛(frontend) rerendering during resize window #1715
2425

2526
## [4.0.0] - 2025-12-01
2627

src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,4 +996,44 @@ test.describe('Doc Editor', () => {
996996
const download = await downloadPromise;
997997
expect(download.suggestedFilename()).toBe('test-pdf.pdf');
998998
});
999+
1000+
test('it preserves text when switching between mobile and desktop views', async ({
1001+
page,
1002+
browserName,
1003+
}) => {
1004+
const [docTitle] = await createDoc(
1005+
page,
1006+
'doc-viewport-test',
1007+
browserName,
1008+
1,
1009+
);
1010+
await verifyDocName(page, docTitle);
1011+
1012+
const editor = await writeInEditor({
1013+
page,
1014+
text: 'Hello World - Desktop Text',
1015+
});
1016+
await expect(editor.getByText('Hello World - Desktop Text')).toBeVisible();
1017+
1018+
await page.waitForTimeout(500);
1019+
1020+
// Switch to mobile viewport
1021+
await page.setViewportSize({ width: 500, height: 1200 });
1022+
await page.waitForTimeout(500);
1023+
1024+
await expect(editor.getByText('Hello World - Desktop Text')).toBeVisible();
1025+
1026+
await writeInEditor({
1027+
page,
1028+
text: 'Mobile Text',
1029+
});
1030+
1031+
await page.waitForTimeout(500);
1032+
1033+
// Switch back to desktop viewport
1034+
await page.setViewportSize({ width: 1280, height: 720 });
1035+
await page.waitForTimeout(500);
1036+
1037+
await expect(editor.getByText('Mobile Text')).toBeVisible();
1038+
});
9991039
});

src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeaderButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useLeftPanelStore } from '../stores';
1212
export const LeftPanelHeaderButton = () => {
1313
const router = useRouter();
1414
const { t } = useTranslation();
15-
const { togglePanel } = useLeftPanelStore();
15+
const { closePanel } = useLeftPanelStore();
1616
const { setIsSkeletonVisible } = useSkeletonStore();
1717
const [isNavigating, setIsNavigating] = useState(false);
1818

@@ -25,7 +25,7 @@ export const LeftPanelHeaderButton = () => {
2525
.then(() => {
2626
// The skeleton will be disabled by the [id] page once the data is loaded
2727
setIsNavigating(false);
28-
togglePanel();
28+
closePanel();
2929
})
3030
.catch(() => {
3131
// In case of navigation error, disable the skeleton

src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
PanelResizeHandle,
77
} from 'react-resizable-panels';
88

9+
import { useResponsiveStore } from '@/stores';
10+
911
// Convert a target pixel width to a percentage of the current viewport width.
1012
const pxToPercent = (px: number) => {
1113
return (px / window.innerWidth) * 100;
@@ -24,18 +26,27 @@ export const ResizableLeftPanel = ({
2426
minPanelSizePx = 300,
2527
maxPanelSizePx = 450,
2628
}: ResizableLeftPanelProps) => {
29+
const { isDesktop } = useResponsiveStore();
2730
const ref = useRef<ImperativePanelHandle>(null);
2831
const savedWidthPxRef = useRef<number>(minPanelSizePx);
2932

30-
const [panelSizePercent, setPanelSizePercent] = useState(() =>
31-
pxToPercent(minPanelSizePx),
32-
);
33-
3433
const minPanelSizePercent = pxToPercent(minPanelSizePx);
3534
const maxPanelSizePercent = Math.min(pxToPercent(maxPanelSizePx), 40);
3635

36+
const [panelSizePercent, setPanelSizePercent] = useState(() => {
37+
const initialSize = pxToPercent(minPanelSizePx);
38+
return Math.max(
39+
minPanelSizePercent,
40+
Math.min(initialSize, maxPanelSizePercent),
41+
);
42+
});
43+
3744
// Keep pixel width constant on window resize
3845
useEffect(() => {
46+
if (!isDesktop) {
47+
return;
48+
}
49+
3950
const handleResize = () => {
4051
const newPercent = pxToPercent(savedWidthPxRef.current);
4152
setPanelSizePercent(newPercent);
@@ -48,7 +59,7 @@ export const ResizableLeftPanel = ({
4859
return () => {
4960
window.removeEventListener('resize', handleResize);
5061
};
51-
}, []);
62+
}, [isDesktop]);
5263

5364
const handleResize = (sizePercent: number) => {
5465
const widthPx = (sizePercent / 100) * window.innerWidth;
@@ -57,29 +68,29 @@ export const ResizableLeftPanel = ({
5768
};
5869

5970
return (
60-
<>
61-
<PanelGroup direction="horizontal">
62-
<Panel
63-
ref={ref}
64-
order={0}
65-
defaultSize={panelSizePercent}
66-
minSize={minPanelSizePercent}
67-
maxSize={maxPanelSizePercent}
68-
onResize={handleResize}
69-
>
70-
{leftPanel}
71-
</Panel>
72-
<PanelResizeHandle
73-
style={{
74-
borderRightWidth: '1px',
75-
borderRightStyle: 'solid',
76-
borderRightColor: 'var(--c--contextuals--border--surface--primary)',
77-
width: '1px',
78-
cursor: 'col-resize',
79-
}}
80-
/>
81-
<Panel order={1}>{children}</Panel>
82-
</PanelGroup>
83-
</>
71+
<PanelGroup direction="horizontal">
72+
<Panel
73+
ref={ref}
74+
order={0}
75+
defaultSize={isDesktop ? panelSizePercent : 0}
76+
minSize={isDesktop ? minPanelSizePercent : 0}
77+
maxSize={isDesktop ? maxPanelSizePercent : 0}
78+
onResize={handleResize}
79+
>
80+
{leftPanel}
81+
</Panel>
82+
<PanelResizeHandle
83+
style={{
84+
borderRightWidth: '1px',
85+
borderRightStyle: 'solid',
86+
borderRightColor: 'var(--c--contextuals--border--surface--primary)',
87+
width: '1px',
88+
cursor: 'col-resize',
89+
}}
90+
disabled={!isDesktop}
91+
/>
92+
93+
<Panel order={1}>{children}</Panel>
94+
</PanelGroup>
8495
);
8596
};

src/frontend/apps/impress/src/features/left-panel/stores/useLeftPanelStore.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { create } from 'zustand';
33
interface LeftPanelState {
44
isPanelOpen: boolean;
55
togglePanel: (value?: boolean) => void;
6+
closePanel: () => void;
67
}
78

89
export const useLeftPanelStore = create<LeftPanelState>((set, get) => ({
@@ -15,4 +16,7 @@ export const useLeftPanelStore = create<LeftPanelState>((set, get) => ({
1516

1617
set({ isPanelOpen: sanitizedValue });
1718
},
19+
closePanel: () => {
20+
set({ isPanelOpen: false });
21+
},
1822
}));

src/frontend/apps/impress/src/layouts/MainLayout.tsx

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,51 @@ export function MainLayoutContent({
5353
enableResizablePanel = false,
5454
}: PropsWithChildren<MainLayoutContentProps>) {
5555
const { isDesktop } = useResponsiveStore();
56+
57+
if (enableResizablePanel) {
58+
return (
59+
<ResizableLeftPanel leftPanel={<LeftPanel />}>
60+
<MainContent backgroundColor={backgroundColor}>{children}</MainContent>
61+
</ResizableLeftPanel>
62+
);
63+
}
64+
65+
if (!isDesktop) {
66+
return (
67+
<>
68+
<LeftPanel />
69+
<MainContent backgroundColor={backgroundColor}>{children}</MainContent>
70+
</>
71+
);
72+
}
73+
74+
return (
75+
<>
76+
<Box
77+
$css={css`
78+
width: 300px;
79+
border-right: 1px solid
80+
var(--c--contextuals--border--surface--primary);
81+
`}
82+
>
83+
<LeftPanel />
84+
</Box>
85+
<MainContent backgroundColor={backgroundColor}>{children}</MainContent>
86+
</>
87+
);
88+
}
89+
90+
const MainContent = ({
91+
children,
92+
backgroundColor,
93+
}: PropsWithChildren<MainLayoutContentProps>) => {
94+
const { isDesktop } = useResponsiveStore();
95+
5696
const { t } = useTranslation();
5797
const { colorsTokens } = useCunninghamTheme();
5898
const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor;
5999

60-
const mainContent = (
100+
return (
61101
<Box
62102
as="main"
63103
role="main"
@@ -92,36 +132,4 @@ export function MainLayoutContent({
92132
{children}
93133
</Box>
94134
);
95-
96-
if (!isDesktop) {
97-
return (
98-
<>
99-
<LeftPanel />
100-
{mainContent}
101-
</>
102-
);
103-
}
104-
105-
if (enableResizablePanel) {
106-
return (
107-
<ResizableLeftPanel leftPanel={<LeftPanel />}>
108-
{mainContent}
109-
</ResizableLeftPanel>
110-
);
111-
}
112-
113-
return (
114-
<>
115-
<Box
116-
$css={css`
117-
width: 300px;
118-
border-right: 1px solid
119-
var(--c--contextuals--border--surface--primary);
120-
`}
121-
>
122-
<LeftPanel />
123-
</Box>
124-
{mainContent}
125-
</>
126-
);
127-
}
135+
};

0 commit comments

Comments
 (0)