🐛(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:
@@ -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