PlPanes
A set of regions with draggable handles between them. Sized in fractions, so the split survives the window being resized without a line of JavaScript running.
import { PlPane, PlPanes } from 'plass-ui';
<PlPanes>
<PlPane defaultSize="240px" minSize="180px" maxSize="50%">
{sidebar}
</PlPane>
<PlPane>{body}</PlPane>
</PlPanes>;import 'package:plass_ui/plass_ui.dart';
PlPanes(
panes: <PlPane>[
PlPane(
defaultSize: const PlPaneSize.pixels(240),
minSize: const PlPaneSize.pixels(180),
maxSize: const PlPaneSize.percent(50),
child: sidebar,
),
PlPane(child: body),
],
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| orientationshared | PlassResponsive<'horizontal' | 'vertical'> | 'horizontal' | Which way the panes run — side by side, or stacked |
| resizable | boolean | true | Whether the handles can be dragged. Off for a split that is a layout rather than a control |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | How thick a handle is, and how wide the target the pointer has to hit |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The family the handles light up in. There is no sheet, so it reaches the hairline, the tint and the ring |
| onResize | (sizes: number[]) => void | — | Fires with every pane's share, in percent, while a handle is dragged |
| onResizeEnd | (sizes: number[]) => void | — | Fires once, with the same shape, when the handle is let go — and on a key press |
| children | ReactNode | — | The PlPanes. Anything that is not one is still laid out, but has no size |
| Prop | Type | Default | Description |
|---|---|---|---|
| panes * | List<PlPane> | — | The regions, as a list of descriptions rather than children — the three sizing values are read by the split, not the pane |
| orientationshared | PlassResponsive<PlassOrientation> | PlassOrientation.horizontal | Which way the panes run — side by side, or stacked |
| resizable | bool | true | Whether the handles can be dragged. Off for a split that is a layout rather than a control |
| sizeshared | PlassSize | PlassSize.md | How thick a handle is, and how wide the target the pointer has to hit |
| colorshared | PlassColor | PlassColor.primary | The family the handles light up in. There is no sheet, so it reaches the hairline, the tint and the ring |
| onResize | ValueChanged<List<double>>? | — | Fires with every pane's share, in percent, while a handle is dragged |
| onResizeEnd | ValueChanged<List<double>>? | — | Fires once, with the same shape, when the handle is let go — and on a key press |
| label | String? | — | What a screen reader calls a handle, before the share it is at |
PlPane
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultSize | number | string | — | The share it starts with. A number is a percentage, a string an absolute length. Without one, panes split what is left evenly |
| minSize | number | string | 0 | How small it may be dragged |
| maxSize | number | string | — | How large it may be dragged. Unbounded when left out |
| children | ReactNode | — | What is inside the pane |
| Prop | Type | Default | Description |
|---|---|---|---|
| child * | Widget | — | What is inside the pane |
| defaultSize | PlPaneSize? | — | The share it starts with, as PlPaneSize.percent or PlPaneSize.pixels — Dart has no number | string union |
| minSize | PlPaneSize? | 0 | How small it may be dragged |
| maxSize | PlPaneSize? | — | How large it may be dragged. Unbounded when left out |
Every native <div> attribute passes through on both. color is excluded on the split because it is a Plass prop there.
A PlPane is a description rather than a widget, the idiom this package uses for an accordion's folds and a table's columns, and it is the same reason: the three sizing values are read by the split, so it has to be able to read them.
A pane carries no surface of its own, and neither does the split: this is layout, and the moment a pane drew a sheet it would stop being usable as the thing a PlCard, a PlTable or an editor is put inside. Put a PlCard in it when a surface is wanted. What the shared axes mean across the library is in prop conventions.
Examples
Measuring a split
The panes are sized in fractions, written out as flex-basis: calc((100% − gutters) × fraction).
That is the one decision the rest of the component follows from. A split described as a percentage survives the window being resized without anything running, so the component measures itself only twice: once on mount, to turn a '240px' default into a fraction, and once at the start of each drag, to know what a pixel of pointer movement is worth.
The measurement is a ResizeObserver rather than a single read, because a split inside a closed PlAccordion or an unselected PlTab is zero wide when it mounts, and dividing by that would put every pane at nothing.
This is the one place Flutter makes the same idea easier rather than harder. CSS measures itself, so the React build needs a ResizeObserver and a mounted split that is zero wide is a real hazard; a LayoutBuilder is handed the extent on every layout pass, so there is nothing to observe and nothing to re-measure. A split with no room yet simply lays its panes out evenly until there is some.
defaultSize, minSize and maxSize
A percentage is how a split is usually described and what keeps its meaning when the window changes size. An absolute length is what a sidebar with a minimum actually needs: "at least 200 pixels" does not survive being written down as a percentage of a width nobody knows yet.
A bare number is the percentage and a string is the length: '240px', '15rem', '20%'.Dart has no number | string union, so the two are two constructors: PlPaneSize.percent and PlPaneSize.pixels.Panes with no defaultSize split whatever is left over equally.
The three props are read by the split rather than used by the pane. A pane cannot know what "half" is; only the thing holding all of them can. Which is also why the direct children of a PlPanes have to be PlPanes. A pane wrapped in something else is a pane with no minimum.
orientation
horizontal puts the panes side by side with upright handles between them; vertical stacks them. Nesting one inside a pane of the other is how a three-region layout is built.
It is responsive, so a set can run one way on a phone and the other on a laptop. A server renders the xs entry and the browser corrects it on hydration.It is resolved against the window's width during build, so the first frame is already right. See breakpoints.
resizable
Turn it off for a split that is a layout rather than a control. The handles stay, they are still the line between two regions, but they stop taking the pointer and leave the tab order.
size and color
size is how thick a handle is. What is drawn is a hairline; what can be grabbed is the track around it. The same split a scrollbar makes between the two, and the reason a one-pixel line is not a target.
The split draws no sheet, so color reaches three things and stops: the handle's hairline when the pointer is on it, the tint under it, and the focus ring.
Accessibility
- Every handle is a
role="separator"carryingaria-valuenowas the share of the pane before it, so a screen reader can say where the boundary is rather than that there is one. - A handle is a tab stop while the split can be resized, and the arrow keys move it. It leaves the tab order entirely when
resizableis off, a control that cannot be operated should not be a stop on the way to one that can. - A key press is a whole gesture on its own, so
onResizeEndfires with it. There is no "let go" to wait for.
- The handle is focused by the browser on a press, not by the component. Focusing it by hand would put a keyboard focus ring on every handle somebody merely dragged.
- A drag takes the page's text selection away for its own length instead of calling
preventDefaulton the press, which is what would have stopped the focus above. The property is written as-webkit-user-selectthroughsetProperty, because WebKit implements only the prefixed name andstyle.userSelect = 'none'silently does nothing there. - A drag in flight is torn down if the split unmounts. The
pointerupthat would have ended it never arrives after a route change, and what is left behind is a page whose text can no longer be selected, on top of two listeners on a detached node.
- A handle is a slider rather than a separator, which is what it actually is to a screen reader here: Flutter's semantics tree has no separator role and no
valuenow, but it has a control with a value that can be turned up and down, andlabelnames it. - The arrow keys move it, and they follow the writing direction, so they run the other way under RTL, exactly as a drag does.
- None of the three drag hazards the other build has exists here. There is no document selection to take away, no browser focusing anything on a press, and a gesture recogniser is disposed with the widget that owns it.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
<PlPane> children | panes: List<PlPane> | The split reads the three sizing values off its members, and a Widget is opaque. |
number | string sizes | PlPaneSize.percent / .pixels | Dart has no union. The two constructors are the two halves of it. |
a ResizeObserver | a LayoutBuilder | CSS measures itself; Flutter hands the extent to whoever asks in the layout pass. |
role="separator", aria-valuenow | slider semantics | Flutter's tree has neither, and a control with a value is the honest description. |
className, style | — | There is no class list and no style attribute to pass through. |