React Accessible Shuttle
A tiny, zero dependency, shuttle (a.k.a list shuttle, dual listbox, etc.) implementation in React using hooks.
Background
Wait, What's a Shuttle?
A Shuttle, or list shuttle, is two containers that allow you to move items from a "source" to a "target". It's pretty rare in the wild, but great for business applications.
TODO: add animated gif
Why?
Other implementations are great but they generally force you to massage your data into a model and are restrictive. Hooks allow you to send data to react accessible shuttle so it can internally manipulate things without sacrificing your ability to control rendering of the shuttle items, controls, etc.
Usage
Installing
npm i react-accessible-shuttle # add peer dependencies npm i react react-dom
Basic Usage
react-accessible-shuttle
is a controlled component, but is flexible and adapts to your needs. Since you have complete control over the rendering process, you can render anything you want no matter how simple or complex your state data is. Here's an example using an array of strings:
import React from 'react';import ReactDOM from 'react-dom'; import Shuttle useShuttleState from 'react-accessible-shuttle';import 'react-accessible-shuttle/css/shuttle.css'; { const shuttle = ; return <Shuttle > <Shuttle.Container> source selected getItemProps source </Shuttle.Container> <Shuttle.Controls /> <Shuttle.Container> target selected getItemProps target </Shuttle.Container> </Shuttle> ;} ReactDOM;
react-accessible-shuttle
is powered by React hooks which allows the nitty-gritty internal details of the component to be handled for you, but while giving you the flexibility to control everything if you need it.
CDN
You can also use react-accessible-shuttle via CDN -- it even works with legacy browsers like IE 11 -- without transpiling.
<!-- Shuttle Dependency --> React Accessible Shuttle <!-- Peer Dependencies --> <!-- Shuttle Dependency --> <!-- Usage -->
If you're new to hooks, the example might seem verbose; however, we can easily abstract react-accessible-shuttle to take in a model and render on your behalf.
Without Hooks
Note: React 16.9 is a peer dependency of react-accessible-shuttle which means we can use hooks! However, if, for some reason, you find yourself stubbing 16.9 APIs so you can use newer stuff without upgrading, then you could possibly make things work 😲
Not on the hooks train yet? No worries. react-accessible-shuttle
depends in React 16.8.0+ so if you have that, then you can use without hooks (i.e. in a class
component) with a some extra effort 😃 (although we should really use hooks because they make our lives much easier).
Here are the things that need to be done:
- Pass
selected
anddisabled
to state (useShuttleState
generates these automatically for us) - Override
Shuttle.Controls
and manually constructsetState
calls. See ShuttleControls.tsx for code you can copy and paste or the example below.
If you're new to state reducing, this might seem mind-bending, but remember that we're using this.setState
to pass information to a function that returns our modified state.
import React from 'react';import Shuttle from 'react-accessible-shuttle'; Component state = source: 'a' 'b' 'c' target: 'd' 'e' 'f' // you MUST provide these when using // class components selections: source: target: disabled: source: target: ; this { this; }; this { this; }; this { this; }; this { this; }; { return <Shuttle = => <Shuttle.Container> /* ... */ </Shuttle.Container> <Shuttle.Controls> <> <button =>'\u00BB'</button> <button =>'\u203A'</button> <button =>'\u2039'</button> <button =>'\u00AB'</button> </> </Shuttle.Controls> <Shuttle.Container> /* ... */ </Shuttle.Container> </Shuttle> ; } ReactDOM;
How it Works
At a high level, react-accessible-shuttle uses state reducing to keep the code maintainable, while offering you the ability to override, extend, and enhance functionality without needing to create a PR for a new feature 😄
useShuttleState
is the entry point. This pure function takes in your data and outputs shuttleState
and setShuttleState
that are generated from React.useReducer
. These are passed down to Shuttle
and off we go.
State Reducer API
If you're new to hooks, but familiar with Redux, then the concepts are the same. react-accessible-shuttle exposes each reducer function as a separate module, modifying the state as needed. react-accessible-shuttle uses a composeReducers
redux-style function to combine all reducers. Like Redux, all reducers are executed when setShuttleState
is called.
If you're brand new to state reducing, fear not! Reducer functions are just pure functions that take in state
+ some arguments and return the modified/unmodified state. Our extra arguments tell us useful information like what kind of action we're getting, additional information that helps us modify the state, debugging info, etc. How does this help? Read on!
Passing Custom Reducers
useShuttleState
takes in four arguments:
- state
- initialSelections - optional
- disabled - optional
- reducers - optional
We can pass custom reducers to enhance functionality pretty easily. Suppose if a container has no selection, but when clicked we want to select the first (0-ith) item in the array. Using state reducing, we can achieve this easily without bloating the Shuttle API:
import React from 'react';import Shuttle useShuttleState from 'react-accessible-shuttle'; { const shuttle = ; return <Shuttle > <Shuttle.Container => source selected getItemProps source </Shuttle.Container> <Shuttle.Controls /> <Shuttle.Container => /* ... */ </Shuttle.Container> </Shuttle> ;} ReactDOM;
FAQ
When I filter items selections are not maintained
react-accessible-shuttle
depends on being able to resolve the index of the item based on the data-index
attribute on Shuttle.Items. If you're child render function in Shuttle.Container
looks like this:
<Shuttle.Container> source selected getItemProps source </Shuttle.Container>
Then you will have issues. selected
contains a set of integers. This mapping breaks when you use filter because data-index
changes. See the with-search example in codesandbox for an example.