@charcuterie/ui 3.20.0 → 3.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/Board/BoardCard.d.ts +42 -18
- package/dist/Board/BoardCard.d.ts.map +1 -1
- package/dist/Board/BoardCard.js +17 -1
- package/dist/Board/BoardCard.js.map +1 -1
- package/dist/DropRail/DropRail.d.ts +109 -0
- package/dist/DropRail/DropRail.d.ts.map +1 -0
- package/dist/DropRail/DropRail.js +277 -0
- package/dist/DropRail/DropRail.js.map +1 -0
- package/dist/NavBar/NavBar.d.ts +130 -0
- package/dist/NavBar/NavBar.d.ts.map +1 -0
- package/dist/NavBar/NavBar.js +114 -0
- package/dist/NavBar/NavBar.js.map +1 -0
- package/dist/NavBar/NavBarLink.d.ts +43 -0
- package/dist/NavBar/NavBarLink.d.ts.map +1 -0
- package/dist/NavBar/NavBarLink.js +67 -0
- package/dist/NavBar/NavBarLink.js.map +1 -0
- package/dist/NavBar/navBarCurrent.d.ts +37 -0
- package/dist/NavBar/navBarCurrent.d.ts.map +1 -0
- package/dist/NavBar/navBarCurrent.js +48 -0
- package/dist/NavBar/navBarCurrent.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Board/Board.stories.tsx +100 -0
- package/src/Board/Board.test.tsx +62 -0
- package/src/Board/BoardCard.tsx +71 -18
- package/src/DropRail/DropRail.mdx +90 -0
- package/src/DropRail/DropRail.stories.tsx +328 -0
- package/src/DropRail/DropRail.test.tsx +154 -0
- package/src/DropRail/DropRail.tsx +496 -0
- package/src/NavBar/NavBar.mdx +157 -0
- package/src/NavBar/NavBar.stories.tsx +350 -0
- package/src/NavBar/NavBar.test.tsx +344 -0
- package/src/NavBar/NavBar.tsx +271 -0
- package/src/NavBar/NavBarLink.tsx +139 -0
- package/src/NavBar/navBarCurrent.test.ts +81 -0
- package/src/NavBar/navBarCurrent.ts +64 -0
- package/src/icons.storyHelpers.tsx +12 -0
- package/src/index.ts +11 -0
- package/src/sourceRules.test.ts +24 -1
|
@@ -188,6 +188,45 @@ const ADA: [string, string] = ["AD", "Ada"]
|
|
|
188
188
|
|
|
189
189
|
const WREN: [string, string] = ["WR", "Wren"]
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* A categorical index per project, and the same four projects the
|
|
193
|
+
* rest of this file uses. The index is what a consumer's user
|
|
194
|
+
* PICKED — it is not derived here, and it is never a hex.
|
|
195
|
+
*/
|
|
196
|
+
const PROJECT_EDGE_ITEMS: BoardItem[] = (
|
|
197
|
+
[
|
|
198
|
+
[
|
|
199
|
+
PROJECTS.signal,
|
|
200
|
+
2,
|
|
201
|
+
"Retire the second scheduler and fold its jobs into the broker",
|
|
202
|
+
],
|
|
203
|
+
[
|
|
204
|
+
PROJECTS.loom,
|
|
205
|
+
5,
|
|
206
|
+
"One composed Storybook for every app in the fleet",
|
|
207
|
+
],
|
|
208
|
+
[
|
|
209
|
+
PROJECTS.atlas,
|
|
210
|
+
8,
|
|
211
|
+
"Fingerprint duplicate uploads before they reach the queue",
|
|
212
|
+
],
|
|
213
|
+
[
|
|
214
|
+
PROJECTS.ferry,
|
|
215
|
+
10,
|
|
216
|
+
"Rewrite the ingest runbook against the new broker",
|
|
217
|
+
],
|
|
218
|
+
] as const
|
|
219
|
+
).map(([project, categorical, title]) => ({
|
|
220
|
+
accentEdge: { categorical },
|
|
221
|
+
// The words the bar cannot say. A colour alone is a WCAG
|
|
222
|
+
// 1.4.1 failure and is silent to a screen reader.
|
|
223
|
+
accentLabel: project,
|
|
224
|
+
href: `#/task/${encodeURIComponent(title)}`,
|
|
225
|
+
key: title,
|
|
226
|
+
meta: <ProjectChip name={project} />,
|
|
227
|
+
title,
|
|
228
|
+
}))
|
|
229
|
+
|
|
191
230
|
const TODO_ITEMS: BoardItem[] = [
|
|
192
231
|
toItem({
|
|
193
232
|
assignee: WREN,
|
|
@@ -565,6 +604,67 @@ export const Responsive: Story = {
|
|
|
565
604
|
),
|
|
566
605
|
}
|
|
567
606
|
|
|
607
|
+
/**
|
|
608
|
+
* **The project on the leading edge**, in both of the shapes a card
|
|
609
|
+
* takes — and the reason `accentEdge` exists beside `accentIntent`.
|
|
610
|
+
*
|
|
611
|
+
* The pill arm is a `w-1` span INSIDE the card, so it is a straight
|
|
612
|
+
* rectangle next to a rounded box. The edge arm is `Card`'s own
|
|
613
|
+
* treatment: a pseudo-element taking `border-radius: inherit`, so it
|
|
614
|
+
* paints a straight stripe down a row that has no corner and wraps
|
|
615
|
+
* the corner once the lane passes `cq-lg` and the row becomes a
|
|
616
|
+
* card. One prop, correct in both shapes, and never a notch.
|
|
617
|
+
*
|
|
618
|
+
* The colours are CATEGORICAL — an identity, not a state. A project,
|
|
619
|
+
* a repo, a source. Priority stays on the pill arm, where an intent
|
|
620
|
+
* belongs.
|
|
621
|
+
*/
|
|
622
|
+
export const AccentEdge: Story = {
|
|
623
|
+
render: () => (
|
|
624
|
+
<div className="flex flex-col gap-8">
|
|
625
|
+
<div className="flex flex-col gap-2">
|
|
626
|
+
<span className="font-mono text-content-muted text-xs">
|
|
627
|
+
a narrow lane — rows, so the edge is a straight
|
|
628
|
+
stripe
|
|
629
|
+
</span>
|
|
630
|
+
|
|
631
|
+
<Frame inlineSize="22rem">
|
|
632
|
+
<Board
|
|
633
|
+
label="Today, by project"
|
|
634
|
+
lanes={[
|
|
635
|
+
{
|
|
636
|
+
items: PROJECT_EDGE_ITEMS,
|
|
637
|
+
key: "todo",
|
|
638
|
+
label: "Todo",
|
|
639
|
+
},
|
|
640
|
+
]}
|
|
641
|
+
/>
|
|
642
|
+
</Frame>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<div className="flex flex-col gap-2">
|
|
646
|
+
<span className="font-mono text-content-muted text-xs">
|
|
647
|
+
the same lane past --cq-lg (48rem) — cards, so the
|
|
648
|
+
edge wraps the corner
|
|
649
|
+
</span>
|
|
650
|
+
|
|
651
|
+
<Frame inlineSize="56rem">
|
|
652
|
+
<Board
|
|
653
|
+
label="Today, by project, wide"
|
|
654
|
+
lanes={[
|
|
655
|
+
{
|
|
656
|
+
items: PROJECT_EDGE_ITEMS,
|
|
657
|
+
key: "todo",
|
|
658
|
+
label: "Todo",
|
|
659
|
+
},
|
|
660
|
+
]}
|
|
661
|
+
/>
|
|
662
|
+
</Frame>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
),
|
|
666
|
+
}
|
|
667
|
+
|
|
568
668
|
/**
|
|
569
669
|
* The complete keyboard path, and it is the **primary** way to move
|
|
570
670
|
* a card rather than an accessible alternative to the real one.
|
package/src/Board/Board.test.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { expectAgentDrivable } from "../testing/index.ts"
|
|
|
13
13
|
import * as stories from "./Board.stories.tsx"
|
|
14
14
|
|
|
15
15
|
const {
|
|
16
|
+
AccentEdge,
|
|
16
17
|
AllStates,
|
|
17
18
|
Default,
|
|
18
19
|
InBoardScreen,
|
|
@@ -405,3 +406,64 @@ test("the board adds no link of its own around a rich title", async () => {
|
|
|
405
406
|
null,
|
|
406
407
|
)
|
|
407
408
|
})
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The edge is drawn on the card's OWN box, which is the whole
|
|
412
|
+
* difference between it and the `accentIntent` pill.
|
|
413
|
+
*
|
|
414
|
+
* A pill is a `w-1` span inside the card: a straight rectangle
|
|
415
|
+
* beside a rounded box, which is the notch three apps grew
|
|
416
|
+
* independently before `Card` took the shape over. The edge is a
|
|
417
|
+
* pseudo-element taking `border-radius: inherit`, so it is correct
|
|
418
|
+
* in both of a board card's shapes without being told which one it
|
|
419
|
+
* is in — a straight stripe down a row that has no corner, and a
|
|
420
|
+
* wrapped bar once the lane passes `cq-lg`.
|
|
421
|
+
*
|
|
422
|
+
* The class is what is asserted rather than a painted pixel: the
|
|
423
|
+
* bar is a `box-shadow` on a `::before`, and the radius it inherits
|
|
424
|
+
* comes from a container query the runner's own width does not
|
|
425
|
+
* decide. `cardAccentEdge.test.ts` owns what the class contains.
|
|
426
|
+
*/
|
|
427
|
+
test("draws a project's colour on the card's own edge", async () => {
|
|
428
|
+
const { canvas } = await mountStory(AccentEdge)
|
|
429
|
+
|
|
430
|
+
const cards = canvas
|
|
431
|
+
.getAllByRole("listitem")
|
|
432
|
+
.filter(
|
|
433
|
+
(item: HTMLElement) => item.dataset.boardCard != null,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
expect(cards.length).toBeGreaterThan(0)
|
|
437
|
+
|
|
438
|
+
for (const card of cards) {
|
|
439
|
+
// The overlay, and the radius it takes from the card.
|
|
440
|
+
expect(card.className).toContain("before:absolute")
|
|
441
|
+
expect(card.className).toContain(
|
|
442
|
+
"before:rounded-[inherit]",
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
// A categorical colour, never an intent — an edge is an
|
|
446
|
+
// identity and an intent is a claim about what happens.
|
|
447
|
+
expect(card.className).toMatch(
|
|
448
|
+
/before:shadow-\[inset_3px_0_0_var\(--color-categorical-\d+-solid\)\]/,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
// And it never eats a press on the card it covers.
|
|
452
|
+
expect(card.className).toContain(
|
|
453
|
+
"before:pointer-events-none",
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// The colour reaches a screen reader as words. Each card names
|
|
458
|
+
// its project through `accentLabel`, so the bar is not the only
|
|
459
|
+
// channel carrying the fact.
|
|
460
|
+
expect(
|
|
461
|
+
canvas.getAllByText("Ferry Docs").length,
|
|
462
|
+
).toBeGreaterThan(1)
|
|
463
|
+
|
|
464
|
+
await expectNoAxeViolations(
|
|
465
|
+
canvas.getByRole("region", {
|
|
466
|
+
name: "Today, by project",
|
|
467
|
+
}),
|
|
468
|
+
)
|
|
469
|
+
})
|
package/src/Board/BoardCard.tsx
CHANGED
|
@@ -3,6 +3,11 @@ import type { ReactNode } from "react"
|
|
|
3
3
|
import { useState } from "react"
|
|
4
4
|
|
|
5
5
|
import { Button } from "../Button/Button.tsx"
|
|
6
|
+
import type { CardAccentEdge } from "../Card/cardAccentEdge.ts"
|
|
7
|
+
import {
|
|
8
|
+
getAccentEdgeClassName,
|
|
9
|
+
getAccentEdgeStyle,
|
|
10
|
+
} from "../Card/cardAccentEdge.ts"
|
|
6
11
|
import {
|
|
7
12
|
FOCUS_RING_CLASS,
|
|
8
13
|
INTENT_SOLID_FILL_CLASS,
|
|
@@ -13,23 +18,7 @@ import { toClassName } from "../toClassName.ts"
|
|
|
13
18
|
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx"
|
|
14
19
|
|
|
15
20
|
type BoardItemFields = {
|
|
16
|
-
/**
|
|
17
|
-
* The leading colour bar. Priority, in the consumer this was built
|
|
18
|
-
* for — *"Priority is styled as a leading colour bar, and **never**
|
|
19
|
-
* a coloured word before the title."*
|
|
20
|
-
*
|
|
21
|
-
* Pair it with `accentLabel`. A bar is colour and nothing else, so
|
|
22
|
-
* on its own it fails WCAG 1.4.1 outright and is invisible to
|
|
23
|
-
* every screen reader; the label rides along in a
|
|
24
|
-
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
25
|
-
* first board painted these `--color-danger-9` — a Radix-style
|
|
26
|
-
* scale this token set has never had — so every bar was
|
|
27
|
-
* transparent while every "is it rendered" assertion passed. The
|
|
28
|
-
* families here are `--color-intent-<intent>-<role>`; there is no
|
|
29
|
-
* numeric scale to reach for.
|
|
30
|
-
*/
|
|
31
|
-
accentIntent?: IntentName
|
|
32
|
-
/** What the bar means, in words. "Priority 0". */
|
|
21
|
+
/** What the bar means, in words. "Priority 0", "Movies and Shows". */
|
|
33
22
|
accentLabel?: string
|
|
34
23
|
/**
|
|
35
24
|
* Under the title and metadata, spanning the card: a live run
|
|
@@ -68,6 +57,42 @@ type BoardItemFields = {
|
|
|
68
57
|
trailing?: ReactNode
|
|
69
58
|
}
|
|
70
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The leading colour bar, in one of two shapes — and the second one
|
|
62
|
+
* exists because the first cannot follow a corner.
|
|
63
|
+
*
|
|
64
|
+
* `accentIntent` is an inset pill: a `w-1` span inside the card,
|
|
65
|
+
* beside the content. It was built for priority — *"Priority is
|
|
66
|
+
* styled as a leading colour bar, and **never** a coloured word
|
|
67
|
+
* before the title."* An intent is a claim the design system makes
|
|
68
|
+
* (`danger` says what happens if you press the thing), so this arm
|
|
69
|
+
* is for a bar that means a STATE.
|
|
70
|
+
*
|
|
71
|
+
* `accentEdge` is the card's own edge — `Card`'s treatment, the same
|
|
72
|
+
* pseudo-element, so it takes `border-radius: inherit` and wraps the
|
|
73
|
+
* corner at `cq-lg` where this card grows one. It carries a
|
|
74
|
+
* CATEGORICAL colour or a computed one, which is what an identity
|
|
75
|
+
* needs: a project, a repo, a source. The owner asked for exactly
|
|
76
|
+
* this on Docket's board — *"The Board has no colors. That could
|
|
77
|
+
* use project colors on the sides at least."* — after reporting the
|
|
78
|
+
* same treatment missing from the task rows.
|
|
79
|
+
*
|
|
80
|
+
* The two are mutually exclusive by type, not by convention. Both
|
|
81
|
+
* paint a leading bar in the same place, and a card wearing two of
|
|
82
|
+
* them is a card whose leading edge means two things.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ Either arm pairs with `accentLabel`. A bar is colour and
|
|
85
|
+
* nothing else, so on its own it fails WCAG 1.4.1 outright and is
|
|
86
|
+
* invisible to every screen reader; the label rides along in a
|
|
87
|
+
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
88
|
+
* first board painted its bars `--color-danger-9` — a Radix-style
|
|
89
|
+
* scale this token set has never had — so every bar was transparent
|
|
90
|
+
* while every "is it rendered" assertion passed.
|
|
91
|
+
*/
|
|
92
|
+
type BoardItemAccent =
|
|
93
|
+
| { accentEdge?: never; accentIntent?: IntentName }
|
|
94
|
+
| { accentEdge: CardAccentEdge; accentIntent?: never }
|
|
95
|
+
|
|
71
96
|
/**
|
|
72
97
|
* Who owns the card title's link — the board, or the caller.
|
|
73
98
|
*
|
|
@@ -114,7 +139,9 @@ type BoardItemTitleLink =
|
|
|
114
139
|
titleContent: ReactNode
|
|
115
140
|
}
|
|
116
141
|
|
|
117
|
-
export type BoardItem =
|
|
142
|
+
export type BoardItem = BoardItemAccent &
|
|
143
|
+
BoardItemFields &
|
|
144
|
+
BoardItemTitleLink
|
|
118
145
|
|
|
119
146
|
export type BoardCardProps = {
|
|
120
147
|
item: BoardItem
|
|
@@ -210,10 +237,36 @@ export const BoardCard = ({
|
|
|
210
237
|
// cannot do this to itself — it is the container, and a
|
|
211
238
|
// container query never matches its own container.
|
|
212
239
|
"cq-lg:mb-2 cq-lg:rounded-lg cq-lg:border cq-lg:bg-surface-raised cq-lg:p-3 cq-lg:shadow-low",
|
|
240
|
+
// The edge is drawn on THIS box, which is why it needs no
|
|
241
|
+
// shape of its own: the box is a row below `cq-lg` and a
|
|
242
|
+
// rounded card above it, and `border-radius: inherit` gives
|
|
243
|
+
// the right answer at both — a straight stripe down a row
|
|
244
|
+
// that has no corner, and a bar that wraps the card's
|
|
245
|
+
// corner once it grows one.
|
|
246
|
+
//
|
|
247
|
+
// `ps-2` is what keeps the content off it. The bar is 3px
|
|
248
|
+
// and overlays rather than displaces, so without this the
|
|
249
|
+
// first mark sits on top of it; at `cq-lg` the card's own
|
|
250
|
+
// `p-3` is already wider than the bar.
|
|
251
|
+
item.accentEdge != null &&
|
|
252
|
+
toClassName(
|
|
253
|
+
"ps-2",
|
|
254
|
+
getAccentEdgeClassName(item.accentEdge),
|
|
255
|
+
),
|
|
213
256
|
item.isDimmed && "opacity-60",
|
|
214
257
|
)}
|
|
215
258
|
data-board-card={item.key}
|
|
259
|
+
{...(item.accentEdge == null
|
|
260
|
+
? {}
|
|
261
|
+
: { style: getAccentEdgeStyle(item.accentEdge) })}
|
|
216
262
|
>
|
|
263
|
+
{/* The label the bar cannot say. An edge has no content of
|
|
264
|
+
its own — it is a pseudo-element — so unlike the pill
|
|
265
|
+
arm below there is nowhere inside it to put the words. */}
|
|
266
|
+
{item.accentEdge != null && item.accentLabel ? (
|
|
267
|
+
<VisuallyHidden>{item.accentLabel}</VisuallyHidden>
|
|
268
|
+
) : null}
|
|
269
|
+
|
|
217
270
|
{item.accentIntent ? (
|
|
218
271
|
<span
|
|
219
272
|
className={toClassName(
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
import * as DropRailStories from "./DropRail.stories";
|
|
4
|
+
|
|
5
|
+
<Meta of={DropRailStories} />
|
|
6
|
+
|
|
7
|
+
# DropRail
|
|
8
|
+
|
|
9
|
+
Every destination at once, brought to the pointer instead of the pointer being carried to
|
|
10
|
+
it.
|
|
11
|
+
|
|
12
|
+
<Canvas of={DropRailStories.Default} />
|
|
13
|
+
|
|
14
|
+
## Props
|
|
15
|
+
|
|
16
|
+
<Controls />
|
|
17
|
+
|
|
18
|
+
## The measurement this exists for
|
|
19
|
+
|
|
20
|
+
`Board` moves a card with a `Menu` on the card, and for a board that is the right control:
|
|
21
|
+
three or four lanes, all of them on screen, and the menu opens where your hand already is.
|
|
22
|
+
|
|
23
|
+
It stops being the right control at the size the fleet's real lists reach. Docket's Backlog
|
|
24
|
+
draws **34 project groups down one page**, of which about four fit on a display. Dragging a
|
|
25
|
+
card from the first group to the thirty-fourth means holding a pointer down while the page
|
|
26
|
+
scrolls under it, and a menu of 34 items is a scrolling list inside a popup — which is the
|
|
27
|
+
same problem in a smaller box.
|
|
28
|
+
|
|
29
|
+
A rail answers both halves. It pins to the top for the length of the move, so no destination
|
|
30
|
+
is ever off screen, and there is no travel: every destination is the same short distance
|
|
31
|
+
away.
|
|
32
|
+
|
|
33
|
+
<Canvas of={DropRailStories.ManyTargets} />
|
|
34
|
+
|
|
35
|
+
## One control, three drivers
|
|
36
|
+
|
|
37
|
+
WCAG 2.5.7 (AA since 2.2) requires a single-pointer alternative to any dragging movement,
|
|
38
|
+
and `FileDropZone` says the same thing about its own gesture: build the accessible path
|
|
39
|
+
first and let the drag be the enhancement.
|
|
40
|
+
|
|
41
|
+
So this is **not** a drag affordance with a fallback bolted on. It is a listbox that three
|
|
42
|
+
things drive, all committing through one `onPick`:
|
|
43
|
+
|
|
44
|
+
| Driver | Active chip | Commits on |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| Pointer | the chip under the pointer, while a button is held | release over a chip |
|
|
47
|
+
| Tap | the chip you touch | the tap |
|
|
48
|
+
| Keyboard | walked with the arrow keys | <kbd>Enter</kbd> |
|
|
49
|
+
|
|
50
|
+
The rail does not know which of the three opened it, and the host does not have to say. It
|
|
51
|
+
reads `event.buttons` on the first pointer event it sees — a button already down means a
|
|
52
|
+
drag is in flight.
|
|
53
|
+
|
|
54
|
+
<Canvas of={DropRailStories.InAList} />
|
|
55
|
+
|
|
56
|
+
## It takes no drag-and-drop dependency
|
|
57
|
+
|
|
58
|
+
The [board's decision](https://github.com/Sawtaytoes/charcuterie) priced four drag-and-drop
|
|
59
|
+
libraries and took none of them, because a published library's dependency list is inherited
|
|
60
|
+
by every consumer — including one with 60 KB gzipped to spend in total. Nothing here changes
|
|
61
|
+
that arithmetic, and this component needs far less than `Board` does: a chip is a box under
|
|
62
|
+
the pointer, so `elementFromPoint` answers the only geometric question there is. No
|
|
63
|
+
snapshot, no collision strategy, no sortable transform.
|
|
64
|
+
|
|
65
|
+
## It is not `Rail`
|
|
66
|
+
|
|
67
|
+
`Rail` is the app shell's side rail — a landmark, an `<aside>` or a `<nav>`, permanent
|
|
68
|
+
furniture down one edge of a page. `DropRail` is a horizontal strip of drop targets that
|
|
69
|
+
exists only for the length of a move, and it is a `listbox`. They share four letters and
|
|
70
|
+
nothing else; `FileDropZone` is the precedent for the prefix carrying the distinction.
|
|
71
|
+
|
|
72
|
+
## The three chip states
|
|
73
|
+
|
|
74
|
+
<Canvas of={DropRailStories.AllStates} />
|
|
75
|
+
|
|
76
|
+
The destination the item is **already in** stays visible and unofferable rather than being
|
|
77
|
+
filtered out. A rail whose contents change depending on what you grabbed is a rail whose
|
|
78
|
+
chips move under the pointer between one move and the next, and muscle memory is the only
|
|
79
|
+
reason this control is faster than a menu.
|
|
80
|
+
|
|
81
|
+
Neither that state nor the disabled one is marked by colour alone. The current chip carries
|
|
82
|
+
visually hidden text — *"— where it is now"* — and both carry `aria-disabled`, because the
|
|
83
|
+
one chip a person must not aim for is exactly the one that must not rely on being dimmer
|
|
84
|
+
(WCAG 1.4.1).
|
|
85
|
+
|
|
86
|
+
## What it deliberately does not do
|
|
87
|
+
|
|
88
|
+
It does not render the thing being dragged, own the handle, or know what a move means. The
|
|
89
|
+
host keeps all three, which is what lets the same rail sit above a board, a card grid, or a
|
|
90
|
+
list of accordions without any of them being modelled here.
|