Enhancement

Droppable

Drag items between dropzones that hold one item each

Import
@parallelogram-js/core/components/Droppable
Selector
[data-droppable]
Stylesheet
@parallelogram-js/core/styles/draggable.css

Playground

Dropzones

Drag the invoice between the columns.

Invoice 42
<div class="droppable-zones" data-droppable>
  <div class="droppable-zone" data-droppable-zone="inbox" aria-label="Inbox"><div class="card">Invoice 42</div></div>
  <div class="droppable-zone" data-droppable-zone="review" aria-label="In review"></div>
  <div class="droppable-zone" data-droppable-zone="paid" aria-label="Paid"></div>
</div>

About

The dropzones are the elements marked data-droppable-zone inside the container, and the items are their children, or the elements data-droppable-items selects. An item dragged over an empty dropzone moves into it; dragged back over its own, or let go outside every dropzone, it goes home. A dropzone holding an item is occupied, so the markup needs no marker for it.

From the keyboard, Space or Enter picks up the focused item, the arrow keys move it to the previous or next free dropzone, Space or Enter drops it and Escape puts it back. Give each dropzone an aria-label so its name is announced; one without is announced by its data-droppable-zone value. Containers with the same data-droppable-group share their dropzones. droppable:change fires once an item has landed in a different dropzone.

Usage

Register the component and the framework loads it the first time a page contains [data-droppable].

import { Parallelogram } from '@parallelogram-js/core';

const app = Parallelogram.create();
app.components.add('[data-droppable]', () => import('@parallelogram-js/core/components/Droppable'));
app.run();

The framework is optional. Droppable.enhanceAll() mounts the component on every matching element by itself, for a page that wants one enhancement rather than a framework:

import Droppable from '@parallelogram-js/core/components/Droppable';

Droppable.enhanceAll();

It takes a selector and the component's options, and returns the instance it mounted them with. What it leaves out is what the framework adds: loading a component only on the pages that use it, mounting again after a page swap, and ordering with dependsOn.

Its styles come from the package stylesheet:

@import '@parallelogram-js/core/styles/draggable.css';

Acknowledgements

Droppable is built on Shopify Draggable, used under the MIT licence. Copyright (c) 2018-present Shopify.

The drag engine, its Mirror, Scrollable and Focusable plugins, and the Sortable, Swappable and Droppable behaviour are ported from Draggable 1.2. A single Pointer Events sensor replaces its mouse, touch and force-touch sensors, the keyboard sensor is new, and state is written to data attributes rather than classes. Thank you to Shopify and Draggable's contributors for the work this builds on.

Attributes

NameTypeDefaultDescription
data-droppablerequiredflagnoneMarks the container
data-droppable-groupstringnoneContainers with the same group share one drag, so items move between them; the first one mounted sets the options For every instance, Droppable.defaults.group.
data-droppable-itemsselectornoneThe elements that can be dragged; the dropzones' children when not set For every instance, Droppable.defaults.items.
data-droppable-handleselectornoneThe part of an item that starts a drag, such as a grip; give it touch-action: none so a touch on it drags rather than scrolls For every instance, Droppable.defaults.handle.
data-droppable-distancenumber4Pixels a mouse or pen moves before a drag starts, so a click on a link or button inside an item still clicks it For every instance, Droppable.defaults.distance.
data-droppable-touch-delaynumber100Milliseconds a finger rests on an item before a drag starts; moving sooner scrolls the page instead For every instance, Droppable.defaults.touchDelay.
data-droppable-axisboth | x | ybothWhich way the copy under the pointer may move For every instance, Droppable.defaults.axis.
data-droppable-disabledbooleanfalseStops new drags from starting in this container while set
data-droppable-picked-upstringPicked up {item}.Announced when an item is picked up; {item} is its label For every instance, Droppable.defaults.pickedUp.
data-droppable-keyboard-hintstringArrow keys move it between dropzones, Space drops it, Escape cancels.Announced after the pick-up message when the keyboard picked the item up For every instance, Droppable.defaults.keyboardHint.
data-droppable-movedstringIn {zone}.Announced after each move; {item} and {zone}, the dropzone it is in For every instance, Droppable.defaults.moved.
data-droppable-droppedstringDropped {item}.Announced when an item is dropped For every instance, Droppable.defaults.dropped.
data-droppable-cancelledstringCancelled. {item} is back where it started.Announced when a drag is cancelled with Escape For every instance, Droppable.defaults.cancelled.
data-droppable-stateset by the componentidle | active | overnoneactive while a drag in its group is in progress, over while the item is over this container
data-droppable-item-stateset by the componentplaceholder | mirror | placednoneOn the placeholder showing where the item will land, the copy following the pointer, and an item just dropped On item.
data-droppable-zonerequiredstringnoneMarks a dropzone; the value names it in announcements when it has no aria-label On dropzone.
data-droppable-zone-stateset by the componentempty | available | occupiednoneoccupied while it holds an item; available when empty during a drag On dropzone.

Methods

NameSignatureDescription
cancel(element: HTMLElement) => voidCancel a drag in progress in the element’s group, putting the item back

Events

NameDetailDescription
droppable:start{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null }An item is being picked up; preventDefault() leaves it where it is DOM event and event bus Cancelable.
droppable:dropped{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; dropzone: HTMLElement }The item is about to move into a dropzone; preventDefault() stops it DOM event and event bus Cancelable.
droppable:returneddropzone: HTMLElementThe item is about to go back to the dropzone it started in; preventDefault() keeps it where it is DOM event and event bus Cancelable.
droppable:stop{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; dropzone: HTMLElement; initialDropzone: HTMLElement; changed: boolean; items: HTMLElement[] }The drag has ended and the item is in place, dispatched on each container it changed; items are that container’s items in order DOM event and event bus
droppable:change{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; dropzone: HTMLElement; initialDropzone: HTMLElement; changed: boolean; items: HTMLElement[] }As droppable:stop, only when the item ended up in a different dropzone: the one to save on DOM event and event bus

CSS custom properties

NameDefaultDescription
--drag-over-bgvar(--color-hover)Background of a container the item is over
--drag-placeholder-opacity0.4Opacity of the placeholder where the item will land
--drag-placeholder-outline2px dashed var(--color-accent)Outline of the placeholder
--drag-mirror-shadowvar(--surface-dropdown-shadow)Shadow of the copy following the pointer
--drag-mirror-opacity0.95Opacity of the copy following the pointer
--drag-mirror-z-index2147483000Stacking of the copy following the pointer
--drag-placed-colorrgba(37, 99, 235, 0.15)Colour a dropped item fades from; none under prefers-reduced-motion
--drag-placed-duration0.6sHow long a dropped item takes to fade back
--drag-zone-outline2px dashed var(--color-border-strong)Outline of a dropzone the item can go into, while dragging