🐛(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.
This commit is contained in:
@@ -21,6 +21,7 @@ and this project adheres to
|
||||
### Fixed
|
||||
|
||||
- 🐛(nginx) fix / location to handle new static pages
|
||||
- 🐛(frontend) rerendering during resize window #1715
|
||||
|
||||
## [4.0.0] - 2025-12-01
|
||||
|
||||
|
||||
@@ -996,4 +996,44 @@ test.describe('Doc Editor', () => {
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe('test-pdf.pdf');
|
||||
});
|
||||
|
||||
test('it preserves text when switching between mobile and desktop views', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [docTitle] = await createDoc(
|
||||
page,
|
||||
'doc-viewport-test',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
const editor = await writeInEditor({
|
||||
page,
|
||||
text: 'Hello World - Desktop Text',
|
||||
});
|
||||
await expect(editor.getByText('Hello World - Desktop Text')).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Switch to mobile viewport
|
||||
await page.setViewportSize({ width: 500, height: 1200 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(editor.getByText('Hello World - Desktop Text')).toBeVisible();
|
||||
|
||||
await writeInEditor({
|
||||
page,
|
||||
text: 'Mobile Text',
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Switch back to desktop viewport
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(editor.getByText('Mobile Text')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useLeftPanelStore } from '../stores';
|
||||
export const LeftPanelHeaderButton = () => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
const { closePanel } = useLeftPanelStore();
|
||||
const { setIsSkeletonVisible } = useSkeletonStore();
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
|
||||
@@ -25,7 +25,7 @@ export const LeftPanelHeaderButton = () => {
|
||||
.then(() => {
|
||||
// The skeleton will be disabled by the [id] page once the data is loaded
|
||||
setIsNavigating(false);
|
||||
togglePanel();
|
||||
closePanel();
|
||||
})
|
||||
.catch(() => {
|
||||
// In case of navigation error, disable the skeleton
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
PanelResizeHandle,
|
||||
} from 'react-resizable-panels';
|
||||
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
// Convert a target pixel width to a percentage of the current viewport width.
|
||||
const pxToPercent = (px: number) => {
|
||||
return (px / window.innerWidth) * 100;
|
||||
@@ -24,18 +26,27 @@ export const ResizableLeftPanel = ({
|
||||
minPanelSizePx = 300,
|
||||
maxPanelSizePx = 450,
|
||||
}: ResizableLeftPanelProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const ref = useRef<ImperativePanelHandle>(null);
|
||||
const savedWidthPxRef = useRef<number>(minPanelSizePx);
|
||||
|
||||
const [panelSizePercent, setPanelSizePercent] = useState(() =>
|
||||
pxToPercent(minPanelSizePx),
|
||||
);
|
||||
|
||||
const minPanelSizePercent = pxToPercent(minPanelSizePx);
|
||||
const maxPanelSizePercent = Math.min(pxToPercent(maxPanelSizePx), 40);
|
||||
|
||||
const [panelSizePercent, setPanelSizePercent] = useState(() => {
|
||||
const initialSize = pxToPercent(minPanelSizePx);
|
||||
return Math.max(
|
||||
minPanelSizePercent,
|
||||
Math.min(initialSize, maxPanelSizePercent),
|
||||
);
|
||||
});
|
||||
|
||||
// Keep pixel width constant on window resize
|
||||
useEffect(() => {
|
||||
if (!isDesktop) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
const newPercent = pxToPercent(savedWidthPxRef.current);
|
||||
setPanelSizePercent(newPercent);
|
||||
@@ -48,7 +59,7 @@ export const ResizableLeftPanel = ({
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
}, [isDesktop]);
|
||||
|
||||
const handleResize = (sizePercent: number) => {
|
||||
const widthPx = (sizePercent / 100) * window.innerWidth;
|
||||
@@ -57,29 +68,29 @@ export const ResizableLeftPanel = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelGroup direction="horizontal">
|
||||
<Panel
|
||||
ref={ref}
|
||||
order={0}
|
||||
defaultSize={panelSizePercent}
|
||||
minSize={minPanelSizePercent}
|
||||
maxSize={maxPanelSizePercent}
|
||||
onResize={handleResize}
|
||||
>
|
||||
{leftPanel}
|
||||
</Panel>
|
||||
<PanelResizeHandle
|
||||
style={{
|
||||
borderRightWidth: '1px',
|
||||
borderRightStyle: 'solid',
|
||||
borderRightColor: 'var(--c--contextuals--border--surface--primary)',
|
||||
width: '1px',
|
||||
cursor: 'col-resize',
|
||||
}}
|
||||
/>
|
||||
<Panel order={1}>{children}</Panel>
|
||||
</PanelGroup>
|
||||
</>
|
||||
<PanelGroup direction="horizontal">
|
||||
<Panel
|
||||
ref={ref}
|
||||
order={0}
|
||||
defaultSize={isDesktop ? panelSizePercent : 0}
|
||||
minSize={isDesktop ? minPanelSizePercent : 0}
|
||||
maxSize={isDesktop ? maxPanelSizePercent : 0}
|
||||
onResize={handleResize}
|
||||
>
|
||||
{leftPanel}
|
||||
</Panel>
|
||||
<PanelResizeHandle
|
||||
style={{
|
||||
borderRightWidth: '1px',
|
||||
borderRightStyle: 'solid',
|
||||
borderRightColor: 'var(--c--contextuals--border--surface--primary)',
|
||||
width: '1px',
|
||||
cursor: 'col-resize',
|
||||
}}
|
||||
disabled={!isDesktop}
|
||||
/>
|
||||
|
||||
<Panel order={1}>{children}</Panel>
|
||||
</PanelGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { create } from 'zustand';
|
||||
interface LeftPanelState {
|
||||
isPanelOpen: boolean;
|
||||
togglePanel: (value?: boolean) => void;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
export const useLeftPanelStore = create<LeftPanelState>((set, get) => ({
|
||||
@@ -15,4 +16,7 @@ export const useLeftPanelStore = create<LeftPanelState>((set, get) => ({
|
||||
|
||||
set({ isPanelOpen: sanitizedValue });
|
||||
},
|
||||
closePanel: () => {
|
||||
set({ isPanelOpen: false });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -53,11 +53,51 @@ export function MainLayoutContent({
|
||||
enableResizablePanel = false,
|
||||
}: PropsWithChildren<MainLayoutContentProps>) {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
if (enableResizablePanel) {
|
||||
return (
|
||||
<ResizableLeftPanel leftPanel={<LeftPanel />}>
|
||||
<MainContent backgroundColor={backgroundColor}>{children}</MainContent>
|
||||
</ResizableLeftPanel>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDesktop) {
|
||||
return (
|
||||
<>
|
||||
<LeftPanel />
|
||||
<MainContent backgroundColor={backgroundColor}>{children}</MainContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
$css={css`
|
||||
width: 300px;
|
||||
border-right: 1px solid
|
||||
var(--c--contextuals--border--surface--primary);
|
||||
`}
|
||||
>
|
||||
<LeftPanel />
|
||||
</Box>
|
||||
<MainContent backgroundColor={backgroundColor}>{children}</MainContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MainContent = ({
|
||||
children,
|
||||
backgroundColor,
|
||||
}: PropsWithChildren<MainLayoutContentProps>) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor;
|
||||
|
||||
const mainContent = (
|
||||
return (
|
||||
<Box
|
||||
as="main"
|
||||
role="main"
|
||||
@@ -92,36 +132,4 @@ export function MainLayoutContent({
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (!isDesktop) {
|
||||
return (
|
||||
<>
|
||||
<LeftPanel />
|
||||
{mainContent}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (enableResizablePanel) {
|
||||
return (
|
||||
<ResizableLeftPanel leftPanel={<LeftPanel />}>
|
||||
{mainContent}
|
||||
</ResizableLeftPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
$css={css`
|
||||
width: 300px;
|
||||
border-right: 1px solid
|
||||
var(--c--contextuals--border--surface--primary);
|
||||
`}
|
||||
>
|
||||
<LeftPanel />
|
||||
</Box>
|
||||
{mainContent}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user