PlTransfer
Two lists and the arrows between them: everything that could be chosen on one side, everything that has been on the other. Ticking is not choosing. The ticks say what the next press will move.
import { PlTransfer } from 'plass-ui';
<PlTransfer
items={columns}
value={value}
onValueChange={setValue}
sourceLabel="Available columns"
targetLabel="In the report"
/>;import 'package:plass_ui/plass_ui.dart';
PlTransfer(
items: columns,
value: value,
onValueChanged: (List<String> next) => setState(() => value = next),
sourceLabel: 'Available columns',
targetLabel: 'In the report',
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'glass' | 'ghost' | 'glass' | What the two panels are made of. The same shell a field wears: a list holds a value rather than being pressed |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The checkboxes, the arrows, the type scale and the padding, moving together |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| items * | readonly PlTransferItem[] | — | Everything that can be on either side, in the order both lists show it |
| value | readonly string[] | — | What is on the trailing side. Use with onValueChange for a controlled pair |
| defaultValue | readonly string[] | — | What starts there, for an uncontrolled one |
| onValueChange | (value: string[]) => void | — | Called when something moves across or back |
| sourceLabel | ReactNode | 'Available' | The heading over the leading list |
| targetLabel | ReactNode | 'Selected' | And over the trailing one |
| searchable | boolean | false | Puts a filter above each list |
| searchLabel | string | 'Search' | What that filter says while it is empty |
| emptyLabel | string | 'Nothing here' | What a list with nothing in it says |
| selectAllLabel | string | 'Select all' | What the tick in a list's heading is announced as |
| toTargetLabel | string | 'Move to selected' | What the outward arrow is announced as |
| toSourceLabel | string | 'Move to available' | What the returning arrow is announced as |
| height | number | string | 220 | How tall each list is. A number of pixels or any CSS length |
| disabled | boolean | false | Nothing can be ticked or moved |
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | PlassVariant | PlassVariant.glass | What the two panels are made of. The same shell a field wears: a list holds a value rather than being pressed |
| sizeshared | PlassSize | PlassSize.md | The checkboxes, the arrows, the type scale and the padding, moving together |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.standard | Padding only — never the height, never the type scale |
| items * | List<PlTransferItem> | — | Everything that can be on either side, in the order both lists show it |
| value | List<String>? | — | What is on the trailing side. Use with onValueChange for a controlled pair |
| defaultValue | List<String> | <String>[] | What starts there, for an uncontrolled one |
| onValueChanged | ValueChanged<List<String>>? | — | Called when something moves across or back |
| sourceLabel | String | 'Available' | The heading over the leading list |
| targetLabel | String | 'Selected' | And over the trailing one |
| searchable | bool | false | Puts a filter above each list |
| searchLabel | String | 'Search' | What that filter says while it is empty |
| emptyLabel | String | 'Nothing here' | What a list with nothing in it says |
| selectAllLabel | String | 'Select all' | What the tick in a list's heading is announced as |
| toTargetLabel | String | 'Move to selected' | What the outward arrow is announced as |
| toSourceLabel | String | 'Move to available' | What the returning arrow is announced as |
| height | double | 220 | How tall each list is. A number of pixels or any CSS length |
| disabled | bool | false | Nothing can be ticked or moved |
Every native <div> attribute passes straight through. color is excluded because it is a Plass prop here, defaultValue because the pair spells it as a list of values, and onChange because the pair reports through onValueChange.
PlTransferItem
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | string | — | What identifies it, and what value is a list of |
| label * | ReactNode | — | What the row says |
| disabled | boolean | false | In the list but not movable |
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | String | — | What identifies it, and what value is a list of |
| label * | String | — | What the row says. A String rather than a widget, so there is no row the filter cannot read |
| disabled | bool | false | In the list but not movable |
What the shared axes (variant size color density elevation) mean across the library is in prop conventions.
When to use it
For a choice that is long. A PlCombobox with forty chips in its field stops being readable, and a list of forty checkboxes gives no answer to "what did I actually pick".
Below about a dozen options, one of those two is the smaller component. This one costs a reader two lists and a pair of arrows; it is worth it exactly when the answer is itself worth reading back.
Ticking is not choosing
value is which side a row is on. The ticks are which rows the next press will move, and they are a separate piece of state on purpose: keeping them apart is what makes a press a deliberate act rather than a side effect of reading down a list.
Three things follow from it:
- The order of
itemsis the order both lists show, so a row does not move when it is sent across and back. - Moving drops the ticks on what moved and keeps the rest. A row that has arrived on the other side is not still waiting to be sent there.
- A row the filter was hiding was never part of that press.
Examples
searchable
Puts a filter above each list, and each one narrows only its own side.
The fold is case- and accent-insensitive, cafe finds Café, and it is the library's one answer to "this matches what I typed", shared by every filter in it. A reader who has learned what one search box in a product does has learned the right thing about the next one.
A label that is a node rather than a string has no text to match and stays. The alternative is a row that disappears from a filter it could never satisfy.
variant
The two panels wear the field shell rather than the sheet one, because a list that holds a value is a field-shaped thing: solid is the well, glass the hairline pane, ghost no surface until the pointer arrives. The arrows follow. They are glass beside a panel that has an edge, and ghost beside one that has none.
Neither panel is dyed. What they hold is somebody's data, and the family reaches the ticks, the arrows and the focus rings.
A row that cannot move
disabled on an item leaves it in the list and takes it out of every press, including the heading's select-all, which is why that tick reports three of four rather than four of four. An option that vanished when it could not be chosen is an option the reader will look for.
disabled on the pair stops everything at once.
Controlled
Pass value with onValueChange. The value is the list of values on the trailing side, in items order, not the objects: a transfer is a form control, and what it holds is what the form submits. Keep the identifiers here and look the objects up on the other side.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
label of ReactNode, a non-string one kept by the filter | label of String | The filter reads the label, and a row it cannot read is a row that disappears from a search it could never satisfy. Making it text is what keeps every row searchable by construction. |
| the fold strips case and combining marks | case only | Dart's core has no String.normalize, and this package has no dependencies. Pulling one in so a search box folds accents would put it in every consumer's binary for the sake of one comparison. |
height as a number or a CSS length | height as a double | There is no second unit to name. |
onValueChange | onValueChanged | Flutter's name. |
className, style, native attributes | — | There is no class list and no style attribute to pass through. |
Accessibility
- Every row is a real
PlCheckboxwith the row's label as its name, so a screen reader reads the list as the list of checkboxes it is. - The tick in each heading is a checkbox too, named by
selectAllLabel, and it reportsindeterminatewhen only some of its list is ticked. - The two arrows are
PlIconButtons with real names, and they are disabled until a press would actually move something. The state a reader can see, made available to one who cannot. - Each list has its own count (
ticked/total) beside its heading, which is the answer to "how much did I just select" without counting rows. - The lists scroll on their own and hold their scroll position, so moving a row does not throw a reader back to the top.