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

use-selectify

v0.4.0

Published

The ultimate drag-to-select solution for React.

Downloads

266

Readme

use-selectify cover image

Introduction

Drag interactions are one of the most challenging aspects of the web. Having complete control over the exact behavior of those interactions is essential, yet most available libraries out there still feel like they are not up to the task.

Recognizing this need, use-selectify was created aiming to address those issues and provide a powerful starting point for drag interactions while still remaining a robust approach to complex selections of elements in a React application, all done through a hook.

Demo & Examples: useselectify.js.org

Key Features

✅ Automatic window scrolling

✅ Flexible and lightweight (4kB gzipped)

✅ Accessible by default

✅ Fine-grained control with multiple approaches

✅ Simple & extensible styling

✅ Works on every device

✅ SSR & Lazy loading support

✅ Zero dependencies

Installation

Install use-selectify from your terminal via npm or yarn.

npm install use-selectify

or

yarn add use-selectify

Import it

Import the useSelectify hook. Both default and named imports are supported.

import { useSelectify } from "use-selectify";

Anatomy

export default () => {
    const {
        SelectBoxOutlet,
        selectedElements,
        isDragging,
        hasSelected,
        selectionBox,
        getSelectableElements,
        selectAll,
        clearSelection,
        mutateSelections,
        cancelSelectionBox,
        options,
    } = useSelectify(ref, options);
};
  • ref: A RefObject containing the parent element that will trigger the selection interactions.
  • options (optional): An object containing options that can be used to configure the selection behavior.

API Reference

  • SelectBoxOutlet: The returned selection box component.
  • selectedElements: A list of every element that has been selected through the hook.
  • isDragging: Whether the user's pointer is dragging or not.
  • hasSelected: Whether there's any element selected. Equal to selectedElements.length > 0.
  • selectionBox: A Rect indicating the internal values of the SelectBoxOutlet coordinates.
  • getSelectableElements: An utility function that returns every selectable element relative to the specified selection criteria
  • selectAll: An utility function that will select every selectableElement.
  • clearSelection: An utility function that will unselect every selected element.
  • mutateSelections: An utility function, similar to a setState, that allows you to modify internal selections.
  • cancelSelectionBox: An utility function that will instantly cancel the drag-selection without selecting any element.
  • options: A copy of the hook options.

Getting Started

Begin by defining the element that will contain the drag interaction, then render the selection box outlet in it.

import * as React from "react";
import { useSelectify } from "use-selectify";

export default function App() {
    const selectionContainerRef = React.useRef(null);
    const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
        onSelect: (element) => {
            console.log(`selected ${element}`);
            element.innerHTML = "Foo bar";
        },
    });

    return (
        <div ref={selectionContainerRef} style={{ position: "relative" }}>
            <div>I can be selected!</div>
            <SelectBoxOutlet />
        </div>
    );
}

By default every element inside the selectionContainerRef is a selectable element. To modify this behavior simply specify a selection criteria using CSS Selectors:

import * as React from "react";
import { useSelectify } from "use-selectify";

export default function App() {
    const selectionContainerRef = React.useRef(null);
    const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
        selectCriteria: ".select-this", // will only select elements with the "select-this" class
        onSelect: (element) => {
            console.log(`selected ${element}`);
            element.innerHTML = "Foo bar";
        },
    });

    return (
        <div ref={selectionContainerRef} style={{ position: "relative" }}>
            <div className="select-this">Hello World</div>
            <div>I won't be selected</div>
            <SelectBoxOutlet />
        </div>
    );
}

Exclusion Zone

In order to prevent selection starting from anywhere inside your container, you can also specify an exclusion zone. It supports both Elements (such as React refs) or CSS Selectors.

const selectionContainerRef = React.useRef(null);
const exclusionZoneRef = React.useRef(null);

const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
    exclusionZone: exclusionZoneRef.current,
});

return (
    <div ref={selectionContainerRef} style={{ position: "relative" }}>
        <div>
            <p>Selection can start from here</p>
        </div>
        <div ref={exclusionZoneRef}>
            <p>But not from here</p>
        </div>
        <SelectBoxOutlet />
    </div>
);

You can specify the callback function outside of the hook for further customization:

const handleSelection = (el: Element) => {
    console.log(`selected ${element}`);
    element.innerHTML = "Bar";
    // ...
};

const handleUnselection = (el: Element) => {
    console.log(`unselected ${element}`);
    element.innerHTML = "Foo";
    // ...
};

const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
    selectCriteria: ".select-this",
    onSelect: handleSelection,
    onUnselect: handleUnselection,
});

Styling

By default the selection box comes with some styling. You can override it with the className prop and specify how you want your selection box to look through the Outlet Component. For Styled Components or Stitches see how to render your own selection box.

