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
5 changes: 5 additions & 0 deletions .changeset/pretty-jokes-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gouvfr-lasuite/ui-components": minor
---

Upgrade react-resizable-panels to v4
2 changes: 1 addition & 1 deletion .github/workflows/front-dependencies-installation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
inputs:
node_version:
required: false
default: "20.x"
default: "24.x"
type: string

jobs:
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: "20.x"
node_version: "24.x"

lint-git:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -48,7 +48,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20.x"
node-version: "24.x"
- name: Restore the frontend cache
uses: actions/cache@v5
with:
Expand All @@ -71,7 +71,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20.x"
node-version: "24.x"
- name: Restore the frontend cache
uses: actions/cache@v5
with:
Expand All @@ -92,7 +92,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20.x"
node-version: "24.x"
- name: Restore the frontend cache
uses: actions/cache@v5
with:
Expand All @@ -113,7 +113,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20.x"
node-version: "24.x"
- name: Restore the frontend cache
uses: actions/cache@v5
with:
Expand All @@ -134,7 +134,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20.x"
node-version: "24.x"
- name: Restore the frontend cache
uses: actions/cache@v5
with:
Expand Down Expand Up @@ -189,7 +189,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20.x"
node-version: "24.x"
- name: Restore the frontend cache
uses: actions/cache@v5
with:
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.19.0
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-alpine
FROM node:24-alpine

WORKDIR /app

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ does not modify the target project's `package.json`, lockfile or `node_modules`.

## Development

Node 20 and Yarn 1.22.22 are required.
Node 24 (the version pinned in `.nvmrc`) and Yarn 1.22.22 are required.

