Skip to content

SelectionArea

WxSelectionArea is the rubber band: drag across a grid or a list and everything the box touches is selected. It is what a file manager does, and what an image library is expected to do.

Drag across the tiles
2 of 12 selected
engine.png
engine.png
hydraulics.png
hydraulics.png
transmission.png
transmission.png
undercarriage.png
undercarriage.png
electrical.png
electrical.png
cabin.png
cabin.png
pdf
filters.pdf
xlsx
fasteners.xlsx
zip
attachments.zip
tyres.png
tyres.png
docx
accessories.docx
bucket.png
bucket.png
Shift or ctrl to add to the selection, alt to take away. Click a card to pick it on its own, the background to clear. The tiles are FileCards, and their buttons are buttons — so a drag that starts on one is the button's, not the box's.
One at a time — a file picker rather than a gallery
engine.png
engine.png
hydraulics.png
hydraulics.png
transmission.png
transmission.png
undercarriage.png
undercarriage.png
No box, no run, no toggle: a click or a tap picks the one under it, the background clears. Chosen: nothing
And down a list, where the box only has to touch a row
WX-4100Ada Lovelace€1,240
WX-4101Grace Hopper€380
WX-4102Alan Turing€2,905
WX-4103Katherine Johnson€145
WX-4104Margaret Hamilton€760
Selected: nothing

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { WxSelectionArea, vWxSelect } from '@webx-ui/core'

const picked = ref<number[]>([])
</script>

<template>
  <wx-selection-area v-slot="{ isSelected }" v-model="picked" class="grid">
    <figure
      v-for="file in files"
      :key="file.id"
      v-wx-select="file.id"
      :class="{ 'is-selected': isSelected(file.id) }"
    >

    </figure>
  </wx-selection-area>
</template>

Two parts: the area, which draws the box and holds the selection, and v-wx-select, which hands it an item and the value that stands for it. app.use(WebxUI) registers the directive along with the components; a named import brings it in on its own.

The item is whatever is already there

A directive rather than a wrapper component, because the thing being selected is already an element — a card, a row, a list item — and putting a box of ours around every one of them would break the grid or the table it sits in. v-wx-select goes on a <tr> as readily as on a <figure>, and it keeps the value's type: v-wx-select="42" selects the number 42, which is what the model holds and what the API expects back.

For markup that is not written in Vue, data-wx-selectable="42" does the same thing and yields the string '42' — all an attribute can carry.

The grid above is FileCard, which is what a media library is made of. It is an element like any other, so the directive goes on it and nothing else changes:

vue
<wx-selection-area v-slot="{ isSelected }" v-model="picked" class="grid">
  <wx-file-card
    v-for="file in files"
    :key="file.id"
    v-wx-select="file.id"
    :name="file.name"
    :thumbnail="file.thumbnail"
    :selected="isSelected(file.id)"
    removable
    copyable
  />
</wx-selection-area>

What the area does not touch

A drag that begins on a link, a button, a field or anything else a pointer already means something to is left to that control. The buttons on a FileCard stay its buttons. Mark anything else with data-wx-no-select — a description a reader is meant to be able to copy, say.

The whole gesture, not just the box

A selection people can only make by dragging is a selection they cannot make one item at a time, so the area handles the rest of it as well:

GestureWhat it does
DragReplaces the selection with what the box caught
Shift- or ctrl-dragAdds to it
Alt-dragTakes away from it
Click an itemPicks that one
Ctrl-click an itemAdds or removes that one
Shift-click an itemTakes the run from the last one clicked
Tap an itemAdds or removes that one
Click the backgroundClears
Ctrl+A / EscapeEverything / nothing

That is the table for a selection that may hold several. Where only one may be held, every row of it that adds a second thing is gone — see below.

Set :click-select="false" to keep the drag and leave clicking to the items themselves — a grid whose tiles open something when clicked wants that.

One at a time

:multiple="false" and the model never holds more than one value. There is no box, no run and no toggle — every one of them is a way of ending up holding a second thing — so a click or a tap picks the item under it and the background clears. A gallery wants several; a file picker wants one.

vue
<template>
  <wx-selection-area v-slot="{ isSelected }" v-model="picked" :multiple="false">
    <figure v-for="file in files" :key="file.id" v-wx-select="file.id">…</figure>
  </wx-selection-area>
</template>

The model stays an array — of nought or one — so nothing else about the component changes with the flag. picked[0] is the file, and a v-model handed several values is left as it was found: the limit is on what the gestures produce, not on what the caller may say.

The selection is the model

v-model is an array of values, in the order they were taken. The area never keeps a second copy of it, so a selection can be set from outside — restored from a query string, cleared after a bulk action — and the grid follows.

isSelected comes down with the slot because the alternative is picked.includes(id) once per item, which is a scan of the whole selection per tile on every render. The slot's version is a Set.

It measures once

The items are measured when the drag begins, in the area's own coordinates, and those do not move when anything scrolls. So dragging past the foot of a long list — which scrolls it, at a speed that grows the further past the edge you are — costs nothing per frame but the arithmetic.

The bill for that is a layout that changes mid-drag: a list that loads more rows underneath one will not catch them until the next drag. Set :edge-scroll="0" where the area should not scroll anything at all.

A finger is not a mouse

A tap picks the item under it, always — that half needs nothing turned on. It is the box that is off by default: on a touch screen a drag across a grid means scroll, and taking that away leaves people stranded. A finger that travels is left to the browser, and the selection it started on is kept, not replaced.

A tap adds and removes rather than replacing, the way ctrl-click does. There is no modifier on a phone and no box either, so a tap that replaced the selection would be a selection that can never hold more than one thing. A tap on the background still clears, which is the way back to none.

touch turns the box on where the gesture is worth more than the scrolling — a canvas, a seat picker. The area then sets touch-action: none, which is the real price: it stops scrolling with a finger at all. Those interfaces usually want a long press first, which this does not do.

Test that on a device rather than in a desktop browser's device mode. The emulator sends the gesture as a pointer and nothing takes it away; a phone hands it to the scroller, which is the whole difference.

Props

PropTypeDefaultDescription
modelValue(string | number)[][]The selection
multiplebooleantrueOff, the model never holds more than one value
match'intersect' | 'contain''intersect'Whether the box has to cover an item or touch it
thresholdnumber5Pixels before a press becomes a drag
clickSelectbooleantrueClicks pick items; a click beside them clears
touchbooleanfalseThe box can be drawn with a finger; a tap always picks
edgeScrollnumber48How near the edge the drag scrolls; 0 never does
disabledbooleanfalseLeaves every pointer alone

Events: update:modelValue; start; end (SelectionValue[]).

Slot props: selected (a Set), isSelected(value), selecting.

Exposed: selectAll(), clear().

Accessibility

A rubber band is a pointer gesture and has no keyboard in it, so it must never be the only way to select something. Give the items their own affordance — a checkbox on the tile, a checkbox column in the table — and let the box be the accelerator it is. Ctrl+A and Escape work once the area has been clicked in; a focusable item inside it keeps its own keyboard behaviour.

If the list has a count or a toolbar that appears with the selection, announce it: a live region saying 3 selected is what a screen reader gets in place of watching the box.

Released under the MIT License.