Note

The component can be placed anywhere in your page, just make sure that for the absolute positioning to work properly, the parent element of the outlet should always be relative: position: relative;

return (
    <div ref={selectionContainerRef} style={{ position: "relative" }}>
        // ...
        <SelectBoxOutlet className="foo bar" />
    </div>
);

Advanced usage

Mapping reactive components without a callback

We can check if an element is selected by passing the selectedElements down and simply looping accordingly:

import * as React from "react";
import { useSelectify } from "use-selectify";

const data = [
    {
        id: 1,
        name: "foo",
        role: "admin",
    },
    {
        id: 2,
        name: "bar",
        role: "editor",
    },
    {
        id: 3,
        name: "foo-bar",
        role: "author",
    },
    {
        id: 4,
        name: "bar-foo",
        role: "author",
    },
];

const ListItem = ({
    selectedElements,
    children,
}: {
    selectedElements: Element[];
    children: React.ReactNode;
}) => {
    const itemRef = React.useRef(null);
    const isSelected = selectedElements.includes(itemRef.current);

    return (
        <li ref={itemRef} className={isSelected ? "list-item-active" : ""}>
            {children}
        </li>
    );
};

export const List = () => {
    const containerRef = React.useRef(null);
    const { SelectBoxOutlet, selectedElements } = useSelectify(selectionContainerRef);

    return (
        <div ref={containerRef} className="container">
            <ul className="list">
                {data.map((user) => (
                    <ListItem key={user.id} selectedElements={selectedElements}>
                        {user.name}
                    </ListItem>
                ))}
            </ul>
            <SelectBoxOutlet />
        </div>
    );
};

Declaratively handling selections

If you wish to couple the internal hook selections state with your own, you can leverage the mutateSelections function. Similarly to a setState, you can modify which elements are internally selected in a declarative way.

// ...

const selectionContainerRef = React.useRef(null);
const { SelectBoxOutlet, selectedElements, mutateSelections } = useSelectify(selectionContainerRef);

const selectElement = (elementToSelect) => {
    mutateSelections((prevSelections) => [...prevSelections, elementToSelect]);
};

const unselectElement = (elementToUnselect) => {
    mutateSelections((prevSelections) =>
        prevSelections.filter((element) => element !== elementToSelect)
    );
};

Tip: If you think the user won't initially be using drag-selection, consider enabling lazy-load:

const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
    lazyLoad: true,
});

Other use-cases

Use something like react-device-detect to distinguish if the user-agent is a mobile device or not, then simply disable the hook accordingly.

import * as React from "react";
import { isMobile } from "react-device-detect";
import { useSelectify } from "use-selectify";

export default function App() {
    const selectionContainerRef = React.useRef(null);
    const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
        disabled: isMobile,
    });

    return (
        <div ref={selectionContainerRef} style={{ position: "relative" }}>
            <div>Hello World</div>
            <SelectBoxOutlet />
        </div>
    );
}
const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
    onDragStart: (event) => {
        setTimeout(() => {}, 250); // wait for 250ms for pinch gestures before starting drag-selection
    },
});

The same can be applied to other use cases, if you need to cancel the selection simply return event.preventDefault().

const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
    onDragStart: (event) => {
        let shouldCancel = false;
        setTimeout(() => {}, 200); // wait 200ms before checking if should cancel selection
        /**
         * check if should cancel
         * ...
         **/
        shouldCancel = true;
        if (shouldCancel) {
            return event.preventDefault(); // cancel selection
        }
    },
});

Start by creating your box component and pass in the provided selectionBoxRef from the hook, then apply the selectionBox to the styles for the pointer coordinates.

Note

You will not have any of the accessibility features included by default.

export function App() {
    const selectionContainerRef = React.useRef(null);
    const { selectionBoxRef, selectionBox } = useSelectify(selectionContainerRef);

    const MyCustomSelectionBoxOutlet = () => {
        // Your custom logic...
        return (
            <div
                ref={selectionBoxRef}
                style={{
                    ...selectionBox,
                    boxSizing: "border-box",
                    position: "absolute",
                }}
            />
        );
    };

    return (
        <div ref={selectionContainerRef} style={{ position: "relative" }}>
            <div>I can be selected!</div>
            <MyCustomSelectionBoxOutlet />
        </div>
    );
}

Make sure you add position: absolute and border-box for floating-ui calculations.

Options

