npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-accessible-shuttle

v0.5.0

Published

A tiny, zero dependency, shuttle (a.k.a list shuttle, dual listbox, etc.) implementation in React using hooks.

Downloads

1,113

Readme

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';

function App() {
    const shuttle = useShuttleState({
        source: ['a', 'b', 'c'],
        target: ['d', 'e', 'f'],
    });

    return (
        <Shuttle {...shuttle}>
            <Shuttle.Container>
                {({ source, selected }, getItemProps) =>
                    source.map((item, index) => (
                        <Shuttle.Item
                            {...getItemProps(index)}
                            key={item}
                            value={item}
                            selected={selected.source.has(index)}>
                            {item}
                        </Shuttle.Item>
                    ))
                }
            </Shuttle.Container>
            <Shuttle.Controls />
            <Shuttle.Container>
                {({ target, selected }, getItemProps) =>
                    target.map((item, index) => (
                        <Shuttle.Item
                            {...getItemProps(index)}
                            key={item}
                            value={item}
                            selected={selected.target.has(index)}>
                            {item}
                        </Shuttle.Item>
                    ))
                }
            </Shuttle.Container>
        </Shuttle>
    );
}

ReactDOM.render(<App />, document.getElementById('app'));

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.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <!-- Shuttle Dependency -->
        <link
            rel="stylesheet"
            href="https://unpkg.com/react-accessible-shuttle/css/shuttle.css"
        />
        <title>React Accessible Shuttle</title>
    </head>
    <body>
        <div id="root"></div>

        <!-- Peer Dependencies -->
        <script src="https://unpkg.com/react/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

        <!-- Shuttle Dependency -->
        <script src="https://unpkg.com/react-accessible-shuttle/dist-browser/index.js"></script>

        <!-- Usage -->
        <script>
            function App() {
                const shuttle = ReactShuttle.useShuttleState({
                    source: [1, 2, 3],
                    target: [4, 5, 6],
                });

                return React.createElement(ReactShuttle, shuttle, [
                    React.createElement(ReactShuttle.Container, null, function(
                        state,
                        getItemProps
                    ) {
                        return state.source.map(function(item, index) {
                            const props = {
                                key: index,
                                value: item,
                            };

                            Object.assign(props, getItemProps(index));

                            return React.createElement(ReactShuttle.Item, props, item);
                        });
                    }),
                    React.createElement(ReactShuttle.Controls, null, null),
                    React.createElement(ReactShuttle.Container, null, function (
                        state,
                        getItemProps
                    ) {
                        return state.target.map(function (item, index) {
                            const props = {
                                key: index,
                                value: item,
                            };

                            Object.assign(props, getItemProps(index));

                            return React.createElement(ReactShuttle.Item, props, item);
                        });
                    }),
                ]);
            }

            ReactDOM.render(React.createElement(App), document.getElementById('root'));
        </script>
    </body>
</html>

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 :astonished:

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 :smiley: (although we should really use hooks because they make our lives much easier).

Here are the things that need to be done:

  • Pass selected and disabled to state (useShuttleState generates these automatically for us)
  • Override Shuttle.Controls and manually construct setState 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';

class App extends React.Component {
    state = {
        source: ['a', 'b', 'c'],
        target: ['d', 'e', 'f'],

        // you MUST provide these when using
        // class components
        selections: {
            source: new Set(),
            target: new Set(),
        },
        disabled: {
            source: new Set(),
            target: new Set(),
        },
    };

    this.moveAllFromSource = () => {
        this.setState({
            action: 'MOVE_ALL',
            from: 'source',
            to: 'target',
        });
    };

    this.moveSelectedFromSource = () => {
        this.setState({
            action: 'MOVE_SELECTIONS',
            from: 'source',
            to: 'target',
        });
    };

    this.moveSelectedFromTarget = () => {
        this.setState({
            action: 'MOVE_SELECTIONS',
            from: 'target',
            to: 'source',
        });
    };

    this.moveAllFromTarget = () => {
        this.setState({
            action: 'MOVE_ALL',
            from: 'target',
            to: 'source',
        });
    };

    render() {
        return (
            <Shuttle shuttleState={this.state} setShuttleState={this.setState}>
                <Shuttle.Container>
                    {/* ... */}
                </Shuttle.Container>
                <Shuttle.Controls>
                    {() => (
                        <>
                            <button onClick={this.moveAllFromSource}>{'\u00BB'}</button>
                            <button onClick={this.moveSelectedFromSource}>{'\u203A'}</button>
                            <button onClick={this.moveSelectedFromTarget}>{'\u2039'}</button>
                            <button onClick={this.moveAllFromTarget}>{'\u00AB'}</button>
                        </>
                    )}
                </Shuttle.Controls>
                <Shuttle.Container>
                    {/* ... */}
                </Shuttle.Container>
            </Shuttle>
        );
    }
}

ReactDOM.render(<App />, document.getElementById('app'));

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 :smile:

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:

  1. state
  2. initialSelections - optional
  3. disabled - optional
  4. 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';

function App() {
    const shuttle = useShuttleState(
        {
            source: ['a', 'b', 'c'],
            target: ['d', 'e', 'f'],
        },
        null,
        null,
        {
            selectFirstItem: (state: any, action: { [key: string]: any } = {}) => {
                if (action.type === 'SELECT_FIRST_ITEM') {
                    if (action.container !== 'source' && action.container !== 'target') {
                        throw new Error('Missing container from SELECT_FIRST_ITEM reducer');
                    }

                    if (!state[action.container].length) {
                        console.warn(
                            `Cannot apply selectFirstItem when ${action.container} is empty`
                        );

                        return { ...state };
                    }

                    if (!state.selected[action.container].size) {
                        state.selected[action.container].add(0);
                    }

                    return { ...state };
                }

                return { ...state };
            },
        }
    );

    return (
        <Shuttle {...shuttle}>
            <Shuttle.Container
                onClick={() => {
                    shuttle.setShuttleState({
                        type: 'SELECT_FIRST_ITEM',
                        container: 'source',
                    });
                }}>
                {({ source, selected }, getItemProps) =>
                    source.map((item, index) => (
                        <Shuttle.Item
                            {...getItemProps(index)}
                            key={item}
                            value={item}
                            selected={selected.source.has(index)}>
                            {item}
                        </Shuttle.Item>
                    ))
                }
            </Shuttle.Container>
            <Shuttle.Controls />
            <Shuttle.Container
                onClick={() => {
                    shuttle.setShuttleState({
                        type: 'SELECT_FIRST_ITEM',
                        container: 'target',
                    });
                }}>
                {/* ... */}
            </Shuttle.Container>
        </Shuttle>
    );
}

ReactDOM.render(<App />, document.getElementById('app'));

FAQ

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
            .filter(item => item.includes(sourceFilter))
            .map((item, index) => (
                <Shuttle.Item
                    {...getItemProps(index)}
                    key={item}
                    value={item}
                    selected={selected.source.has(index) && source[index] === item}>
                    {item}
                </Shuttle.Item>
            ))
    }
</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.