Skip to content

Commit e9943dc

Browse files
committed
✨(frontend) hover effect on panel resize handle
In order to help user to understand it can resize a panel, we had a light hover effect (resize handle is enlarged). Make the class generic to allow user to reuse it in their app for other resize handler.
1 parent 6954bbb commit e9943dc

10 files changed

Lines changed: 288 additions & 8 deletions

File tree

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,15 @@ public/storybook/preview-files/pdf_with_js_link.pdf
5959

6060
# Site media
6161
/data/
62+
storybook-static/
6263

6364
# IDEs
6465
.idea/
6566
.vscode/
6667
*.iml
67-
.devcontainer
68+
.devcontainer
6869
node_modules
6970
.claude/
7071
CLAUDE.md
7172
openspec/
72-
skills-lock.json
73+
skills-lock.json

cunningham.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export const commonTokenOverrides = {
1515
"border-radius--active": "4px",
1616
"border-radius--focus": "4px",
1717
},
18+
"resize-handle": {
19+
// Color revealed on the interactive resize handle when hovered. Set to
20+
// "transparent" (app-wide or on a subtree) to disable the hover effect.
21+
"hover--color": "ref(contextuals.border.surface.primary)",
22+
},
1823
datagrid: {
1924
"header--color": "ref(contextuals.content.semantic.neutral.primary)",
2025
"header--size": "12px",

src/components/layout/MainLayout.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,9 @@ export const MainLayout = ({
140140
</Panel>
141141
{isDesktop && (
142142
<PanelResizeHandle
143-
className="border-clr-surface-primary"
144-
style={{
145-
borderRightWidth: "1px",
146-
borderRightStyle: "solid",
147-
}}
143+
className={clsx("c__resize-handle", {
144+
"c__resize-handle--interactive": enableResize,
145+
})}
148146
/>
149147
)}
150148
</>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Meta, Canvas } from "@storybook/blocks";
2+
import * as ResizeHandleStories from "./resize-handle.stories";
3+
4+
<Meta of={ResizeHandleStories} />
5+
6+
# ResizeHandle
7+
8+
`c__resize-handle` is a reusable style for a [`react-resizable-panels`](https://github.com/bvaughn/react-resizable-panels) resize handle (`PanelResizeHandle` in v2, `Separator` in v4).
9+
10+
The UI kit ships a CSS class rather than a React component on purpose: it keeps the style agnostic of the `react-resizable-panels` version the consumer installs, and lets each product wire the handle into its own panel layout.
11+
12+
## Usage
13+
14+
Apply the class directly to your handle:
15+
16+
```tsx
17+
// react-resizable-panels v2
18+
<PanelResizeHandle className="c__resize-handle c__resize-handle--interactive" />
19+
20+
// react-resizable-panels v4
21+
<Separator className="c__resize-handle c__resize-handle--interactive" />
22+
```
23+
24+
The styles are bundled in `@gouvfr-lasuite/ui-kit/style`, so make sure it is imported once in your app.
25+
26+
## Variants
27+
28+
### Interactive
29+
30+
The `c__resize-handle--interactive` modifier reveals a thicker line on hover plus an enlarged catch zone, signalling the handle can be dragged. Add it **only when the handle can actually resize** — otherwise it suggests an interaction that does not exist.
31+
32+
<Canvas of={ResizeHandleStories.Interactive} />
33+
34+
It works the same way for a stacked layout (drag up/down):
35+
36+
<Canvas of={ResizeHandleStories.InteractiveVertical} />
37+
38+
### Static
39+
40+
Without the modifier, the handle is just a thin separating line, with no hover affordance.
41+
42+
<Canvas of={ResizeHandleStories.Static} />
43+
44+
## Disabling the hover effect globally
45+
46+
The hover affordance is driven by the `--c--components--resize-handle--hover--color` design token. To turn it off across the whole app — without editing every `c__resize-handle--interactive` usage — set it to `transparent` on any ancestor scope:
47+
48+
```css
49+
:root {
50+
--c--components--resize-handle--hover--color: transparent;
51+
}
52+
```
53+
54+
You can also scope it to a subtree by setting the property on a wrapping element instead of `:root`. The keyboard focus indicator is intentionally **not** affected, so disabling the hover effect never removes the focus-visible affordance.
55+
56+
## Orientation
57+
58+
You never set the orientation yourself: it is derived from whichever attribute the installed version exposes, so the same class works across versions.
59+
60+
| Version | Attribute | Vertical line | Horizontal line |
61+
| --- | --- | --- | --- |
62+
| v2 | `data-panel-group-direction` | `horizontal` | `vertical` |
63+
| v4 | `aria-orientation` | `vertical` | `horizontal` |
64+
65+
- **Vertical line** → panels side by side → drag left/right.
66+
- **Horizontal line** → panels stacked → drag up/down.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Reusable styling for a `react-resizable-panels` resize handle
2+
// (`PanelResizeHandle` in v2, `Separator` in v4).
3+
//
4+
// By default it renders as a thin static line. Add the `--interactive`
5+
// modifier to reveal a hover affordance (a thicker line on hover plus an
6+
// enlarged catch zone) that makes it explicit the handle can be dragged.
7+
//
8+
// Usage (consumer side):
9+
// <PanelResizeHandle className="c__resize-handle c__resize-handle--interactive" />
10+
//
11+
// The hover effect can be disabled globally (without editing each usage) by
12+
// overriding the `--c--components--resize-handle--hover--color` token to
13+
// `transparent` on an ancestor scope, e.g. `:root` for the whole app. The
14+
// keyboard focus indicator stays visible.
15+
//
16+
// Orientation is derived from whichever attribute the installed version of
17+
// react-resizable-panels exposes, so the same class works across versions:
18+
// v2 → `data-panel-group-direction` (direction of the panel group)
19+
// v4 → `aria-orientation` (orientation of the handle line)
20+
.c__resize-handle {
21+
background-color: var(--c--contextuals--border--surface--primary);
22+
position: relative;
23+
flex-shrink: 0;
24+
25+
// Vertical line.
26+
&[data-panel-group-direction="horizontal"],
27+
&[aria-orientation="vertical"] {
28+
width: 1px;
29+
}
30+
31+
// Horizontal line.
32+
&[data-panel-group-direction="vertical"],
33+
&[aria-orientation="horizontal"] {
34+
height: 1px;
35+
}
36+
37+
&--interactive {
38+
&:before {
39+
content: "";
40+
background: transparent;
41+
position: absolute;
42+
transition: background-color 200ms var(--c--globals--transitions--ease-out);
43+
z-index: 10;
44+
}
45+
46+
// Expand the hover catch zone beyond the visible line.
47+
&:after {
48+
content: "";
49+
position: absolute;
50+
}
51+
52+
&:hover:before {
53+
background: var(--c--components--resize-handle--hover--color);
54+
transition-timing-function: var(--c--globals--transitions--ease-in);
55+
transition-delay: 100ms;
56+
}
57+
58+
&:focus-visible {
59+
outline: none;
60+
61+
&:before {
62+
background: var(--c--contextuals--border--semantic--brand--secondary);
63+
transition-timing-function: var(--c--globals--transitions--ease-in);
64+
transition-delay: 100ms;
65+
}
66+
}
67+
68+
// Vertical line.
69+
&[data-panel-group-direction="horizontal"],
70+
&[aria-orientation="vertical"] {
71+
&:before {
72+
width: 3px;
73+
left: -1px;
74+
top: 0;
75+
bottom: 0;
76+
}
77+
78+
&:after {
79+
width: 11px;
80+
left: -5px;
81+
top: 0;
82+
bottom: 0;
83+
}
84+
}
85+
86+
// Horizontal line.
87+
&[data-panel-group-direction="vertical"],
88+
&[aria-orientation="horizontal"] {
89+
&:before {
90+
height: 3px;
91+
top: -1px;
92+
left: 0;
93+
right: 0;
94+
}
95+
96+
&:after {
97+
height: 11px;
98+
top: -5px;
99+
left: 0;
100+
right: 0;
101+
}
102+
}
103+
104+
@media (prefers-reduced-motion: reduce) {
105+
&:before {
106+
transition: none;
107+
}
108+
}
109+
}
110+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Meta, StoryObj } from "@storybook/react";
2+
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
3+
4+
// Reusable styling for a `react-resizable-panels` resize handle.
5+
// See ResizeHandle.mdx for the full documentation.
6+
const meta: Meta = {
7+
title: "Components/ResizeHandle",
8+
};
9+
10+
export default meta;
11+
12+
const panelStyle: React.CSSProperties = {
13+
display: "flex",
14+
alignItems: "center",
15+
justifyContent: "center",
16+
background: "var(--c--contextuals--background--surface--secondary)",
17+
color: "var(--c--contextuals--content--semantic--neutral--primary)",
18+
};
19+
20+
const frameStyle: React.CSSProperties = {
21+
height: 240,
22+
width: 480,
23+
border: "1px solid var(--c--contextuals--border--surface--primary)",
24+
};
25+
26+
// --- Vertical handle: panels laid out side by side (drag left/right) ---
27+
export const Interactive: StoryObj = {
28+
render: () => (
29+
<div style={frameStyle}>
30+
<PanelGroup direction="horizontal">
31+
<Panel defaultSize={50} minSize={20}>
32+
<div style={panelStyle}>Left</div>
33+
</Panel>
34+
<PanelResizeHandle className="c__resize-handle c__resize-handle--interactive" />
35+
<Panel minSize={20}>
36+
<div style={panelStyle}>Right</div>
37+
</Panel>
38+
</PanelGroup>
39+
</div>
40+
),
41+
};
42+
43+
// --- Horizontal handle: stacked panels (drag up/down) ---
44+
export const InteractiveVertical: StoryObj = {
45+
render: () => (
46+
<div style={frameStyle}>
47+
<PanelGroup direction="vertical">
48+
<Panel defaultSize={50} minSize={20}>
49+
<div style={panelStyle}>Top</div>
50+
</Panel>
51+
<PanelResizeHandle className="c__resize-handle c__resize-handle--interactive" />
52+
<Panel minSize={20}>
53+
<div style={panelStyle}>Bottom</div>
54+
</Panel>
55+
</PanelGroup>
56+
</div>
57+
),
58+
};
59+
60+
// --- Static line: no hover affordance (resizing disabled) ---
61+
export const Static: StoryObj = {
62+
render: () => (
63+
<div style={frameStyle}>
64+
<PanelGroup direction="horizontal">
65+
<Panel defaultSize={50}>
66+
<div style={panelStyle}>Left</div>
67+
</Panel>
68+
<PanelResizeHandle className="c__resize-handle" disabled />
69+
<Panel>
70+
<div style={panelStyle}>Right</div>
71+
</Panel>
72+
</PanelGroup>
73+
</div>
74+
),
75+
};

src/cunningham-tokens-sass.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,9 @@ $themes: (
697697
'border-radius--active': 4px,
698698
'border-radius--focus': 4px
699699
),
700+
'resize-handle': (
701+
'hover--color': #E2E2EA
702+
),
700703
'datagrid': (
701704
'header--color': #25252F,
702705
'header--size': 12px,
@@ -1456,6 +1459,9 @@ $themes: (
14561459
'border-radius--active': 4px,
14571460
'border-radius--focus': 4px
14581461
),
1462+
'resize-handle': (
1463+
'hover--color': #E2E2EA
1464+
),
14591465
'datagrid': (
14601466
'header--color': #25252F,
14611467
'header--size': 12px,
@@ -2188,6 +2194,9 @@ $themes: (
21882194
'border-radius--active': 4px,
21892195
'border-radius--focus': 4px
21902196
),
2197+
'resize-handle': (
2198+
'hover--color': #E2E2EA
2199+
),
21912200
'datagrid': (
21922201
'header--color': #25252F,
21932202
'header--size': 12px,
@@ -2920,6 +2929,9 @@ $themes: (
29202929
'border-radius--active': 4px,
29212930
'border-radius--focus': 4px
29222931
),
2932+
'resize-handle': (
2933+
'hover--color': #E2E2EA
2934+
),
29232935
'datagrid': (
29242936
'header--color': #25252F,
29252937
'header--size': 12px,
@@ -3655,6 +3667,9 @@ $themes: (
36553667
'border-radius--active': 4px,
36563668
'border-radius--focus': 4px
36573669
),
3670+
'resize-handle': (
3671+
'hover--color': #E2E2EA
3672+
),
36583673
'datagrid': (
36593674
'header--color': #25252F,
36603675
'header--size': 12px,
@@ -4390,6 +4405,9 @@ $themes: (
43904405
'border-radius--active': 4px,
43914406
'border-radius--focus': 4px
43924407
),
4408+
'resize-handle': (
4409+
'hover--color': #E2E2EA
4410+
),
43934411
'datagrid': (
43944412
'header--color': #25252F,
43954413
'header--size': 12px,

0 commit comments

Comments
 (0)