| Prop | Type | Default | Description | | ----------------------- | -------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | maxSelections | number | false | - | Maximum number of elements that can be selected. Will stop selecting after reaching that number and keep already selected elements. | | autoScroll | boolean | true | Automatically try to scroll the window when the pointer approaches the viewport edge while dragging. | | autoScrollEdgeDistance | number | 100 | Distance in px from the viewport's edges from which the box will try scrolling the window when the pointer approaches the viewport edge while dragging. | | autoScrollStep | number | 30 | Auto scroll speed. | | disableUnselection | boolean | false | Will keep every item selected after selection. Can be cleared with clearSelection(). | | selectCriteria | string | undefined | "*" | The specific CSS Selector criteria to match for selecting elements. | | onlySelectOnFullOverlap | boolean | false | Will only select the element if the full rect intersects. | | onlySelectOnDragEnd | boolean | false | Will only select elements after user has stopped dragging or cursor has left the screen while dragging. | | selectionDelay | number | 0 | Specify a delay in miliseconds before elements are selected to prevent accidental selection. | | label | string | "Drag Selection" | Accessible label for screen readers. | | selectionTolerance | number | 0 | Distance in px from which elements can be selected even if the selection box is not visually intersecting. | | activateOnMetaKey | boolean | false | Only enables the selection box if the user was pressing a meta key while initiating the drag. Included Meta keys are: Shift, Ctrl/Cmd and Alt. | | activateOnKey | string[] | [] | Only enables the selection box if the user was pressing a specified key while initiating the drag. Ex: ["Tab", "Control", "Alt"] | | theme | "default" | "outline" | "default" | Included theme options for the selection box appearance. | | hideOnScroll | boolean | false | Whether to hide the selection box when the window starts scrolling. Incompatible with autoScroll. | | exclusionZone | Element | Element[] | string | - | Won't enable the selection box if the user tries initiating the drag from one of the specified elements. | | scrollContext | HTMLElement | Window | window | Sets the scrollable element for the automatic window scrolling to react. | | exclusionZone | Element | Element[] | - | Won't enable the selection box if the user tries initiating the drag from one of the specified elements. Supports CSS Selectors. | | lazyLoad | boolean | false | Defers loading the selection box. | | disabled | boolean | false | Disables the selection box interaction & dragging. | | forceMount | boolean | false | Forces the mounting of the selection box on initialization. | | onSelect | (element: Element) => void | - | Callback function when an element is selected. | | onUnselect | (unselectedElement: Element) => void | - | Callback function when an element is unselected. | | onDragStart | (e: PointerEvent) => void | - | Callback function when drag starts. | | onDragMove | (e: PointerEvent, selectedElements: Element[]) => void | - | Callback function when dragging. | | onDragEnd | (e?: PointerEvent, selectedElements?: Element[]) => void | - | Callback function when drag ends. | | onEscapeKeyDown | (e: KeyboardEvent) => void | - | Callback function when escape key is pressed. |

Accessibility (optional)

By default use-selectify already follows WAI-ARIA best practices. Though to ensure that drag interactions are as accessible as possible, we must consider the following aspects:

  1. Add ARIA attributes: To indicate to assistive technology users that the elements are available for selection, we can use an aria-label to each selectable element. This label should be descriptive and informative, indicating either the purpose of selecting that element or how to select it for screen readers. Additionally, we can use the aria-selected attribute to indicate when elements are selected:

    const { SelectBoxOutlet } = useSelectify(selectionContainerRef, {
        onSelect: (el) => {
            el.setAttribute("aria-selected", "true");
        },
    });
    // ...
  2. Make elements focusable: To ensure that keyboard-only users can access and select the elements, all functionality should be also operable through the keyboard alone. Ensure that every selectable element is also focusable. This means either adding a tabindex attribute to the element and setting it to 0 or using an element that is focusable by default.

  3. Arrow navigation: Make sure every selectable element can also be selected using the arrow keys.

FAQ

How performant is it?

Stays decently performant up until a few thousands elements. Open to improvements.

Can I use this library with an older version of React?

No, currently the only supported version is ^18.0.0.

How do I it make mobile friendly

Although we support touch interactions, it should be considered the conflict of panning/scrolling and selecting when presented with a single gesture. To effectively support mobile devices in an accessible way you would need to provide a way to switch between panning and drag-selecting like seen in our figma example and even then, such interactions are not recommended in small viewports.

Does this support React Native?

No, not currently.


Looking forward to seeing how this project and the community evolve and provide feedback. Whether it's a feature request, bug report, or a project to showcase, feel free to get involved!

Development

  1. Clone the repo into a public GitHub repository (or fork https://github.com/rortan134/use-selectify/fork).

    git clone https://github.com/rortan134/use-selectify.git
  2. Go to the project folder

    cd use-selectify
  3. Install packages with yarn

    yarn
  4. Start the Storybook preview for development and modify as you please.

    yarn storybook

License

Distributed under the MIT License. See LICENSE for more information.