-
Notifications
You must be signed in to change notification settings - Fork 4
Resizable #213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
andrre-ls
wants to merge
23
commits into
main
Choose a base branch
from
resizable
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Resizable #213
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
60a8175
resizable poc
andrre-ls 032cf0a
fix bugs
andrre-ls b85b329
fix bugs
andrre-ls 624c68c
update docs
andrre-ls b340003
min/max size example
andrre-ls 5507640
pr review
andrre-ls 0d56b23
improve docs
andrre-ls d17f859
add handle focus-visible
andrre-ls 063c21b
fix min size
andrre-ls 75d0312
improve resize handler for keydown events
andrre-ls 919aeff
cleanup copy
andrre-ls 5de9654
localStorage persist
andrre-ls 2dc3630
Merge branch 'main' into resizable
andrre-ls 133a547
Merge branch 'main' into resizable
andrre-ls a5ac3a4
fix pr review
andrre-ls 9ab6097
add comment
andrre-ls f7ea5cc
onResize prop poc
andrre-ls c391aea
extract handle to own component
andrre-ls b271e73
replace onResize setter with snap transformer
andrre-ls c656730
add imperative resize handle to Resizable.Panel
andrre-ls afd7c1b
refactor
andrre-ls 06661dc
refactor and fix resizable
andrre-ls a4960f2
refactor resizable panel registration
andrre-ls File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
30 changes: 30 additions & 0 deletions
30
src/foundations/ui/resizable/examples/resizable-collapsible.preview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { SidebarIcon } from "@phosphor-icons/react/dist/ssr"; | ||
| import { useState } from "react"; | ||
| import { Resizable } from "@/foundations/ui/resizable/resizable"; | ||
|
|
||
| export const meta = { layout: "centered" } as const; | ||
|
|
||
| export default function ResizableCollapsiblePreview() { | ||
| const [collapsed, setCollapsed] = useState(false); | ||
|
|
||
| return ( | ||
| <Resizable className="h-72 w-200 max-w-2xl rounded-xl border border-border"> | ||
| <Resizable.Panel | ||
| snap={(size) => (size < 100 ? 48 : size)} | ||
| onResize={(size) => setCollapsed(size < 100)} | ||
| > | ||
| <div className="grid h-full place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm"> | ||
| {collapsed ? <SidebarIcon /> : "Sidebar"} | ||
| </span> | ||
| </div> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel> | ||
| <div className="grid h-full grow place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Editor</span> | ||
| </div> | ||
| </Resizable.Panel> | ||
| </Resizable> | ||
| ); | ||
| } |
17 changes: 17 additions & 0 deletions
17
src/foundations/ui/resizable/examples/resizable-min-max.preview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { Resizable } from "@/foundations/ui/resizable/resizable"; | ||
|
|
||
| export const meta = { layout: "centered" } as const; | ||
|
|
||
| export default function ResizableMinMaxPreview() { | ||
| return ( | ||
| <Resizable className="h-72 w-200 max-w-2xl rounded-xl border border-border"> | ||
| <Resizable.Panel className="grid min-w-32 max-w-64 place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Sidebar</span> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Content</span> | ||
| </Resizable.Panel> | ||
| </Resizable> | ||
| ); | ||
| } | ||
25 changes: 25 additions & 0 deletions
25
src/foundations/ui/resizable/examples/resizable-nested.preview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { Resizable } from "@/foundations/ui/resizable/resizable"; | ||
|
|
||
| export const meta = { layout: "fullscreen", mode: "iframe" } as const; | ||
|
|
||
| export default function ResizableNestedPreview() { | ||
| return ( | ||
| <Resizable className="h-screen w-screen"> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Sidebar</span> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel> | ||
| <Resizable orientation="vertical" className="h-full"> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Main</span> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Console</span> | ||
| </Resizable.Panel> | ||
| </Resizable> | ||
| </Resizable.Panel> | ||
| </Resizable> | ||
| ); | ||
| } |
20 changes: 20 additions & 0 deletions
20
src/foundations/ui/resizable/examples/resizable-persist.preview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { Resizable } from "@/foundations/ui/resizable/resizable"; | ||
|
|
||
| export const meta = { layout: "centered" } as const; | ||
|
|
||
| export default function ResizablePersistPreview() { | ||
| return ( | ||
| <Resizable | ||
| persist="resizable-persist-preview" | ||
| className="h-72 w-200 max-w-2xl rounded-xl border border-border" | ||
|
andrre-ls marked this conversation as resolved.
|
||
| > | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Sidebar</span> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Editor</span> | ||
| </Resizable.Panel> | ||
| </Resizable> | ||
| ); | ||
| } | ||
38 changes: 38 additions & 0 deletions
38
src/foundations/ui/resizable/examples/resizable-set-size.preview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { useRef } from "react"; | ||
| import { Resizable, type ResizablePanelHandle } from "@/foundations/ui/resizable/resizable"; | ||
|
|
||
| export const meta = { layout: "centered" } as const; | ||
|
|
||
| export default function ResizableSetSizePreview() { | ||
| const sidebar = useRef<ResizablePanelHandle>(null); | ||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-3"> | ||
| <div className="flex gap-2"> | ||
| <button | ||
| type="button" | ||
| onClick={() => sidebar.current?.resize(120)} | ||
| className="rounded-md border border-border px-2 py-1 text-foreground-secondary text-sm hover:border-foreground/24" | ||
| > | ||
| Narrow | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => sidebar.current?.resize(400)} | ||
| className="rounded-md border border-border px-2 py-1 text-foreground-secondary text-sm hover:border-foreground/24" | ||
| > | ||
| Wide | ||
| </button> | ||
| </div> | ||
| <Resizable className="h-72 w-200 max-w-2xl rounded-xl border border-border"> | ||
| <Resizable.Panel ref={sidebar} className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Sidebar</span> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Editor</span> | ||
| </Resizable.Panel> | ||
| </Resizable> | ||
| </div> | ||
| ); | ||
| } |
17 changes: 17 additions & 0 deletions
17
src/foundations/ui/resizable/examples/resizable-vertical.preview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { Resizable } from "@/foundations/ui/resizable/resizable"; | ||
|
|
||
| export const meta = { layout: "fullscreen", mode: "iframe" } as const; | ||
|
|
||
| export default function ResizableVerticalPreview() { | ||
| return ( | ||
| <Resizable orientation="vertical" className="h-screen w-screen"> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Editor</span> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Terminal</span> | ||
| </Resizable.Panel> | ||
| </Resizable> | ||
| ); | ||
| } |
21 changes: 21 additions & 0 deletions
21
src/foundations/ui/resizable/examples/resizable.preview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { Resizable } from "@/foundations/ui/resizable/resizable"; | ||
|
|
||
| export const meta = { layout: "centered" } as const; | ||
|
|
||
| export default function ResizablePreview() { | ||
| return ( | ||
| <Resizable className="h-72 w-200 max-w-2xl rounded-xl border border-border"> | ||
|
andrre-ls marked this conversation as resolved.
|
||
| <Resizable.Panel className="grid w-full place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Sidebar</span> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel className="grid w-24 min-w-24 place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Editor</span> | ||
| </Resizable.Panel> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel className="grid place-items-center p-4"> | ||
| <span className="text-foreground-secondary text-sm">Preview</span> | ||
| </Resizable.Panel> | ||
| </Resizable> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| --- | ||
| title: Resizable | ||
| description: >- | ||
| A container whose panels can be resized by dragging the handle on the boundary between them | ||
| preview: resizable | ||
| files: | ||
| - src/foundations/ui/resizable/resizable.tsx | ||
| dependencies: | ||
| - name: Slot | ||
| href: /components/slot | ||
| - name: composeRefs | ||
| href: /utils/compose-refs | ||
| - name: math/clamp | ||
| href: /utils/math/#clamp | ||
|
|
||
| folder: UI | ||
| --- | ||
|
|
||
| ## Features | ||
|
|
||
| - **CSS-driven initial sizing**: Each panel's starting size is its computed CSS width (or height) at mount — set it with a `width` utility or any flex sizing class, no prop needed | ||
| - **Inferred bounds**: Minimum and maximum sizes are derived from the panel's CSS `min-width` / `min-height` and `max-width` / `max-height` — no props required | ||
| - **Percentage-based sizing**: Panels are sized as percentages of the container so the layout adapts as the container changes | ||
| - **Persistent sizes**: Pass a `persist` key to save and restore panel sizes across visits via `localStorage` | ||
| - **Keyboard support**: Handles are focusable and accept arrow key input to nudge the boundary | ||
|
|
||
| ## Anatomy | ||
|
|
||
| ```tsx | ||
| <Resizable orientation="horizontal"> | ||
| <Resizable.Panel /> | ||
| <Resizable.Handle /> | ||
| <Resizable.Panel /> | ||
| </Resizable> | ||
| ``` | ||
|
|
||
| Panels and handles must be **direct children** of `Resizable`. Place a `Resizable.Handle` between | ||
| any two panels you want to be resizable — omit it to create a fixed boundary. Panels without a | ||
| handle between them cannot be resized against each other. | ||
|
|
||
| ## API Reference | ||
|
|
||
| ### Resizable | ||
|
|
||
| Extends the `div` element. | ||
|
|
||
| <PropsTable | ||
| definition={{ | ||
| orientation: { | ||
| type: '"horizontal" | "vertical"', | ||
| default: '"horizontal"', | ||
| description: "Axis along which panels are laid out and resized.", | ||
| }, | ||
| persist: { | ||
| type: "string", | ||
| description: | ||
| "Unique key used to save panel sizes to localStorage and restore them on the next visit. Must be unique per layout — if the panel count changes, stored sizes for unrecognised panels are ignored and those panels fall back to their CSS size.", | ||
| }, | ||
| }} | ||
| /> | ||
|
|
||
| ### Resizable.Panel | ||
|
|
||
| Extends the `div` element. | ||
|
|
||
| Panels split the container’s space. On mount, each panel’s computed CSS size is read | ||
| and locked in as a percentage of the container — that percentage then changes as the | ||
| user drags. This means initial sizing is entirely CSS-driven: use a `width` class | ||
| (e.g. `w-64`, `w-1/3`) to set the starting size of a panel, and the rest of the | ||
| space is distributed to the other panels by flexbox before the lock-in happens. | ||
|
|
||
| <PropsTable | ||
| definition={{ | ||
| asChild: { | ||
| type: "boolean", | ||
| description: "Whether to merge props onto the child element.", | ||
| }, | ||
| snap: { | ||
| type: "(size: number) => number", | ||
| description: | ||
| "Transform the panel's size during a resize: receives the pointer-tracked size in pixels and returns the size the panel should take. Runs every frame — return a fixed size to snap or collapse, or the size unchanged for a no-op.", | ||
| }, | ||
| onResize: { | ||
| type: "(size: number) => void", | ||
| description: | ||
| "Read-only notification called during drag and keyboard resize with the panel's pointer-tracked size in pixels.", | ||
| }, | ||
| }} | ||
| /> | ||
|
|
||
| A panel's `ref` exposes an imperative `ResizablePanelHandle` (not the DOM node) so you can | ||
| set its size programmatically — a "reset" button, presets, or a button-driven collapse: | ||
|
|
||
| ```tsx | ||
| const sidebar = useRef<ResizablePanelHandle>(null); | ||
| // sidebar.current?.resize(300) — set the panel to 300px; the adjacent panel absorbs the difference | ||
| ``` | ||
|
|
||
| ### Resizable.Handle | ||
|
|
||
| Extends the `div` element. | ||
|
|
||
| A draggable separator placed explicitly between panels. It finds its adjacent panels by DOM | ||
| traversal, so it works correctly regardless of where it appears between two | ||
| `Resizable.Panel` elements. It is a `role="separator"` element, focusable by keyboard, and | ||
| accepts `className` and `ref` for customisation. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Horizontal | ||
|
|
||
| The default: panels sit in a row, dragging the handle left and right. | ||
|
|
||
| <Preview slug="resizable" /> | ||
|
|
||
| ### Min / max size | ||
|
|
||
| Apply `min-width` / `max-width` (or `min-height` / `max-height` for vertical) directly on a panel. The handle stops automatically at those boundaries. | ||
|
|
||
| <Preview slug="resizable-min-max" /> | ||
|
|
||
| ### Persistent sizes | ||
|
|
||
| Pass a unique `persist` key to save panel sizes to `localStorage`. Sizes are restored on the next visit — drag the handle and reload the page to see it in action. | ||
|
|
||
| <Preview slug="resizable-persist" /> | ||
|
|
||
| ### Vertical | ||
|
|
||
| Set `orientation="vertical"` to stack panels and drag the handle up and down. | ||
|
|
||
| <Preview slug="resizable-vertical" /> | ||
|
|
||
| ### Nested | ||
|
|
||
| Put a `Resizable` inside a `Resizable.Panel` to build grid-like layouts. | ||
|
|
||
| <Preview slug="resizable-nested" /> | ||
|
|
||
| ### Collapsible panel | ||
|
|
||
| Use `snap` to pin a panel to a collapsed size when it crosses a threshold — it returns the size the panel should take. Pair it with `onResize` to react to the change (here, swapping the label for an icon). | ||
|
|
||
| <Preview slug="resizable-collapsible" /> | ||
|
|
||
| ### Set size externally | ||
|
|
||
| Grab a panel's `ref` to get a `ResizablePanelHandle` and call `resize(size)` to set its size in pixels from outside a drag — for presets, a reset button, or programmatic collapse. | ||
|
|
||
| <Preview slug="resizable-set-size" /> | ||
|
|
||
| ## Accessibility | ||
|
|
||
| - Each handle is a `role="separator"` element with `tabIndex={0}`, so it is reachable by keyboard. | ||
| - `aria-orientation` reflects the axis of the separator — a horizontal layout uses a vertical separator, and vice versa. | ||
| - Arrow keys move the focused handle by 10px; the handle stops at the bounds derived from the adjacent panels' CSS constraints. | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Set a size on the container**: The container has no intrinsic size — give it a height (and width, if needed) so panels have space to fill. | ||
| 2. **Use CSS to constrain panels**: Set `min-width` / `min-height` on a panel to prevent it from being collapsed, and `max-width` / `max-height` to cap how large it can grow. The handle stops automatically at these boundaries. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.