```bash
yarn install --frozen-lockfile
Expand Down
3 changes: 2 additions & 1 deletion docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ the documented `npx @gouvfr-lasuite/ui-codemod@<version>` commands.

## Prerequisites

- Node 20 and Yarn 1.22.22;
- Node 24 (the version pinned in `.nvmrc`) and Yarn 1.22.22 — `changeset` needs
a Node release that can `require()` an ESM module;
- a clean checkout with an up-to-date `origin/main`;
- an npm account allowed to publish the `@gouvfr-lasuite` scope;
- npm two-factor authentication in `auth-and-writes` mode;
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@
"scalingo-postbuild": "yarn build && yarn build-storybook"
},
"devDependencies": {
"@changesets/cli": "2.29.2",
"@changesets/cli": "2.31.1",
"prettier": "3.5.3",
"turbo": "2.5.2"
},
"engines": {
"node": ">=20.0.0"
"node": ">=24.0.0"
},
"packageManager": "yarn@1.22.22",
"volta": {
"node": "20.16.0",
"node": "24.19.0",
"yarn": "1.22.22"
}
}
2 changes: 1 addition & 1 deletion packages/ui-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"react-aria": "3.47.0",
"react-aria-components": "1.16.0",
"react-modal": "3.16.3",
"react-resizable-panels": "2.1.7",
"react-resizable-panels": "4.12.2",
"react-stately": "3.45.0",
"react-virtualized": "9.22.6",
"react-pdf": "10.1.0",
Expand Down
99 changes: 60 additions & 39 deletions packages/ui-components/src/components/layout/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
import { PropsWithChildren, useEffect, useRef, useState } from "react";
import {
PropsWithChildren,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Header } from "./header/Header";
import { LeftPanel } from "./left-panel/LeftPanel";
import clsx from "clsx";

// Left panel constraints, in pixels: react-resizable-panels v4 reads bare
// numbers as pixels, so they can be handed to `Panel` as-is.
const MIN_LEFT_PANEL_PX = 300;
const MAX_LEFT_PANEL_PX = 450;
const MAX_LEFT_PANEL_PERCENT = 40;
// ...except the upper bound, which is also capped to a share of the viewport so
// the panel never dominates a narrow desktop window. This is the only
// constraint that depends on the window size, hence the only one recomputed on
// resize.
const MAX_LEFT_PANEL_VIEWPORT_RATIO = 0.4;

const cappedMaxLeftPanelPx = () =>
Math.min(MAX_LEFT_PANEL_PX, window.innerWidth * MAX_LEFT_PANEL_VIEWPORT_RATIO);

// Panels are identified explicitly so the persisted layout survives the left
// panel being conditionally mounted (react-resizable-panels v4 keys stored
// layouts by panel id, where v2 used the `order` prop).
const LEFT_PANEL_ID = "left";
const CENTER_PANEL_ID = "center";

import { useResponsive } from ":/hooks/useResponsive";
import {
ImperativePanelHandle,
Group,
Panel,
PanelGroup,
PanelResizeHandle,
Separator,
useDefaultLayout,
} from "react-resizable-panels";
import { DropdownMenuOption } from "../dropdown-menu/types";
import { RightPanel } from "./right-panel/RightPanel";
Expand Down Expand Up @@ -56,7 +77,6 @@ export const MainLayout = ({

const { isDesktop } = useResponsive();

const ref = useRef<ImperativePanelHandle>(null);
const [isResizing, setIsResizing] = useState(false);
const resizeTimeoutRef = useRef<number | undefined>(undefined);

Expand All @@ -68,28 +88,37 @@ export const MainLayout = ({
const mountLeftPanel = isDesktop ? !hideLeftPanelOnDesktop : true;
const showLeftPanel = isDesktop ? !hideLeftPanelOnDesktop : isLeftPanelOpen;

const [minPanelSize, setMinPanelSize] = useState(
calculateDefaultSize(MIN_LEFT_PANEL_PX, isDesktop)
const [maxResizablePanelSize, setMaxResizablePanelSize] = useState(
cappedMaxLeftPanelPx
);
const [maxPanelSize, setMaxPanelSize] = useState(
calculateDefaultSize(MAX_LEFT_PANEL_PX, isDesktop)

// The left panel is collapsed to nothing on mobile, where it is displayed as
// an overlay rather than as a panel of the group.
const minPanelSize = isDesktop ? MIN_LEFT_PANEL_PX : 0;
const maxPanelSize =
isDesktop && enableResize ? maxResizablePanelSize : minPanelSize;

const panelIds = useMemo(
() =>
mountLeftPanel ? [LEFT_PANEL_ID, CENTER_PANEL_ID] : [CENTER_PANEL_ID],
[mountLeftPanel]
);

// Replaces the `autoSaveId` prop dropped in react-resizable-panels v4.
// `panelIds` keeps a separate stored layout per panel combination, so the
// width saved with the left panel mounted is not applied without it.
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
id: "persistance",
panelIds,
});

const onTogglePanel = () => {
setIsLeftPanelOpen(!isLeftPanelOpen);
};

// Combined resize listener: disable transitions during window resize + update panel sizes
// Combined resize listener: disable transitions during window resize + keep
// the viewport-capped upper bound in sync.
useEffect(() => {
const updatePanelSizes = () => {
const min = Math.round(calculateDefaultSize(MIN_LEFT_PANEL_PX, isDesktop));
const max = Math.round(
Math.min(calculateDefaultSize(MAX_LEFT_PANEL_PX, isDesktop), MAX_LEFT_PANEL_PERCENT)
);
setMinPanelSize(isDesktop ? min : 0);
setMaxPanelSize(enableResize ? max : min);
};

const handleResize = () => {
setIsResizing(true);
if (resizeTimeoutRef.current) {
Expand All @@ -98,10 +127,9 @@ export const MainLayout = ({
resizeTimeoutRef.current = window.setTimeout(() => {
setIsResizing(false);
}, 150);
updatePanelSizes();
setMaxResizablePanelSize(cappedMaxLeftPanelPx());
};

updatePanelSizes();
window.addEventListener("resize", handleResize);

return () => {
Expand All @@ -110,7 +138,7 @@ export const MainLayout = ({
clearTimeout(resizeTimeoutRef.current);
}
};
}, [isDesktop, enableResize]);
}, []);

return (
<div className={clsx("c__main-layout", { resizing: isResizing })}>
Expand All @@ -124,12 +152,15 @@ export const MainLayout = ({
/>
</div>
<div className="c__main-layout__content">
<PanelGroup autoSaveId={"persistance"} direction="horizontal">
<Group
orientation="horizontal"
defaultLayout={defaultLayout}
onLayoutChanged={onLayoutChanged}
>
{mountLeftPanel && (
<>
<Panel
ref={ref}
order={0}
id={LEFT_PANEL_ID}
defaultSize={minPanelSize}
minSize={minPanelSize}
maxSize={maxPanelSize}
Expand All @@ -139,15 +170,15 @@ export const MainLayout = ({
</LeftPanel>
</Panel>
{isDesktop && (
<PanelResizeHandle
<Separator
className={clsx("c__resize-handle", {
"c__resize-handle--interactive": enableResize,
})}
/>
)}
</>
)}
<Panel order={1}>
<Panel id={CENTER_PANEL_ID}>
<div className="c__main-layout__content__center">
<div className="c__main-layout__content__center__children">
{children}
Expand All @@ -158,18 +189,8 @@ export const MainLayout = ({
</RightPanel>
</div>
</Panel>
</PanelGroup>
</Group>
</div>
</div>
);
};

const calculateDefaultSize = (targetWidth: number, isDesktop: boolean) => {
if (!isDesktop) {
return 0;
}

const windowWidth = window.innerWidth;

return (targetWidth / windowWidth) * 100;
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meta, StoryObj } from "@storybook/react";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { Group, Panel, Separator } from "react-resizable-panels";

// Reusable styling for a `react-resizable-panels` resize handle.
// Reusable styling for a `react-resizable-panels` separator.
// See ResizeHandle.mdx for the full documentation.
const meta: Meta = {
title: "Components/ResizeHandle",
Expand All @@ -27,15 +27,15 @@ const frameStyle: React.CSSProperties = {
export const Interactive: StoryObj = {
render: () => (
<div style={frameStyle}>
<PanelGroup direction="horizontal">
<Panel defaultSize={50} minSize={20}>
<Group orientation="horizontal">
<Panel defaultSize="50%" minSize="20%">
<div style={panelStyle}>Left</div>
</Panel>
<PanelResizeHandle className="c__resize-handle c__resize-handle--interactive" />
<Panel minSize={20}>
<Separator className="c__resize-handle c__resize-handle--interactive" />
<Panel minSize="20%">
<div style={panelStyle}>Right</div>
</Panel>
</PanelGroup>
</Group>
</div>
),
};
Expand All @@ -44,15 +44,15 @@ export const Interactive: StoryObj = {
export const InteractiveVertical: StoryObj = {
render: () => (
<div style={frameStyle}>
<PanelGroup direction="vertical">
<Panel defaultSize={50} minSize={20}>
<Group orientation="vertical">
<Panel defaultSize="50%" minSize="20%">
<div style={panelStyle}>Top</div>
</Panel>
<PanelResizeHandle className="c__resize-handle c__resize-handle--interactive" />
<Panel minSize={20}>
<Separator className="c__resize-handle c__resize-handle--interactive" />
<Panel minSize="20%">
<div style={panelStyle}>Bottom</div>
</Panel>
</PanelGroup>
</Group>
</div>
),
};
Expand All @@ -61,15 +61,15 @@ export const InteractiveVertical: StoryObj = {
export const Static: StoryObj = {
render: () => (
<div style={frameStyle}>
<PanelGroup direction="horizontal">
<Panel defaultSize={50}>
<Group orientation="horizontal">
<Panel defaultSize="50%">
<div style={panelStyle}>Left</div>
</Panel>
<PanelResizeHandle className="c__resize-handle" disabled />
<Separator className="c__resize-handle" disabled />
<Panel>
<div style={panelStyle}>Right</div>
</Panel>
</PanelGroup>
</Group>
</div>
),
};
2 changes: 1 addition & 1 deletion packages/ui-tokens/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"node": ">=18.0.0"
},
"volta": {
"node": "20.16.0",
"node": "24.19.0",
"yarn": "1.22.22"
}
}
Loading
Loading