@charcuterie/ui 2.18.0 → 3.1.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 +10 -0
- package/dist/Badge/Badge.d.ts +33 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +20 -2
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Board/Board.d.ts +119 -0
- package/dist/Board/Board.d.ts.map +1 -0
- package/dist/Board/Board.js +165 -0
- package/dist/Board/Board.js.map +1 -0
- package/dist/Board/BoardCard.d.ts +120 -0
- package/dist/Board/BoardCard.d.ts.map +1 -0
- package/dist/Board/BoardCard.js +77 -0
- package/dist/Board/BoardCard.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +103 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -0
- package/dist/Board/BoardLaneList.js +69 -0
- package/dist/Board/BoardLaneList.js.map +1 -0
- package/dist/Board/boardMove.d.ts +116 -0
- package/dist/Board/boardMove.d.ts.map +1 -0
- package/dist/Board/boardMove.js +116 -0
- package/dist/Board/boardMove.js.map +1 -0
- package/dist/Board/useBoardDrag.d.ts +44 -0
- package/dist/Board/useBoardDrag.d.ts.map +1 -0
- package/dist/Board/useBoardDrag.js +237 -0
- package/dist/Board/useBoardDrag.js.map +1 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +15 -4
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/dist/DataTable/DataTable.d.ts +159 -0
- package/dist/DataTable/DataTable.d.ts.map +1 -0
- package/dist/DataTable/DataTable.js +197 -0
- package/dist/DataTable/DataTable.js.map +1 -0
- package/dist/DatePicker/DateGrid.d.ts +79 -0
- package/dist/DatePicker/DateGrid.d.ts.map +1 -0
- package/dist/DatePicker/DateGrid.js +220 -0
- package/dist/DatePicker/DateGrid.js.map +1 -0
- package/dist/DatePicker/DatePicker.d.ts +138 -0
- package/dist/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/DatePicker/DatePicker.js +384 -0
- package/dist/DatePicker/DatePicker.js.map +1 -0
- package/dist/DatePicker/datePresets.d.ts +29 -0
- package/dist/DatePicker/datePresets.d.ts.map +1 -0
- package/dist/DatePicker/datePresets.js +28 -0
- package/dist/DatePicker/datePresets.js.map +1 -0
- package/dist/DatePicker/parseDateInput.d.ts +106 -0
- package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
- package/dist/DatePicker/parseDateInput.js +419 -0
- package/dist/DatePicker/parseDateInput.js.map +1 -0
- package/dist/DatePicker/plainDate.d.ts +167 -0
- package/dist/DatePicker/plainDate.d.ts.map +1 -0
- package/dist/DatePicker/plainDate.js +272 -0
- package/dist/DatePicker/plainDate.js.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.js +379 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.js +303 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
- package/dist/Select/Select.d.ts +36 -1
- package/dist/Select/Select.d.ts.map +1 -1
- package/dist/Select/Select.js +38 -2
- package/dist/Select/Select.js.map +1 -1
- package/dist/categoricalStyles.d.ts +53 -0
- package/dist/categoricalStyles.d.ts.map +1 -0
- package/dist/categoricalStyles.js +195 -0
- package/dist/categoricalStyles.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Badge/Badge.mdx +46 -0
- package/src/Badge/Badge.stories.tsx +98 -4
- package/src/Badge/Badge.test.tsx +157 -2
- package/src/Badge/Badge.tsx +49 -3
- package/src/Board/Board.mdx +199 -0
- package/src/Board/Board.stories.tsx +707 -0
- package/src/Board/Board.test.tsx +310 -0
- package/src/Board/Board.tsx +333 -0
- package/src/Board/BoardCard.tsx +290 -0
- package/src/Board/BoardLaneList.tsx +275 -0
- package/src/Board/boardMove.test.ts +213 -0
- package/src/Board/boardMove.ts +225 -0
- package/src/Board/useBoardDrag.ts +413 -0
- package/src/Checkbox/Checkbox.tsx +34 -3
- package/src/DataTable/DataTable.mdx +123 -0
- package/src/DataTable/DataTable.stories.tsx +411 -0
- package/src/DataTable/DataTable.test.tsx +386 -0
- package/src/DataTable/DataTable.tsx +497 -0
- package/src/DatePicker/DateGrid.tsx +467 -0
- package/src/DatePicker/DatePicker.mdx +170 -0
- package/src/DatePicker/DatePicker.stories.tsx +268 -0
- package/src/DatePicker/DatePicker.test.tsx +455 -0
- package/src/DatePicker/DatePicker.tsx +852 -0
- package/src/DatePicker/datePresets.ts +34 -0
- package/src/DatePicker/parseDateInput.test.ts +187 -0
- package/src/DatePicker/parseDateInput.ts +629 -0
- package/src/DatePicker/plainDate.test.ts +375 -0
- package/src/DatePicker/plainDate.ts +465 -0
- package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
- package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
- package/src/MarkdownEditor/markdownCommands.ts +566 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
- package/src/MarkdownEditor/markdownSpans.ts +458 -0
- package/src/Select/Select.mdx +44 -0
- package/src/Select/Select.stories.tsx +55 -0
- package/src/Select/Select.test.tsx +89 -1
- package/src/Select/Select.tsx +58 -2
- package/src/argTypes.storyHelpers.ts +39 -0
- package/src/categoricalStyles.ts +244 -0
- package/src/index.ts +89 -0
- package/src/sourceRules.test.ts +52 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
BoardDropLane,
|
|
5
|
+
BoardRect,
|
|
6
|
+
} from "./boardMove.ts"
|
|
7
|
+
import {
|
|
8
|
+
chooseDropIndex,
|
|
9
|
+
chooseDropTarget,
|
|
10
|
+
describeMove,
|
|
11
|
+
getIsMoveMeaningful,
|
|
12
|
+
toSettledIndex,
|
|
13
|
+
} from "./boardMove.ts"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The geometry, in Node, against fixtures — because the browser
|
|
17
|
+
* cannot test it honestly.
|
|
18
|
+
*
|
|
19
|
+
* `userEvent` will dispatch every `pointermove` you ask it for, but
|
|
20
|
+
* nothing in a `jsdom`-shaped world has laid out, and even in the
|
|
21
|
+
* real chromium the test canvas is mounted, measured and torn down
|
|
22
|
+
* inside one frame. Every rectangle the drop calculation would
|
|
23
|
+
* reason about is plausible-looking garbage, so a browser test of
|
|
24
|
+
* "did it pick the right lane" is a test of whether two zeroes
|
|
25
|
+
* compare equal.
|
|
26
|
+
*
|
|
27
|
+
* Split out, the rule is exact: these are the numbers, this is the
|
|
28
|
+
* answer, including the ties.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const toRect = (
|
|
32
|
+
top: number,
|
|
33
|
+
height: number,
|
|
34
|
+
): BoardRect => ({
|
|
35
|
+
height,
|
|
36
|
+
left: 0,
|
|
37
|
+
top,
|
|
38
|
+
width: 100,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
/** Three cards, 40px each, stacked from y=0. */
|
|
42
|
+
const CARD_RECTS: BoardRect[] = [
|
|
43
|
+
toRect(0, 40),
|
|
44
|
+
toRect(40, 40),
|
|
45
|
+
toRect(80, 40),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const LANES: BoardDropLane[] = [
|
|
49
|
+
{
|
|
50
|
+
cardRects: CARD_RECTS,
|
|
51
|
+
key: "todo",
|
|
52
|
+
rect: { height: 200, left: 0, top: 0, width: 100 },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
cardRects: [toRect(0, 40)],
|
|
56
|
+
key: "doing",
|
|
57
|
+
rect: { height: 200, left: 120, top: 0, width: 100 },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
cardRects: [],
|
|
61
|
+
key: "done",
|
|
62
|
+
rect: { height: 200, left: 240, top: 0, width: 100 },
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
test("the insertion point flips at a card's midpoint, not its edge", () => {
|
|
67
|
+
// Above the first midpoint: before everything.
|
|
68
|
+
expect(chooseDropIndex(CARD_RECTS, { x: 10, y: 5 })).toBe(
|
|
69
|
+
0,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// Still in the first card, but past its centre: after it.
|
|
73
|
+
expect(
|
|
74
|
+
chooseDropIndex(CARD_RECTS, { x: 10, y: 25 }),
|
|
75
|
+
).toBe(1)
|
|
76
|
+
|
|
77
|
+
expect(
|
|
78
|
+
chooseDropIndex(CARD_RECTS, { x: 10, y: 65 }),
|
|
79
|
+
).toBe(2)
|
|
80
|
+
|
|
81
|
+
// Below everything.
|
|
82
|
+
expect(
|
|
83
|
+
chooseDropIndex(CARD_RECTS, { x: 10, y: 300 }),
|
|
84
|
+
).toBe(3)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("exactly on a midpoint counts as before, not after", () => {
|
|
88
|
+
// The tie, stated rather than discovered. `>` and not `>=`, so a
|
|
89
|
+
// pointer resting precisely on the centre line does not oscillate
|
|
90
|
+
// between two indices as the last sub-pixel rounds.
|
|
91
|
+
expect(
|
|
92
|
+
chooseDropIndex(CARD_RECTS, { x: 10, y: 20 }),
|
|
93
|
+
).toBe(0)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("an empty lane accepts a drop at index 0", () => {
|
|
97
|
+
expect(
|
|
98
|
+
chooseDropTarget(LANES, { x: 250, y: 100 }),
|
|
99
|
+
).toEqual({ index: 0, laneKey: "done" })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("the pointer picks the lane it is inside", () => {
|
|
103
|
+
expect(chooseDropTarget(LANES, { x: 10, y: 90 })).toEqual(
|
|
104
|
+
{ index: 2, laneKey: "todo" },
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect(
|
|
108
|
+
chooseDropTarget(LANES, { x: 130, y: 10 }),
|
|
109
|
+
).toEqual({ index: 0, laneKey: "doing" })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("a release just outside a lane still lands in the nearest one", () => {
|
|
113
|
+
// Two pixels past the edge is a drop, not a cancel. Returning
|
|
114
|
+
// `null` here is how a board earns "it sometimes just puts the
|
|
115
|
+
// card back".
|
|
116
|
+
expect(
|
|
117
|
+
chooseDropTarget(LANES, { x: 105, y: 100 }),
|
|
118
|
+
).toEqual({ index: 2, laneKey: "todo" })
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
chooseDropTarget(LANES, { x: 400, y: 100 }),
|
|
122
|
+
).toEqual({ index: 0, laneKey: "done" })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("there is no target when there are no lanes", () => {
|
|
126
|
+
expect(chooseDropTarget([], { x: 0, y: 0 })).toBeNull()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("dropping a card back where it was is not a move", () => {
|
|
130
|
+
// Both sides of its own midpoint resolve to "no change", because
|
|
131
|
+
// removing the card shifts everything below it up by one.
|
|
132
|
+
expect(
|
|
133
|
+
getIsMoveMeaningful({
|
|
134
|
+
fromIndex: 2,
|
|
135
|
+
fromLaneKey: "todo",
|
|
136
|
+
toIndex: 2,
|
|
137
|
+
toLaneKey: "todo",
|
|
138
|
+
}),
|
|
139
|
+
).toBe(false)
|
|
140
|
+
|
|
141
|
+
expect(
|
|
142
|
+
getIsMoveMeaningful({
|
|
143
|
+
fromIndex: 2,
|
|
144
|
+
fromLaneKey: "todo",
|
|
145
|
+
toIndex: 3,
|
|
146
|
+
toLaneKey: "todo",
|
|
147
|
+
}),
|
|
148
|
+
).toBe(false)
|
|
149
|
+
|
|
150
|
+
expect(
|
|
151
|
+
getIsMoveMeaningful({
|
|
152
|
+
fromIndex: 2,
|
|
153
|
+
fromLaneKey: "todo",
|
|
154
|
+
toIndex: 4,
|
|
155
|
+
toLaneKey: "todo",
|
|
156
|
+
}),
|
|
157
|
+
).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("a cross-lane move to the same index is always meaningful", () => {
|
|
161
|
+
expect(
|
|
162
|
+
getIsMoveMeaningful({
|
|
163
|
+
fromIndex: 1,
|
|
164
|
+
fromLaneKey: "todo",
|
|
165
|
+
toIndex: 1,
|
|
166
|
+
toLaneKey: "doing",
|
|
167
|
+
}),
|
|
168
|
+
).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test("a downward within-lane index is corrected for the removal", () => {
|
|
172
|
+
expect(
|
|
173
|
+
toSettledIndex({
|
|
174
|
+
fromIndex: 0,
|
|
175
|
+
fromLaneKey: "todo",
|
|
176
|
+
toIndex: 3,
|
|
177
|
+
toLaneKey: "todo",
|
|
178
|
+
}),
|
|
179
|
+
).toBe(2)
|
|
180
|
+
|
|
181
|
+
// Upward inside the lane, and any cross-lane move, are already
|
|
182
|
+
// quoted against the list the card is not in.
|
|
183
|
+
expect(
|
|
184
|
+
toSettledIndex({
|
|
185
|
+
fromIndex: 3,
|
|
186
|
+
fromLaneKey: "todo",
|
|
187
|
+
toIndex: 1,
|
|
188
|
+
toLaneKey: "todo",
|
|
189
|
+
}),
|
|
190
|
+
).toBe(1)
|
|
191
|
+
|
|
192
|
+
expect(
|
|
193
|
+
toSettledIndex({
|
|
194
|
+
fromIndex: 0,
|
|
195
|
+
fromLaneKey: "todo",
|
|
196
|
+
toIndex: 3,
|
|
197
|
+
toLaneKey: "doing",
|
|
198
|
+
}),
|
|
199
|
+
).toBe(3)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test("the announcement names the destination and the position, one-based", () => {
|
|
203
|
+
expect(
|
|
204
|
+
describeMove({
|
|
205
|
+
index: 2,
|
|
206
|
+
laneLabel: "In Progress",
|
|
207
|
+
laneSize: 6,
|
|
208
|
+
title: "Unify the page chrome",
|
|
209
|
+
}),
|
|
210
|
+
).toBe(
|
|
211
|
+
"Moved Unify the page chrome to In Progress, position 3 of 6.",
|
|
212
|
+
)
|
|
213
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything about a board move that is arithmetic rather than
|
|
3
|
+
* React — so it can be tested in Node, exhaustively, without a
|
|
4
|
+
* browser and without a pointer.
|
|
5
|
+
*
|
|
6
|
+
* That split is not tidiness. The pointer path is the one part of
|
|
7
|
+
* this component a test cannot drive convincingly: `userEvent` can
|
|
8
|
+
* dispatch `pointermove`, but it cannot lay anything out, so every
|
|
9
|
+
* rectangle it would reason about is `0x0` in a test environment
|
|
10
|
+
* that has not painted. Pulling the geometry out means the *rule* —
|
|
11
|
+
* which lane, which index, what happens on a tie — is red/green in
|
|
12
|
+
* Node, and the browser test is left proving only the wiring.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A lane as the drop calculation sees it: a box, a key, and the
|
|
17
|
+
* boxes of the cards inside it.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately plain data rather than elements. `chooseDropTarget`
|
|
20
|
+
* never touches the DOM, which is why it can be called with
|
|
21
|
+
* fixtures.
|
|
22
|
+
*/
|
|
23
|
+
export type BoardDropLane = {
|
|
24
|
+
cardRects: readonly BoardRect[]
|
|
25
|
+
key: string
|
|
26
|
+
rect: BoardRect
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type BoardPoint = {
|
|
30
|
+
x: number
|
|
31
|
+
y: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The subset of `DOMRect` this needs. Named rather than reusing
|
|
36
|
+
* `DOMRect`, because `DOMRect` is a browser global and this module
|
|
37
|
+
* is imported by the Node test project.
|
|
38
|
+
*/
|
|
39
|
+
export type BoardRect = {
|
|
40
|
+
height: number
|
|
41
|
+
left: number
|
|
42
|
+
top: number
|
|
43
|
+
width: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type BoardDropTarget = {
|
|
47
|
+
index: number
|
|
48
|
+
laneKey: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `left`/`top` rather than `insetInlineStart`, and that is not a
|
|
53
|
+
* violation of the logical-properties rule.
|
|
54
|
+
*
|
|
55
|
+
* The rule is scoped to `className` literals, because a Tailwind
|
|
56
|
+
* utility is a *style* and a style has a writing direction.
|
|
57
|
+
* `getBoundingClientRect()` returns physical pixels in the
|
|
58
|
+
* viewport's own coordinate space; there is no logical version of it
|
|
59
|
+
* to prefer, and inventing one here would mean re-deriving a number
|
|
60
|
+
* the browser already gave us. The lint rule carves this out
|
|
61
|
+
* explicitly.
|
|
62
|
+
*/
|
|
63
|
+
const getIsPointInside = (
|
|
64
|
+
rect: BoardRect,
|
|
65
|
+
point: BoardPoint,
|
|
66
|
+
): boolean =>
|
|
67
|
+
point.x >= rect.left &&
|
|
68
|
+
point.x <= rect.left + rect.width &&
|
|
69
|
+
point.y >= rect.top &&
|
|
70
|
+
point.y <= rect.top + rect.height
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* How far a point is from a rectangle, zero when it is inside.
|
|
74
|
+
*
|
|
75
|
+
* Used only as a fallback, and the fallback is the reason a drag
|
|
76
|
+
* feels finished rather than cancelled: a pointer released two
|
|
77
|
+
* pixels past a lane's edge is a drop *on that lane* as far as the
|
|
78
|
+
* person doing it is concerned, and returning `null` there is how a
|
|
79
|
+
* board earns "it just puts the card back sometimes".
|
|
80
|
+
*/
|
|
81
|
+
const getDistanceToRect = (
|
|
82
|
+
rect: BoardRect,
|
|
83
|
+
point: BoardPoint,
|
|
84
|
+
): number => {
|
|
85
|
+
const dx = Math.max(
|
|
86
|
+
rect.left - point.x,
|
|
87
|
+
0,
|
|
88
|
+
point.x - (rect.left + rect.width),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const dy = Math.max(
|
|
92
|
+
rect.top - point.y,
|
|
93
|
+
0,
|
|
94
|
+
point.y - (rect.top + rect.height),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Where in a lane a point lands: the number of cards whose midpoint
|
|
102
|
+
* is above it.
|
|
103
|
+
*
|
|
104
|
+
* Midpoints rather than edges, so the insertion flips when the
|
|
105
|
+
* pointer passes the *centre* of a card. Against edges, the top
|
|
106
|
+
* half of every card is a dead zone that means "same place you
|
|
107
|
+
* already were", and the indicator stutters.
|
|
108
|
+
*/
|
|
109
|
+
export const chooseDropIndex = (
|
|
110
|
+
cardRects: readonly BoardRect[],
|
|
111
|
+
point: BoardPoint,
|
|
112
|
+
): number =>
|
|
113
|
+
cardRects.filter(
|
|
114
|
+
(rect) => point.y > rect.top + rect.height / 2,
|
|
115
|
+
).length
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The lane and index a pointer is currently over.
|
|
119
|
+
*
|
|
120
|
+
* Hit test first, nearest-lane second, and `null` only when there
|
|
121
|
+
* are no lanes at all — a drag that started has to end somewhere.
|
|
122
|
+
*/
|
|
123
|
+
export const chooseDropTarget = (
|
|
124
|
+
lanes: readonly BoardDropLane[],
|
|
125
|
+
point: BoardPoint,
|
|
126
|
+
): BoardDropTarget | null => {
|
|
127
|
+
if (lanes.length === 0) {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hitLane = lanes.find((lane) =>
|
|
132
|
+
getIsPointInside(lane.rect, point),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const lane =
|
|
136
|
+
hitLane ??
|
|
137
|
+
lanes.reduce((nearest, candidate) =>
|
|
138
|
+
getDistanceToRect(candidate.rect, point) <
|
|
139
|
+
getDistanceToRect(nearest.rect, point)
|
|
140
|
+
? candidate
|
|
141
|
+
: nearest,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
index: chooseDropIndex(lane.cardRects, point),
|
|
146
|
+
laneKey: lane.key,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether a move would actually change anything.
|
|
152
|
+
*
|
|
153
|
+
* The subtle half is `toIndex`. Taking a card out of its own lane
|
|
154
|
+
* shifts every card below it up by one, so "drop it back where it
|
|
155
|
+
* was" arrives as both `fromIndex` and `fromIndex + 1` depending on
|
|
156
|
+
* which side of its own midpoint the pointer ended on. Both are
|
|
157
|
+
* no-ops, and a board that fires `onMove` for them makes a consumer
|
|
158
|
+
* re-render — and, if the consumer is optimistic, flash — for a
|
|
159
|
+
* drag that went nowhere.
|
|
160
|
+
*/
|
|
161
|
+
export const getIsMoveMeaningful = ({
|
|
162
|
+
fromIndex,
|
|
163
|
+
fromLaneKey,
|
|
164
|
+
toIndex,
|
|
165
|
+
toLaneKey,
|
|
166
|
+
}: {
|
|
167
|
+
fromIndex: number
|
|
168
|
+
fromLaneKey: string
|
|
169
|
+
toIndex: number
|
|
170
|
+
toLaneKey: string
|
|
171
|
+
}): boolean =>
|
|
172
|
+
fromLaneKey !== toLaneKey ||
|
|
173
|
+
(toIndex !== fromIndex && toIndex !== fromIndex + 1)
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Where a card ends up once it has been removed from its old
|
|
177
|
+
* position.
|
|
178
|
+
*
|
|
179
|
+
* A within-lane move is quoted against the list *including* the
|
|
180
|
+
* card being moved — that is what the pointer was over — but a
|
|
181
|
+
* consumer splicing an array removes it first, so every index below
|
|
182
|
+
* the origin is one too high. Folding that here means the consumer's
|
|
183
|
+
* handler is `splice(from, 1)` then `splice(to, 0, item)` with no
|
|
184
|
+
* arithmetic of its own, which is the version nobody gets wrong.
|
|
185
|
+
*/
|
|
186
|
+
export const toSettledIndex = ({
|
|
187
|
+
fromIndex,
|
|
188
|
+
fromLaneKey,
|
|
189
|
+
toIndex,
|
|
190
|
+
toLaneKey,
|
|
191
|
+
}: {
|
|
192
|
+
fromIndex: number
|
|
193
|
+
fromLaneKey: string
|
|
194
|
+
toIndex: number
|
|
195
|
+
toLaneKey: string
|
|
196
|
+
}): number =>
|
|
197
|
+
fromLaneKey === toLaneKey && toIndex > fromIndex
|
|
198
|
+
? toIndex - 1
|
|
199
|
+
: toIndex
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* What the live region says after a move.
|
|
203
|
+
*
|
|
204
|
+
* A sentence rather than a status word, and it names the
|
|
205
|
+
* **destination position** as well as the lane, because "moved to In
|
|
206
|
+
* Progress" leaves a screen-reader user with no idea whether the
|
|
207
|
+
* card went to the top of the lane or the bottom of thirty. That is
|
|
208
|
+
* the single piece of feedback a sighted user gets for free from
|
|
209
|
+
* watching the card land, and it is the one every drag-and-drop
|
|
210
|
+
* implementation forgets to say out loud.
|
|
211
|
+
*
|
|
212
|
+
* One-based, because it is read to a person.
|
|
213
|
+
*/
|
|
214
|
+
export const describeMove = ({
|
|
215
|
+
index,
|
|
216
|
+
laneLabel,
|
|
217
|
+
laneSize,
|
|
218
|
+
title,
|
|
219
|
+
}: {
|
|
220
|
+
index: number
|
|
221
|
+
laneLabel: string
|
|
222
|
+
laneSize: number
|
|
223
|
+
title: string
|
|
224
|
+
}): string =>
|
|
225
|
+
`Moved ${title} to ${laneLabel}, position ${index + 1} of ${laneSize}.`
|