@react-x11/components 0.4.0 → 0.5.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 +125 -4
- package/dist/charts/components.d.ts +3 -1
- package/dist/charts/components.d.ts.map +1 -1
- package/dist/charts/components.js +8 -4
- package/dist/charts/components.js.map +1 -1
- package/dist/charts/node.d.ts +13 -6
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +75 -24
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +4 -0
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +11 -6
- package/dist/charts/render.js.map +1 -1
- package/dist/code-editor/node.d.ts +47 -16
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +121 -51
- package/dist/code-editor/node.js.map +1 -1
- package/dist/flow/draw.d.ts +19 -1
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +64 -35
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +24 -8
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/model.d.ts +4 -2
- package/dist/flow/model.d.ts.map +1 -1
- package/dist/flow/model.js +4 -2
- package/dist/flow/model.js.map +1 -1
- package/dist/flow/node.d.ts +40 -9
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +142 -53
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/types.d.ts +46 -21
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/formula/index.d.ts +4 -2
- package/dist/formula/index.d.ts.map +1 -1
- package/dist/formula/index.js.map +1 -1
- package/dist/formula/node.d.ts +5 -1
- package/dist/formula/node.d.ts.map +1 -1
- package/dist/formula/node.js +19 -3
- package/dist/formula/node.js.map +1 -1
- package/dist/html/controls.d.ts +4 -1
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +11 -5
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +18 -8
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +15 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +41 -16
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +3 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3 -1
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/values.d.ts +14 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +11 -8
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +1 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +12 -5
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +7 -4
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +22 -1
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +22 -8
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +97 -15
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +3 -0
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +2 -2
- package/dist/html/paint.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/maps/clip.d.ts +43 -0
- package/dist/maps/clip.d.ts.map +1 -0
- package/dist/maps/clip.js +171 -0
- package/dist/maps/clip.js.map +1 -0
- package/dist/maps/gzip.d.ts +15 -0
- package/dist/maps/gzip.d.ts.map +1 -0
- package/dist/maps/gzip.js +56 -0
- package/dist/maps/gzip.js.map +1 -0
- package/dist/maps/index.d.ts +55 -0
- package/dist/maps/index.d.ts.map +1 -0
- package/dist/maps/index.js +141 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/labels.d.ts +123 -0
- package/dist/maps/labels.d.ts.map +1 -0
- package/dist/maps/labels.js +323 -0
- package/dist/maps/labels.js.map +1 -0
- package/dist/maps/mvt.d.ts +244 -0
- package/dist/maps/mvt.d.ts.map +1 -0
- package/dist/maps/mvt.js +820 -0
- package/dist/maps/mvt.js.map +1 -0
- package/dist/maps/node.d.ts +278 -0
- package/dist/maps/node.d.ts.map +1 -0
- package/dist/maps/node.js +1459 -0
- package/dist/maps/node.js.map +1 -0
- package/dist/maps/overlay.d.ts +157 -0
- package/dist/maps/overlay.d.ts.map +1 -0
- package/dist/maps/overlay.js +472 -0
- package/dist/maps/overlay.js.map +1 -0
- package/dist/maps/paint.d.ts +226 -0
- package/dist/maps/paint.d.ts.map +1 -0
- package/dist/maps/paint.js +705 -0
- package/dist/maps/paint.js.map +1 -0
- package/dist/maps/proj.d.ts +281 -0
- package/dist/maps/proj.d.ts.map +1 -0
- package/dist/maps/proj.js +451 -0
- package/dist/maps/proj.js.map +1 -0
- package/dist/maps/sources.d.ts +256 -0
- package/dist/maps/sources.d.ts.map +1 -0
- package/dist/maps/sources.js +210 -0
- package/dist/maps/sources.js.map +1 -0
- package/dist/maps/style.d.ts +141 -0
- package/dist/maps/style.d.ts.map +1 -0
- package/dist/maps/style.js +147 -0
- package/dist/maps/style.js.map +1 -0
- package/dist/maps/styles.d.ts +82 -0
- package/dist/maps/styles.d.ts.map +1 -0
- package/dist/maps/styles.js +898 -0
- package/dist/maps/styles.js.map +1 -0
- package/dist/maps/tiles.d.ts +283 -0
- package/dist/maps/tiles.d.ts.map +1 -0
- package/dist/maps/tiles.js +469 -0
- package/dist/maps/tiles.js.map +1 -0
- package/dist/maps/types.d.ts +244 -0
- package/dist/maps/types.d.ts.map +1 -0
- package/dist/maps/types.js +2 -0
- package/dist/maps/types.js.map +1 -0
- package/dist/reorder/hx.d.ts +18 -0
- package/dist/reorder/hx.d.ts.map +1 -0
- package/dist/reorder/hx.js +10 -0
- package/dist/reorder/hx.js.map +1 -0
- package/dist/reorder/index.d.ts +314 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +1539 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/model.d.ts +107 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +162 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/richtext/node.d.ts +15 -5
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +40 -11
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +35 -13
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +83 -27
- package/dist/richtext/runs.js.map +1 -1
- package/dist/tabs/index.d.ts +17 -0
- package/dist/tabs/index.d.ts.map +1 -1
- package/dist/tabs/index.js +849 -186
- package/dist/tabs/index.js.map +1 -1
- package/dist/terminal/vt/fonts.d.ts +45 -15
- package/dist/terminal/vt/fonts.d.ts.map +1 -1
- package/dist/terminal/vt/fonts.js +89 -19
- package/dist/terminal/vt/fonts.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +42 -0
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +95 -11
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +13 -3
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/package.json +13 -3
- package/src/charts/components.ts +14 -7
- package/src/charts/node.ts +81 -26
- package/src/charts/render.ts +16 -6
- package/src/code-editor/node.ts +147 -53
- package/src/flow/draw.ts +104 -35
- package/src/flow/index.ts +33 -13
- package/src/flow/model.ts +4 -2
- package/src/flow/node.ts +158 -59
- package/src/flow/types.ts +46 -21
- package/src/formula/index.ts +4 -2
- package/src/formula/node.ts +24 -5
- package/src/html/controls.ts +4 -1
- package/src/html/css/cascade.ts +18 -7
- package/src/html/css/parse.ts +46 -21
- package/src/html/css/style.ts +6 -1
- package/src/html/css/values.ts +25 -9
- package/src/html/index.ts +1 -0
- package/src/html/layout/boxes.ts +19 -9
- package/src/html/layout/inline.ts +23 -1
- package/src/html/node.ts +110 -17
- package/src/html/paint.ts +17 -2
- package/src/index.ts +98 -0
- package/src/maps/clip.ts +184 -0
- package/src/maps/gzip.ts +66 -0
- package/src/maps/index.ts +316 -0
- package/src/maps/labels.ts +442 -0
- package/src/maps/mvt.ts +857 -0
- package/src/maps/node.ts +1819 -0
- package/src/maps/overlay.ts +639 -0
- package/src/maps/paint.ts +978 -0
- package/src/maps/proj.ts +641 -0
- package/src/maps/sources.ts +425 -0
- package/src/maps/style.ts +309 -0
- package/src/maps/styles.ts +1040 -0
- package/src/maps/tiles.ts +676 -0
- package/src/maps/types.ts +260 -0
- package/src/reorder/hx.ts +31 -0
- package/src/reorder/index.ts +2223 -0
- package/src/reorder/model.ts +232 -0
- package/src/richtext/node.ts +49 -14
- package/src/richtext/runs.ts +115 -30
- package/src/tabs/index.ts +1040 -204
- package/src/terminal/vt/fonts.ts +128 -36
- package/src/terminal/vt/node.ts +105 -12
- package/src/terminal/vt/renderer.ts +13 -3
|
@@ -0,0 +1,1539 @@
|
|
|
1
|
+
// <ReorderList> — a list the user reorders by dragging its items, or by
|
|
2
|
+
// lifting one from the keyboard and walking it with the arrows: a todo list,
|
|
3
|
+
// a playlist, a settings dialog's columns, a kanban board when several lists
|
|
4
|
+
// share a `group`.
|
|
5
|
+
//
|
|
6
|
+
// This is the **sortable layer over react-x11's own drag and drop**, the
|
|
7
|
+
// position `@dnd-kit/sortable` holds over `@dnd-kit/core` — except that the
|
|
8
|
+
// engine is core's (docs/drag-and-drop.md there): the 4px threshold, click
|
|
9
|
+
// suppression, `:dragging`/`:drag-over`, the in-app payload arriving by
|
|
10
|
+
// reference on `e.items`, edge auto-scroll of any scroll container, the
|
|
11
|
+
// `<popup dragPreview>` a preview is drawn in, and the promotion of the same
|
|
12
|
+
// drag to XDND — or, on the cocoa backend, to NSDragging — when the pointer
|
|
13
|
+
// leaves the app. None of that is here. What is here is what a sortable
|
|
14
|
+
// preset is: the insertion arithmetic (`./model.ts`), the indicator, the
|
|
15
|
+
// keyboard model, and an event vocabulary that speaks list-and-index rather
|
|
16
|
+
// than node-and-pointer.
|
|
17
|
+
//
|
|
18
|
+
// `docs/prd-reorder.md` is the design record — the survey of dnd-kit,
|
|
19
|
+
// hello-pangea, pragmatic-drag-and-drop, React Aria and Framer's Reorder,
|
|
20
|
+
// and what each settled. The decisions a reader of this file would not
|
|
21
|
+
// guess, each with its paragraph there:
|
|
22
|
+
//
|
|
23
|
+
// - **The list is the only drop target; items are not.** One `dropAccept`
|
|
24
|
+
// on the root, one `onDragOver` that reads every item's rectangle and
|
|
25
|
+
// finds the closest edge, and the two items either side of the slot are
|
|
26
|
+
// told to draw or clear their indicator through a per-item setter — so a
|
|
27
|
+
// pointer motion re-renders at most two items and never the list.
|
|
28
|
+
// - **Membership is in the payload's type name.** Lists in a group accept
|
|
29
|
+
// `application/x-react-x11-reorder;scope=group:<name>`; a list on its own
|
|
30
|
+
// accepts `…scope=list:<its uid>`. That keeps "who takes this drop" a
|
|
31
|
+
// declarative fact core answers from `dropAccept` data with no React in
|
|
32
|
+
// the loop — and it has to be, because `e.accept()` in `onDragOver` can
|
|
33
|
+
// only override a node whose `dropAccept` already matched. `canDrop` is
|
|
34
|
+
// the *dynamic* half, and it works the other way round: it can only
|
|
35
|
+
// refuse (`ev.reject()`), which is a decision core lets a handler make.
|
|
36
|
+
// - **A move between lists is two local handlers**, `onInsert` on the list
|
|
37
|
+
// it landed in and `onRemove` on the list it left — React Aria's model,
|
|
38
|
+
// and the order core already fires them in (`onDrop`, then `onDragEnd`
|
|
39
|
+
// "always last"). The target writes where the item landed onto the live
|
|
40
|
+
// payload object, which is how the source can say `to`.
|
|
41
|
+
// - **The hover channel is module state, and it has to be.** `onDragOver`
|
|
42
|
+
// hands the target a `DragEvent`, which carries no `items` — only the
|
|
43
|
+
// drop does — so a target physically cannot write on the payload while
|
|
44
|
+
// the pointer is merely over it. `activeDrag` below is the one-gesture
|
|
45
|
+
// scratchpad the target writes and the source reads in its own `onDrag`,
|
|
46
|
+
// which is what makes `onDragUpdate` able to say *which list* the pointer
|
|
47
|
+
// is over. It is one object for one gesture, never read at import time.
|
|
48
|
+
// - **The indicator, not the slide.** Neighbours do not glide out of the
|
|
49
|
+
// way: this renderer has no transform, and a line at the closest edge
|
|
50
|
+
// answers the same question for a rectangle per item. The one thing that
|
|
51
|
+
// does move is the drop: `dropAnimation` flies a copy of the item from
|
|
52
|
+
// where the pointer let go to where the item landed. That copy is a box
|
|
53
|
+
// **inside the item**, not the ghost popup carried on — a popup outlives
|
|
54
|
+
// the gesture as a real window over the list, and the press that follows
|
|
55
|
+
// a drop by less than the animation lands on it instead of on the list.
|
|
56
|
+
// An absolutely positioned box with `pointerEvents: 'none'` cannot take
|
|
57
|
+
// an input the list should have had.
|
|
58
|
+
// - **The ghost popup is `transparent`.** It is a real window, and the card
|
|
59
|
+
// inside it is rounded, so on an opaque popup the four corners the radius
|
|
60
|
+
// gives up show the window's own ground — white on a light theme, near
|
|
61
|
+
// black on a dark one. Where no compositor runs the window fills itself
|
|
62
|
+
// square, which is what an opaque one looked like anyway.
|
|
63
|
+
// - **A drag's events bubble, so an item must check the source is its own.**
|
|
64
|
+
// `DragStart`, `onDrag` and `onDragEnd` dispatch capture → target →
|
|
65
|
+
// bubble like every other event, which means a list item that *contains*
|
|
66
|
+
// the dragged one — a board's column, holding the list the card is in —
|
|
67
|
+
// sees them all, and `useDragSource` inside it dutifully sets a position
|
|
68
|
+
// and would draw a second ghost. Worse, its handlers would claim the
|
|
69
|
+
// gesture: a second `dragStarted` on the outer list, and `activeDrag`
|
|
70
|
+
// overwritten with the column's payload, so the hover channel described
|
|
71
|
+
// the wrong item. Every source callback here therefore begins by asking
|
|
72
|
+
// whether the event's target is this item's own drag source (itself, or
|
|
73
|
+
// its handle) and returns if not. `preventDefault` is not the tool for
|
|
74
|
+
// that: on this event it cancels the whole drag.
|
|
75
|
+
// - **An in-window ghost lifts the whole path, not just its item.**
|
|
76
|
+
// `zIndex` sorts a node among its *siblings* — `paintOrder()` is per
|
|
77
|
+
// node, and there is no stacking context to escape — so a ghost lifted
|
|
78
|
+
// inside its item paints under the next item, its list under the next
|
|
79
|
+
// list, its column under the next column. Every `<ReorderList>` and
|
|
80
|
+
// `<ReorderItem>` therefore asks, for the length of the gesture, whether
|
|
81
|
+
// the drag is inside it (`node.contains(activeDrag.source)`) and lifts
|
|
82
|
+
// itself if so: a board's card, its column and the board all come
|
|
83
|
+
// forward together. They learn about it by subscribing, because a list
|
|
84
|
+
// two levels up has nothing to re-render it — its children are the
|
|
85
|
+
// application's own elements, unchanged.
|
|
86
|
+
// What that cannot reach is content outside the outermost list; a popup
|
|
87
|
+
// ghost has no such limit, which is why `'auto'` is one. (It was the
|
|
88
|
+
// in-window box on the cocoa backend for two releases, while a preview
|
|
89
|
+
// window there stopped the drop reaching the list — react-x11#488, fixed
|
|
90
|
+
// in 2.8.2 by giving the preview `ignoresMouseEvents` so the pointer
|
|
91
|
+
// passes through it.)
|
|
92
|
+
// - **Every item (or its handle) is a tab stop.** An item is arbitrary
|
|
93
|
+
// content, often with controls of its own; the Tree/Table model of one
|
|
94
|
+
// tab stop with a cursor would leave a button inside a card unreachable.
|
|
95
|
+
// - **A press on a control inside an item is not a drag.** Core arms a drag
|
|
96
|
+
// from the nearest draggable *ancestor* of whatever was pressed, so a
|
|
97
|
+
// press on a `<textinput>` in a card would drag the card after 4px. The
|
|
98
|
+
// item records what the press landed on and cancels the drag at the
|
|
99
|
+
// threshold — `onDragStart`'s `preventDefault()`, which core documents as
|
|
100
|
+
// "the gesture continues as ordinary mouse events". `dragFromInteractive`
|
|
101
|
+
// turns it off; a `<ReorderHandle>` makes the question moot.
|
|
102
|
+
//
|
|
103
|
+
// It is pure composition of `<box>`, `<popup>` and core's `<Icon>`, so
|
|
104
|
+
// there is no `registerElement` here and no side effect at import time.
|
|
105
|
+
import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
106
|
+
import { Icon, announce, useDragSource, useSystemAppearance, useTheme, } from 'react-x11';
|
|
107
|
+
import { flattenStyle } from 'react-x11/style';
|
|
108
|
+
import { XK_DOWN, XK_END, XK_ESCAPE, XK_HOME, XK_LEFT, XK_RETURN, XK_RIGHT, XK_UP, } from 'react-x11/keysyms';
|
|
109
|
+
import { hx } from './hx.js';
|
|
110
|
+
// Shared with <Tree> and <Table> — internal, deliberately not a shared
|
|
111
|
+
// *module*; the header of src/internal/timers.ts says why. The drop flight
|
|
112
|
+
// needs both: a tick after the layout that placed the item, and a clock.
|
|
113
|
+
import { afterLayout, cancelAfterLayout, cancelLater, later, } from '../internal/timers.js';
|
|
114
|
+
import { closestSlot, insertManyAtSlot, moveManyToSlot, slotMark, } from './model.js';
|
|
115
|
+
export { arrayMove, closestSlot, insertAtSlot, insertManyAtSlot, isNoopSlot, moveManyToSlot, moveToSlot, slotMark, } from './model.js';
|
|
116
|
+
const h = React.createElement;
|
|
117
|
+
/**
|
|
118
|
+
* The private payload type every item offers. The scope suffix is what
|
|
119
|
+
* `dropAccept` matches on — see the header. Exported because it is the one
|
|
120
|
+
* string only this component writes (the tree-shaking guard keys on it),
|
|
121
|
+
* and because an app's own dropzone may want to refuse or take a list item
|
|
122
|
+
* by name.
|
|
123
|
+
*/
|
|
124
|
+
export const REORDER_TYPE = 'application/x-react-x11-reorder';
|
|
125
|
+
function scopedType(scope) {
|
|
126
|
+
return `${REORDER_TYPE};scope=${scope}`;
|
|
127
|
+
}
|
|
128
|
+
/** The indicator's thickness. Centred on the edge it marks. */
|
|
129
|
+
const INDICATOR = 2;
|
|
130
|
+
/** The default handle: two columns of core's `moreVertical` dots. */
|
|
131
|
+
const GRIP_DOT = 10;
|
|
132
|
+
/**
|
|
133
|
+
* How much of an item, at each end, still reads as an edge once `combine`
|
|
134
|
+
* is on — so the middle half of an item is where a merge lands. Small
|
|
135
|
+
* enough that reordering past a combining list is still comfortable.
|
|
136
|
+
*/
|
|
137
|
+
const COMBINE_BAND = 0.25;
|
|
138
|
+
/** How long the ghost takes to fly to where the item landed. */
|
|
139
|
+
const DROP_MS = 180;
|
|
140
|
+
/** One step of that flight. Not a frame clock: this is a handful of ticks
|
|
141
|
+
* moving one override-redirect window, and core's own animation machinery
|
|
142
|
+
* drives style properties rather than a popup's geometry. */
|
|
143
|
+
const FLIGHT_STEP_MS = 16;
|
|
144
|
+
/** What the focus target tells a screen reader it can do. */
|
|
145
|
+
const HINT = 'Draggable. Press Space to lift, the arrow keys to move, Space to drop.';
|
|
146
|
+
/** Every announcement, in one place, because none of it is localised yet. */
|
|
147
|
+
const SAY = {
|
|
148
|
+
lifted: (label, at, of, many) => `Lifted ${label}${many > 1 ? ` and ${many - 1} more` : ''}, ` +
|
|
149
|
+
`position ${at} of ${of}. ` +
|
|
150
|
+
'Use the arrow keys to move, Space to drop, Escape to cancel.',
|
|
151
|
+
pickedUp: (label, at, of, many) => `Dragging ${label}${many > 1 ? ` and ${many - 1} more` : ''}, ` +
|
|
152
|
+
`position ${at} of ${of}.`,
|
|
153
|
+
moved: (label, at, of) => `${label} moved to position ${at} of ${of}.`,
|
|
154
|
+
dropped: (label, at, of) => `${label} dropped at position ${at} of ${of}.`,
|
|
155
|
+
droppedInto: (label, list) => `${label} dropped${list ? ` in ${list}` : ' in another list'}.`,
|
|
156
|
+
combined: (label, into) => `${label} dropped onto ${into}.`,
|
|
157
|
+
cancelled: (label, at) => `Move cancelled. ${label} returned to position ${at}.`,
|
|
158
|
+
returned: (label) => `${label} returned to where it was.`,
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* The gesture's scratchpad — see the header. Written by whichever list the
|
|
162
|
+
* pointer is over, read by the source's own `onDrag`, and null between
|
|
163
|
+
* gestures. One drag at a time is a property of the pointer, not an
|
|
164
|
+
* assumption this makes.
|
|
165
|
+
*
|
|
166
|
+
* `source` and `inline` are here for the stacking: an in-window ghost has to
|
|
167
|
+
* be painted over everything between it and the drop, and `zIndex` only
|
|
168
|
+
* sorts siblings, so every list and item that *contains* the drag lifts
|
|
169
|
+
* itself. They ask this.
|
|
170
|
+
*/
|
|
171
|
+
let activeDrag = null;
|
|
172
|
+
/**
|
|
173
|
+
* Who to tell when a drag starts or ends.
|
|
174
|
+
*
|
|
175
|
+
* A list two levels above the one being dragged in has nothing to re-render
|
|
176
|
+
* it — its children are the application's elements, unchanged — so it
|
|
177
|
+
* subscribes instead. Twice a gesture, for as many entries as there are
|
|
178
|
+
* mounted lists and items, each answering one `contains`.
|
|
179
|
+
*/
|
|
180
|
+
const watchers = new Set();
|
|
181
|
+
function announceDragChanged() {
|
|
182
|
+
for (const watcher of [...watchers])
|
|
183
|
+
watcher();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether the drag in flight is inside `ref`, and drawing a ghost this node
|
|
187
|
+
* has to be painted above. What lifts the whole path from the dragged item
|
|
188
|
+
* up to the outermost list — a board's card, its column, and the board.
|
|
189
|
+
*/
|
|
190
|
+
function useCarryingDrag(ref) {
|
|
191
|
+
const [carrying, setCarrying] = useState(false);
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const update = () => {
|
|
194
|
+
const drag = activeDrag;
|
|
195
|
+
const node = ref.current;
|
|
196
|
+
const holds = Boolean(drag?.inline && drag.source && node && node.contains(drag.source));
|
|
197
|
+
setCarrying(holds);
|
|
198
|
+
};
|
|
199
|
+
watchers.add(update);
|
|
200
|
+
update();
|
|
201
|
+
return () => {
|
|
202
|
+
watchers.delete(update);
|
|
203
|
+
};
|
|
204
|
+
}, [ref]);
|
|
205
|
+
return carrying;
|
|
206
|
+
}
|
|
207
|
+
const ListContext = React.createContext(null);
|
|
208
|
+
function useList(part) {
|
|
209
|
+
const list = useContext(ListContext);
|
|
210
|
+
if (!list) {
|
|
211
|
+
throw new Error(`@react-x11/components: <${part}> has to be inside a <ReorderList>.`);
|
|
212
|
+
}
|
|
213
|
+
return list;
|
|
214
|
+
}
|
|
215
|
+
const ItemContext = React.createContext(null);
|
|
216
|
+
/**
|
|
217
|
+
* The state of the `<ReorderItem>` a component is rendered inside — what
|
|
218
|
+
* function `children` are handed, for a component deeper in the item. Reads
|
|
219
|
+
* `preview` to tell the ghost's copy from the item in the list.
|
|
220
|
+
*/
|
|
221
|
+
export function useReorderItem() {
|
|
222
|
+
const item = useContext(ItemContext);
|
|
223
|
+
if (!item) {
|
|
224
|
+
throw new Error('@react-x11/components: useReorderItem() has to be called inside a <ReorderItem>.');
|
|
225
|
+
}
|
|
226
|
+
return item.state;
|
|
227
|
+
}
|
|
228
|
+
const NO_DRAG = {};
|
|
229
|
+
const noop = () => { };
|
|
230
|
+
const detachNoop = () => noop;
|
|
231
|
+
/** What the preview's copy of the children is handed: the state with
|
|
232
|
+
* `preview` set, and a handle rendered in the ghost draws its grip and does
|
|
233
|
+
* nothing else — it is a picture of one. */
|
|
234
|
+
function inertItem(state, styles) {
|
|
235
|
+
return {
|
|
236
|
+
state,
|
|
237
|
+
disabled: true,
|
|
238
|
+
dragProps: NO_DRAG,
|
|
239
|
+
onKeyDown: noop,
|
|
240
|
+
onBlur: noop,
|
|
241
|
+
styles,
|
|
242
|
+
attachHandle: detachNoop,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Where a list's ghost is drawn, resolved.
|
|
247
|
+
*
|
|
248
|
+
* `'auto'` is the popup on both backends. It was the in-window box on cocoa
|
|
249
|
+
* for two releases, while a `<popup dragPreview>` there stopped the drop
|
|
250
|
+
* reaching the list beneath it: first because it registered as a dragging
|
|
251
|
+
* destination (react-x11#488, fixed in 2.8.1), and then because the window
|
|
252
|
+
* server finds the window under the pointer whether or not it has a
|
|
253
|
+
* destination, so a preview with none left the drag with none (#492, fixed
|
|
254
|
+
* in 2.8.2 by creating the preview with `ignoresMouseEvents`, so the hit
|
|
255
|
+
* passes through it). That needs `@windowkit/appkit` >= 0.6.0, which
|
|
256
|
+
* react-x11 2.8.2 asks for — an older bridge ignores the option silently.
|
|
257
|
+
*/
|
|
258
|
+
function ghostMode(preview) {
|
|
259
|
+
return preview === 'auto' ? 'popup' : preview;
|
|
260
|
+
}
|
|
261
|
+
function scaleOf(node) {
|
|
262
|
+
const scale = node?.scale;
|
|
263
|
+
return scale && scale > 0 ? scale : 1;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The elements and roles a press belongs to rather than to the item around
|
|
267
|
+
* them. Roles rather than kinds wherever core's own widgets have one, so an
|
|
268
|
+
* app's `<Button>`, `<Slider>` or `<Switch>` is covered without this
|
|
269
|
+
* knowing what they are made of; kinds for the text elements, which are
|
|
270
|
+
* elements rather than compositions.
|
|
271
|
+
*/
|
|
272
|
+
const INTERACTIVE_KINDS = new Set(['textinput', 'textarea', 'codeeditor']);
|
|
273
|
+
const INTERACTIVE_ROLES = new Set([
|
|
274
|
+
'button',
|
|
275
|
+
'checkbox',
|
|
276
|
+
'combobox',
|
|
277
|
+
'link',
|
|
278
|
+
'menuitem',
|
|
279
|
+
'menuitemcheckbox',
|
|
280
|
+
'menuitemradio',
|
|
281
|
+
'option',
|
|
282
|
+
'radio',
|
|
283
|
+
'scrollbar',
|
|
284
|
+
'searchbox',
|
|
285
|
+
'slider',
|
|
286
|
+
'spinbutton',
|
|
287
|
+
'switch',
|
|
288
|
+
'tab',
|
|
289
|
+
'textbox',
|
|
290
|
+
]);
|
|
291
|
+
/** Was this press on a control, somewhere between the hit node and the item
|
|
292
|
+
* around it? */
|
|
293
|
+
function pressedControl(target, item) {
|
|
294
|
+
for (let n = target; n; n = n.parent) {
|
|
295
|
+
if (INTERACTIVE_KINDS.has(n.kind))
|
|
296
|
+
return true;
|
|
297
|
+
const role = n.props?.role;
|
|
298
|
+
if (typeof role === 'string' && INTERACTIVE_ROLES.has(role))
|
|
299
|
+
return true;
|
|
300
|
+
if (n === item)
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
/** Every piece of text under a node, joined — what an item is called when
|
|
306
|
+
* it has no `aria-label`. `textContent()` answers for one node; a card is
|
|
307
|
+
* several. */
|
|
308
|
+
function labelOf(node) {
|
|
309
|
+
if (!node)
|
|
310
|
+
return 'item';
|
|
311
|
+
const parts = [];
|
|
312
|
+
const walk = (n) => {
|
|
313
|
+
const text = n.textContent();
|
|
314
|
+
if (text)
|
|
315
|
+
parts.push(text);
|
|
316
|
+
for (const child of n.children)
|
|
317
|
+
walk(child);
|
|
318
|
+
};
|
|
319
|
+
walk(node);
|
|
320
|
+
const label = parts.join(' ').trim();
|
|
321
|
+
return label || 'item';
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The list's `dropAccept`: its own scope, plus whatever the app's `accept`
|
|
325
|
+
* says. An array gains the scope and a predicate is or-ed with it, so
|
|
326
|
+
* core's own group matching (`'files'`, `'text'`, `'uris'`) keeps applying
|
|
327
|
+
* to the foreign half — nothing about accept vocabulary is re-implemented
|
|
328
|
+
* here.
|
|
329
|
+
*/
|
|
330
|
+
function widenAccept(accept, type) {
|
|
331
|
+
if (accept === undefined)
|
|
332
|
+
return [type];
|
|
333
|
+
if (typeof accept === 'function') {
|
|
334
|
+
return (types) => types.includes(type) || Boolean(accept(types));
|
|
335
|
+
}
|
|
336
|
+
return [...(Array.isArray(accept) ? accept : [accept]), type];
|
|
337
|
+
}
|
|
338
|
+
/** Whether the app's `accept` — on its own, without the list's scope —
|
|
339
|
+
* takes what is on offer. `ev.has` is core's alias-aware test, which is
|
|
340
|
+
* what keeps the semantic groups core's to define. */
|
|
341
|
+
function foreignAccepted(accept, ev) {
|
|
342
|
+
if (accept === undefined)
|
|
343
|
+
return false;
|
|
344
|
+
if (typeof accept === 'function')
|
|
345
|
+
return Boolean(accept(ev.types));
|
|
346
|
+
return (Array.isArray(accept) ? accept : [accept]).some((want) => ev.has(want));
|
|
347
|
+
}
|
|
348
|
+
function refuse() {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
function sameOver(a, b) {
|
|
352
|
+
if (a === b)
|
|
353
|
+
return true;
|
|
354
|
+
if (!a || !b)
|
|
355
|
+
return false;
|
|
356
|
+
return (a.listUid === b.listUid && a.index === b.index && a.combine === b.combine);
|
|
357
|
+
}
|
|
358
|
+
/** Ease-out cubic — the curve core's own transitions use. */
|
|
359
|
+
function ease(t) {
|
|
360
|
+
return 1 - (1 - t) ** 3;
|
|
361
|
+
}
|
|
362
|
+
// --- the list ---------------------------------------------------------------
|
|
363
|
+
/**
|
|
364
|
+
* `<ReorderList onReorder>` around `<ReorderItem id>`s.
|
|
365
|
+
*
|
|
366
|
+
* <ReorderList onReorder={(e) => setTodos((l) => arrayMove(l, e.from, e.to))}>
|
|
367
|
+
* {todos.map((t) => (
|
|
368
|
+
* <ReorderItem key={t.id} id={t.id}><text>{t.title}</text></ReorderItem>
|
|
369
|
+
* ))}
|
|
370
|
+
* </ReorderList>
|
|
371
|
+
*
|
|
372
|
+
* The order is read from the tree the list rendered, so it is written once —
|
|
373
|
+
* there is no `items` array to keep beside the children.
|
|
374
|
+
*/
|
|
375
|
+
export function ReorderList(props) {
|
|
376
|
+
const { id, group, orientation = 'vertical', disabled = false, selected, onReorder, onInsert, onRemove, onCombine, combine = false, canDrop, accept, onDrop, onDragStart, onDragUpdate, onDragEnd, preview = 'auto', renderPreview, previewSize, dropAnimation = true, styles, style, 'data-testname': testname, children, } = props;
|
|
377
|
+
const uid = React.useId();
|
|
378
|
+
const type = scopedType(group !== undefined ? `group:${group}` : `list:${uid}`);
|
|
379
|
+
const { reducedMotion } = useSystemAppearance();
|
|
380
|
+
const rootRef = useRef(null);
|
|
381
|
+
const entries = useRef(new Map());
|
|
382
|
+
/** The item showing the indicator, and how. */
|
|
383
|
+
const marked = useRef(null);
|
|
384
|
+
/** The item of *this* list the pointer is dragging, if any. */
|
|
385
|
+
const dragging = useRef(null);
|
|
386
|
+
/** What the last `onDragUpdate` said, so one is not sent per motion. */
|
|
387
|
+
const reportedOver = useRef(null);
|
|
388
|
+
/** Whether the drag in flight is inside this list and drawing an
|
|
389
|
+
* in-window ghost — which is what its own stacking depends on. */
|
|
390
|
+
const carrying = useCarryingDrag(rootRef);
|
|
391
|
+
const [lifted, setLifted] = useState(null);
|
|
392
|
+
const liftedRef = useRef(lifted);
|
|
393
|
+
liftedRef.current = lifted;
|
|
394
|
+
// The handlers by ref, so the context value — and with it every item —
|
|
395
|
+
// does not re-render because an app wrote an inline arrow.
|
|
396
|
+
const handlers = useRef({
|
|
397
|
+
onReorder,
|
|
398
|
+
onInsert,
|
|
399
|
+
onRemove,
|
|
400
|
+
onCombine,
|
|
401
|
+
onDrop,
|
|
402
|
+
onDragStart,
|
|
403
|
+
onDragUpdate,
|
|
404
|
+
onDragEnd,
|
|
405
|
+
canDrop,
|
|
406
|
+
accept,
|
|
407
|
+
selected,
|
|
408
|
+
combine,
|
|
409
|
+
});
|
|
410
|
+
handlers.current = {
|
|
411
|
+
onReorder,
|
|
412
|
+
onInsert,
|
|
413
|
+
onRemove,
|
|
414
|
+
onCombine,
|
|
415
|
+
onDrop,
|
|
416
|
+
onDragStart,
|
|
417
|
+
onDragUpdate,
|
|
418
|
+
onDragEnd,
|
|
419
|
+
canDrop,
|
|
420
|
+
accept,
|
|
421
|
+
selected,
|
|
422
|
+
combine,
|
|
423
|
+
};
|
|
424
|
+
/**
|
|
425
|
+
* The ids in tree order — the order the app rendered the items in. A
|
|
426
|
+
* walk of the root's retained children, stopping at each item, so a
|
|
427
|
+
* wrapper around an item costs nothing and an item inside an item is not
|
|
428
|
+
* a thing.
|
|
429
|
+
*/
|
|
430
|
+
const order = useCallback(() => {
|
|
431
|
+
const root = rootRef.current;
|
|
432
|
+
if (!root)
|
|
433
|
+
return [];
|
|
434
|
+
const byNode = new Map();
|
|
435
|
+
for (const entry of entries.current.values()) {
|
|
436
|
+
if (entry.node.current)
|
|
437
|
+
byNode.set(entry.node.current, entry.id);
|
|
438
|
+
}
|
|
439
|
+
const out = [];
|
|
440
|
+
const walk = (n) => {
|
|
441
|
+
const found = byNode.get(n);
|
|
442
|
+
if (found !== undefined) {
|
|
443
|
+
out.push(found);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
for (const child of n.children)
|
|
447
|
+
walk(child);
|
|
448
|
+
};
|
|
449
|
+
for (const child of root.children)
|
|
450
|
+
walk(child);
|
|
451
|
+
return out;
|
|
452
|
+
}, []);
|
|
453
|
+
/** What travels when this item is picked up: the selection, in the list's
|
|
454
|
+
* own order, when the item is in it — and just the item otherwise. */
|
|
455
|
+
const movingIds = useCallback((itemId) => {
|
|
456
|
+
const set = handlers.current.selected;
|
|
457
|
+
if (!set?.length || !set.includes(itemId))
|
|
458
|
+
return [itemId];
|
|
459
|
+
const chosen = new Set(set);
|
|
460
|
+
const ids = order().filter((x) => chosen.has(x));
|
|
461
|
+
return ids.length ? ids : [itemId];
|
|
462
|
+
}, [order]);
|
|
463
|
+
/** The items' boxes in logical pixels — `abs` is device, and the pointer
|
|
464
|
+
* in a drag event is not. One division, here. */
|
|
465
|
+
const rectsOf = (ids) => {
|
|
466
|
+
const scale = scaleOf(rootRef.current);
|
|
467
|
+
return ids.map((itemId) => {
|
|
468
|
+
const node = entries.current.get(itemId)?.node.current;
|
|
469
|
+
const { x, y, width, height } = node?.abs ?? {
|
|
470
|
+
x: 0,
|
|
471
|
+
y: 0,
|
|
472
|
+
width: 0,
|
|
473
|
+
height: 0,
|
|
474
|
+
};
|
|
475
|
+
return {
|
|
476
|
+
x: x / scale,
|
|
477
|
+
y: y / scale,
|
|
478
|
+
width: width / scale,
|
|
479
|
+
height: height / scale,
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
/** Move the indicator: clear the item that had it, set the one that does. */
|
|
484
|
+
const mark = (next) => {
|
|
485
|
+
const prev = marked.current;
|
|
486
|
+
const same = prev !== null &&
|
|
487
|
+
next !== null &&
|
|
488
|
+
prev.id === next.id &&
|
|
489
|
+
prev.edge === next.edge &&
|
|
490
|
+
prev.combine === next.combine &&
|
|
491
|
+
prev.offset === next.offset;
|
|
492
|
+
if (same)
|
|
493
|
+
return;
|
|
494
|
+
if (prev)
|
|
495
|
+
entries.current.get(prev.id)?.setMark(null);
|
|
496
|
+
if (next) {
|
|
497
|
+
entries.current.get(next.id)?.setMark({
|
|
498
|
+
edge: next.edge,
|
|
499
|
+
combine: next.combine,
|
|
500
|
+
offset: next.offset,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
marked.current = next;
|
|
504
|
+
};
|
|
505
|
+
/** Where a drag event's pointer would land. */
|
|
506
|
+
const slotFor = (ev, moving) => {
|
|
507
|
+
const ids = order();
|
|
508
|
+
const hit = closestSlot(rectsOf(ids), { x: ev.x, y: ev.y }, orientation, rootRef.current?.direction ?? 'ltr', handlers.current.combine ? COMBINE_BAND : 0);
|
|
509
|
+
if (!hit)
|
|
510
|
+
return { ids, slot: 0, at: null };
|
|
511
|
+
const over = ids[hit.index];
|
|
512
|
+
// an item cannot merge into itself, nor into anything travelling with it
|
|
513
|
+
if (hit.combine && !moving.includes(over)) {
|
|
514
|
+
return {
|
|
515
|
+
ids,
|
|
516
|
+
slot: hit.slot,
|
|
517
|
+
at: { id: over, edge: hit.edge, combine: true, offset: 0 },
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
// The mark comes from the **slot**, not from the item the slot was read
|
|
521
|
+
// off: the lower half of one item and the upper half of the next are the
|
|
522
|
+
// same gap, and a mark keyed to the item would draw that one insertion
|
|
523
|
+
// point in two places and flip between them mid-gap. See `slotMark`.
|
|
524
|
+
const line = slotMark(ids.length, hit.slot);
|
|
525
|
+
if (!line)
|
|
526
|
+
return { ids, slot: hit.slot, at: null };
|
|
527
|
+
// half the gap it is marking, so the line sits in the space rather than
|
|
528
|
+
// against the item below it. The ends have no gap: they stay on the edge.
|
|
529
|
+
const rects = rectsOf(ids);
|
|
530
|
+
const before = line.edge === 'before' ? line.index - 1 : -1;
|
|
531
|
+
let offset = 0;
|
|
532
|
+
if (before >= 0) {
|
|
533
|
+
const a = rects[before];
|
|
534
|
+
const b = rects[line.index];
|
|
535
|
+
const rtl = (rootRef.current?.direction ?? 'ltr') === 'rtl';
|
|
536
|
+
const space = orientation === 'vertical'
|
|
537
|
+
? b.y - (a.y + a.height)
|
|
538
|
+
: rtl
|
|
539
|
+
? a.x - (b.x + b.width)
|
|
540
|
+
: b.x - (a.x + a.width);
|
|
541
|
+
offset = -Math.max(space, 0) / 2;
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
ids,
|
|
545
|
+
slot: hit.slot,
|
|
546
|
+
at: { id: ids[line.index], edge: line.edge, combine: false, offset },
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
/** What the payload of a drag is, when it is one of ours. */
|
|
550
|
+
const payloadOf = (ev) => {
|
|
551
|
+
const items = ev.items;
|
|
552
|
+
const found = items?.[type];
|
|
553
|
+
return found && typeof found === 'object' ? found : null;
|
|
554
|
+
};
|
|
555
|
+
/** The live payload while the pointer is merely *over* us: a `DragEvent`
|
|
556
|
+
* carries no `items`, so the scratchpad is the only way to know whose
|
|
557
|
+
* item this is — see the header. */
|
|
558
|
+
const hoveringPayload = (ev) => {
|
|
559
|
+
if (!ev.types.includes(type))
|
|
560
|
+
return null;
|
|
561
|
+
return payloadOf(ev) ?? activeDrag?.payload ?? null;
|
|
562
|
+
};
|
|
563
|
+
const allowed = (ev, payload, slot, combining) => {
|
|
564
|
+
const ask = handlers.current.canDrop;
|
|
565
|
+
if (!ask)
|
|
566
|
+
return true;
|
|
567
|
+
return Boolean(ask({
|
|
568
|
+
id: payload?.id,
|
|
569
|
+
ids: payload?.ids ?? [],
|
|
570
|
+
source: payload ? { list: payload.list, index: payload.index } : null,
|
|
571
|
+
index: slot,
|
|
572
|
+
combine: combining,
|
|
573
|
+
event: ev,
|
|
574
|
+
}));
|
|
575
|
+
};
|
|
576
|
+
const onDragOver = (ev) => {
|
|
577
|
+
// `onDragOver` reaches every node on the path under the pointer, matched
|
|
578
|
+
// or not — so a list has to ask the question `dropAccept` already
|
|
579
|
+
// answered before it promises a slot to a drag it will never be given
|
|
580
|
+
const payload = hoveringPayload(ev);
|
|
581
|
+
if (!payload && !foreignAccepted(handlers.current.accept, ev)) {
|
|
582
|
+
mark(null);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const moving = payload?.listUid === uid ? payload.ids : [];
|
|
586
|
+
const { ids, slot, at } = slotFor(ev, moving);
|
|
587
|
+
const combining = at?.combine ? at.id : null;
|
|
588
|
+
if (!allowed(ev, payload, slot, combining)) {
|
|
589
|
+
mark(null);
|
|
590
|
+
if (activeDrag?.over?.listUid === uid)
|
|
591
|
+
activeDrag.over = null;
|
|
592
|
+
// core keeps the node its `dropAccept` matched; what a handler may do
|
|
593
|
+
// is refuse this position, which is what turns the ghost's `accepted`
|
|
594
|
+
// off and what `canDrop` means
|
|
595
|
+
ev.reject();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// one of this list's own items, over a gap that is already its own:
|
|
599
|
+
// nothing would move, so nothing is promised
|
|
600
|
+
const move = !combining && moving.length > 0
|
|
601
|
+
? moveManyToSlot(ids, moving, slot, payload.id)
|
|
602
|
+
: null;
|
|
603
|
+
if (!combining && moving.length > 0 && move === null) {
|
|
604
|
+
mark(null);
|
|
605
|
+
if (activeDrag?.over?.listUid === uid)
|
|
606
|
+
activeDrag.over = null;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
mark(at);
|
|
610
|
+
if (activeDrag) {
|
|
611
|
+
activeDrag.over = {
|
|
612
|
+
listUid: uid,
|
|
613
|
+
list: id,
|
|
614
|
+
// where it would land, not the gap it is over — a same-list move
|
|
615
|
+
// closes the hole it left behind, and a newcomer lands at the gap
|
|
616
|
+
index: move ? move.to : slot,
|
|
617
|
+
combine: combining,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
const onDragLeave = () => {
|
|
622
|
+
mark(null);
|
|
623
|
+
if (activeDrag?.over?.listUid === uid)
|
|
624
|
+
activeDrag.over = null;
|
|
625
|
+
};
|
|
626
|
+
const onDropHere = (ev) => {
|
|
627
|
+
mark(null);
|
|
628
|
+
const payload = payloadOf(ev);
|
|
629
|
+
const moving = payload?.listUid === uid ? payload.ids : [];
|
|
630
|
+
const { ids, slot, at } = slotFor(ev, moving);
|
|
631
|
+
const combining = at?.combine ? at.id : null;
|
|
632
|
+
if (!allowed(ev, payload, slot, combining)) {
|
|
633
|
+
ev.reject();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (payload) {
|
|
637
|
+
// a copy leaves the source's item where it is; anything else moves it
|
|
638
|
+
const action = ev.action === 'copy' ? 'copy' : 'move';
|
|
639
|
+
payload.to = { listUid: uid, list: id, index: slot, combine: combining };
|
|
640
|
+
if (combining !== null) {
|
|
641
|
+
ev.accept(action);
|
|
642
|
+
handlers.current.onCombine?.({
|
|
643
|
+
id: payload.id,
|
|
644
|
+
ids: payload.ids,
|
|
645
|
+
into: combining,
|
|
646
|
+
index: ids.indexOf(combining),
|
|
647
|
+
source: { list: payload.list, index: payload.index },
|
|
648
|
+
event: ev,
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (payload.listUid === uid) {
|
|
653
|
+
// a list does not duplicate into itself: a same-list drop reorders
|
|
654
|
+
const move = moveManyToSlot(ids, payload.ids, slot, payload.id);
|
|
655
|
+
payload.to.index = move ? move.to : payload.index;
|
|
656
|
+
ev.accept('move');
|
|
657
|
+
if (move) {
|
|
658
|
+
handlers.current.onReorder?.({
|
|
659
|
+
items: move.items,
|
|
660
|
+
id: payload.id,
|
|
661
|
+
ids: move.ids,
|
|
662
|
+
from: move.from,
|
|
663
|
+
to: move.to,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
ev.accept(action);
|
|
669
|
+
handlers.current.onInsert?.({
|
|
670
|
+
items: insertManyAtSlot(ids, payload.ids, slot),
|
|
671
|
+
id: payload.id,
|
|
672
|
+
ids: payload.ids,
|
|
673
|
+
index: slot,
|
|
674
|
+
action,
|
|
675
|
+
source: { list: payload.list, index: payload.index },
|
|
676
|
+
event: ev,
|
|
677
|
+
});
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
// Not a live list item: files, text, another application's drag — or
|
|
681
|
+
// the scope type from another process, which is JSON by then and not
|
|
682
|
+
// ours to act on. The app's `accept` alone decides.
|
|
683
|
+
if (!foreignAccepted(handlers.current.accept, ev)) {
|
|
684
|
+
ev.reject();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
handlers.current.onDrop?.({ index: slot, event: ev });
|
|
688
|
+
};
|
|
689
|
+
// --- the keyboard's half, lent to the items ------------------------------
|
|
690
|
+
const labelFor = (itemId) => entries.current.get(itemId)?.label() ?? 'item';
|
|
691
|
+
const lift = useCallback((itemId) => {
|
|
692
|
+
const ids = order();
|
|
693
|
+
const moving = movingIds(itemId);
|
|
694
|
+
setLifted({ id: itemId, ids: moving, origin: ids });
|
|
695
|
+
const at = ids.indexOf(itemId) + 1;
|
|
696
|
+
handlers.current.onDragStart?.({
|
|
697
|
+
id: itemId,
|
|
698
|
+
ids: moving,
|
|
699
|
+
index: at - 1,
|
|
700
|
+
input: 'keyboard',
|
|
701
|
+
});
|
|
702
|
+
announce(SAY.lifted(labelFor(itemId), at, ids.length, moving.length));
|
|
703
|
+
}, [order, movingIds]);
|
|
704
|
+
const drop = useCallback(() => {
|
|
705
|
+
const current = liftedRef.current;
|
|
706
|
+
if (!current)
|
|
707
|
+
return;
|
|
708
|
+
setLifted(null);
|
|
709
|
+
const ids = order();
|
|
710
|
+
const at = ids.indexOf(current.id);
|
|
711
|
+
handlers.current.onDragEnd?.({
|
|
712
|
+
id: current.id,
|
|
713
|
+
ids: current.ids,
|
|
714
|
+
from: current.origin.indexOf(current.id),
|
|
715
|
+
to: { list: id, index: at },
|
|
716
|
+
combine: null,
|
|
717
|
+
reason: 'drop',
|
|
718
|
+
input: 'keyboard',
|
|
719
|
+
});
|
|
720
|
+
announce(SAY.dropped(labelFor(current.id), at + 1, ids.length));
|
|
721
|
+
}, [id, order]);
|
|
722
|
+
const cancel = useCallback(() => {
|
|
723
|
+
const current = liftedRef.current;
|
|
724
|
+
if (!current)
|
|
725
|
+
return;
|
|
726
|
+
setLifted(null);
|
|
727
|
+
const ids = order();
|
|
728
|
+
const from = ids.indexOf(current.id);
|
|
729
|
+
const to = current.origin.indexOf(current.id);
|
|
730
|
+
if (from >= 0 && to >= 0 && from !== to) {
|
|
731
|
+
const back = moveManyToSlot(ids, current.ids, to + (to > from ? 1 : 0), current.id);
|
|
732
|
+
if (back) {
|
|
733
|
+
handlers.current.onReorder?.({
|
|
734
|
+
items: back.items,
|
|
735
|
+
id: current.id,
|
|
736
|
+
ids: back.ids,
|
|
737
|
+
from: back.from,
|
|
738
|
+
to: back.to,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
handlers.current.onDragEnd?.({
|
|
743
|
+
id: current.id,
|
|
744
|
+
ids: current.ids,
|
|
745
|
+
from,
|
|
746
|
+
to: null,
|
|
747
|
+
combine: null,
|
|
748
|
+
reason: 'cancel',
|
|
749
|
+
input: 'keyboard',
|
|
750
|
+
});
|
|
751
|
+
announce(SAY.cancelled(labelFor(current.id), (to >= 0 ? to : from) + 1));
|
|
752
|
+
}, [order]);
|
|
753
|
+
const step = useCallback((itemId, where) => {
|
|
754
|
+
const current = liftedRef.current;
|
|
755
|
+
const ids = order();
|
|
756
|
+
const from = ids.indexOf(itemId);
|
|
757
|
+
if (from < 0)
|
|
758
|
+
return;
|
|
759
|
+
const moving = current?.ids ?? [itemId];
|
|
760
|
+
const set = new Set(moving);
|
|
761
|
+
const held = ids
|
|
762
|
+
.map((x, i) => (set.has(x) ? i : -1))
|
|
763
|
+
.filter((i) => i >= 0);
|
|
764
|
+
// A step moves the whole run **past its next neighbour**, which for one
|
|
765
|
+
// item is the ordinary "swap with the one below". `from + 1` would be
|
|
766
|
+
// inside the run itself for a multi-drag, and land it where it already
|
|
767
|
+
// is — the shape of that bug is a selection that will not move.
|
|
768
|
+
let slot;
|
|
769
|
+
if (where === 'home')
|
|
770
|
+
slot = 0;
|
|
771
|
+
else if (where === 'end')
|
|
772
|
+
slot = ids.length;
|
|
773
|
+
else if (where === 1) {
|
|
774
|
+
const last = held.at(-1) ?? from;
|
|
775
|
+
const next = ids.findIndex((x, i) => i > last && !set.has(x));
|
|
776
|
+
if (next < 0)
|
|
777
|
+
return;
|
|
778
|
+
slot = next + 1;
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
const first = held[0] ?? from;
|
|
782
|
+
let prev = -1;
|
|
783
|
+
for (let i = first - 1; i >= 0; i--) {
|
|
784
|
+
if (!set.has(ids[i])) {
|
|
785
|
+
prev = i;
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (prev < 0)
|
|
790
|
+
return;
|
|
791
|
+
slot = prev;
|
|
792
|
+
}
|
|
793
|
+
const move = moveManyToSlot(ids, moving, slot, itemId);
|
|
794
|
+
if (!move)
|
|
795
|
+
return;
|
|
796
|
+
handlers.current.onReorder?.({
|
|
797
|
+
items: move.items,
|
|
798
|
+
id: itemId,
|
|
799
|
+
ids: move.ids,
|
|
800
|
+
from: move.from,
|
|
801
|
+
to: move.to,
|
|
802
|
+
});
|
|
803
|
+
handlers.current.onDragUpdate?.({
|
|
804
|
+
id: itemId,
|
|
805
|
+
ids: move.ids,
|
|
806
|
+
from,
|
|
807
|
+
over: { list: id, index: move.to },
|
|
808
|
+
combine: null,
|
|
809
|
+
input: 'keyboard',
|
|
810
|
+
});
|
|
811
|
+
announce(SAY.moved(labelFor(itemId), move.to + 1, ids.length));
|
|
812
|
+
}, [id, order]);
|
|
813
|
+
const focusNeighbour = useCallback((itemId, delta) => {
|
|
814
|
+
const ids = order();
|
|
815
|
+
const next = ids[ids.indexOf(itemId) + delta];
|
|
816
|
+
if (next !== undefined)
|
|
817
|
+
entries.current.get(next)?.focus();
|
|
818
|
+
}, [order]);
|
|
819
|
+
const register = useCallback((entry) => {
|
|
820
|
+
entries.current.set(entry.id, entry);
|
|
821
|
+
return () => {
|
|
822
|
+
if (entries.current.get(entry.id) === entry) {
|
|
823
|
+
entries.current.delete(entry.id);
|
|
824
|
+
}
|
|
825
|
+
if (marked.current?.id === entry.id)
|
|
826
|
+
marked.current = null;
|
|
827
|
+
};
|
|
828
|
+
}, []);
|
|
829
|
+
const dragStarted = useCallback((itemId) => {
|
|
830
|
+
dragging.current = itemId;
|
|
831
|
+
// a pointer drag and a keyboard lift are one gesture's worth of state
|
|
832
|
+
if (liftedRef.current)
|
|
833
|
+
setLifted(null);
|
|
834
|
+
reportedOver.current = null;
|
|
835
|
+
const ids = order();
|
|
836
|
+
const moving = movingIds(itemId);
|
|
837
|
+
const index = ids.indexOf(itemId);
|
|
838
|
+
handlers.current.onDragStart?.({
|
|
839
|
+
id: itemId,
|
|
840
|
+
ids: moving,
|
|
841
|
+
index,
|
|
842
|
+
input: 'pointer',
|
|
843
|
+
});
|
|
844
|
+
announce(SAY.pickedUp(labelFor(itemId), index + 1, ids.length, moving.length));
|
|
845
|
+
return { index, ids: moving };
|
|
846
|
+
}, [order, movingIds]);
|
|
847
|
+
/** One pointer position, on the source's side: core tells the target
|
|
848
|
+
* first, so the scratchpad is already up to date here. */
|
|
849
|
+
const dragMoved = useCallback((itemId) => {
|
|
850
|
+
const over = activeDrag?.over ?? null;
|
|
851
|
+
if (sameOver(over, reportedOver.current))
|
|
852
|
+
return;
|
|
853
|
+
reportedOver.current = over;
|
|
854
|
+
const payload = activeDrag?.payload;
|
|
855
|
+
handlers.current.onDragUpdate?.({
|
|
856
|
+
id: itemId,
|
|
857
|
+
ids: payload?.ids ?? [itemId],
|
|
858
|
+
from: payload?.index ?? -1,
|
|
859
|
+
over: over ? { list: over.list, index: over.index } : null,
|
|
860
|
+
combine: over?.combine ?? null,
|
|
861
|
+
input: 'pointer',
|
|
862
|
+
});
|
|
863
|
+
}, []);
|
|
864
|
+
const dragEnded = useCallback((itemId, ev, payload) => {
|
|
865
|
+
dragging.current = null;
|
|
866
|
+
reportedOver.current = null;
|
|
867
|
+
mark(null);
|
|
868
|
+
const to = payload?.to;
|
|
869
|
+
const moved = ev.dropped && ev.action === 'move';
|
|
870
|
+
const ids = order();
|
|
871
|
+
// a move within this list has already been reported as a reorder
|
|
872
|
+
const elsewhere = moved && !(to && to.listUid === uid);
|
|
873
|
+
if (elsewhere) {
|
|
874
|
+
handlers.current.onRemove?.({
|
|
875
|
+
items: ids.filter((x) => !(payload?.ids ?? [itemId]).includes(x)),
|
|
876
|
+
id: itemId,
|
|
877
|
+
ids: payload?.ids ?? [itemId],
|
|
878
|
+
index: ids.indexOf(itemId),
|
|
879
|
+
to: to ? { list: to.list, index: to.index } : null,
|
|
880
|
+
event: ev,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
handlers.current.onDragEnd?.({
|
|
884
|
+
id: itemId,
|
|
885
|
+
ids: payload?.ids ?? [itemId],
|
|
886
|
+
from: payload?.index ?? -1,
|
|
887
|
+
to: ev.dropped && to ? { list: to.list, index: to.index } : null,
|
|
888
|
+
combine: to?.combine ?? null,
|
|
889
|
+
reason: ev.dropped ? 'drop' : 'cancel',
|
|
890
|
+
input: 'pointer',
|
|
891
|
+
});
|
|
892
|
+
const label = labelFor(itemId);
|
|
893
|
+
if (!ev.dropped)
|
|
894
|
+
announce(SAY.returned(label));
|
|
895
|
+
else if (to?.combine != null) {
|
|
896
|
+
announce(SAY.combined(label, labelFor(to.combine)));
|
|
897
|
+
}
|
|
898
|
+
else if (to && to.listUid === uid) {
|
|
899
|
+
announce(SAY.dropped(label, to.index + 1, ids.length));
|
|
900
|
+
}
|
|
901
|
+
else if (to)
|
|
902
|
+
announce(SAY.droppedInto(label, to.list));
|
|
903
|
+
}, [uid, order]);
|
|
904
|
+
const shared = useMemo(() => ({
|
|
905
|
+
uid,
|
|
906
|
+
id,
|
|
907
|
+
type,
|
|
908
|
+
orientation,
|
|
909
|
+
disabled,
|
|
910
|
+
preview: preview === true ? 'auto' : preview,
|
|
911
|
+
renderPreview,
|
|
912
|
+
previewSize,
|
|
913
|
+
dropMs: reducedMotion || dropAnimation === false
|
|
914
|
+
? 0
|
|
915
|
+
: dropAnimation === true
|
|
916
|
+
? DROP_MS
|
|
917
|
+
: dropAnimation,
|
|
918
|
+
styles,
|
|
919
|
+
liftedId: lifted?.id ?? null,
|
|
920
|
+
register,
|
|
921
|
+
order,
|
|
922
|
+
movingIds,
|
|
923
|
+
lift,
|
|
924
|
+
drop,
|
|
925
|
+
cancel,
|
|
926
|
+
step,
|
|
927
|
+
focusNeighbour,
|
|
928
|
+
dragStarted,
|
|
929
|
+
dragMoved,
|
|
930
|
+
dragEnded,
|
|
931
|
+
}), [
|
|
932
|
+
uid,
|
|
933
|
+
id,
|
|
934
|
+
type,
|
|
935
|
+
orientation,
|
|
936
|
+
disabled,
|
|
937
|
+
preview,
|
|
938
|
+
renderPreview,
|
|
939
|
+
previewSize,
|
|
940
|
+
dropAnimation,
|
|
941
|
+
reducedMotion,
|
|
942
|
+
styles,
|
|
943
|
+
lifted,
|
|
944
|
+
register,
|
|
945
|
+
order,
|
|
946
|
+
movingIds,
|
|
947
|
+
lift,
|
|
948
|
+
drop,
|
|
949
|
+
cancel,
|
|
950
|
+
step,
|
|
951
|
+
focusNeighbour,
|
|
952
|
+
dragStarted,
|
|
953
|
+
dragMoved,
|
|
954
|
+
dragEnded,
|
|
955
|
+
]);
|
|
956
|
+
const dropAccept = useMemo(() => (disabled ? refuse : widenAccept(accept, type)), [accept, type, disabled]);
|
|
957
|
+
return hx('box', {
|
|
958
|
+
ref: rootRef,
|
|
959
|
+
role: 'list',
|
|
960
|
+
'aria-orientation': orientation,
|
|
961
|
+
'data-testname': testname,
|
|
962
|
+
dropAccept,
|
|
963
|
+
onDragOver: disabled ? undefined : onDragOver,
|
|
964
|
+
onDragLeave: disabled ? undefined : onDragLeave,
|
|
965
|
+
onDrop: disabled ? undefined : onDropHere,
|
|
966
|
+
style: [
|
|
967
|
+
{ flexDirection: orientation === 'horizontal' ? 'row' : 'column' },
|
|
968
|
+
style,
|
|
969
|
+
// An in-window ghost is a box inside this list, and `zIndex` only
|
|
970
|
+
// sorts among siblings — so every list that *contains* the drag comes
|
|
971
|
+
// forward among its own, or the ghost paints under whatever is next.
|
|
972
|
+
// See the header.
|
|
973
|
+
carrying ? { zIndex: 1 } : null,
|
|
974
|
+
],
|
|
975
|
+
}, h(ListContext.Provider, { value: shared }, children));
|
|
976
|
+
}
|
|
977
|
+
// --- the item ---------------------------------------------------------------
|
|
978
|
+
/** The item's own look, under its `style`. */
|
|
979
|
+
function itemStyle(state, hasHandle) {
|
|
980
|
+
return { cursor: state.disabled || hasHandle ? undefined : 'grab' };
|
|
981
|
+
}
|
|
982
|
+
/** An item that contains the drag has to paint over its neighbours, or the
|
|
983
|
+
* ghost following the pointer slides under the next row — or, for the item
|
|
984
|
+
* that is a whole board column, under the next column. */
|
|
985
|
+
function liftStyle(lifted) {
|
|
986
|
+
return lifted ? { zIndex: 3 } : null;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* What a lifted or dragging item wears — the wash the palette already has
|
|
990
|
+
* for "this one", the way a hovered row does. It sits *over* the item's
|
|
991
|
+
* `style`, the way a `:hover` block would: it is state, not look, and an
|
|
992
|
+
* app that painted its rows `$surface` still gets to see which one it is
|
|
993
|
+
* holding. `styles.item(state)` merges over this, so it stays the seam.
|
|
994
|
+
*/
|
|
995
|
+
function washStyle(state, theme) {
|
|
996
|
+
return state.lifted || state.dragging
|
|
997
|
+
? { backgroundColor: theme.surfaceHover }
|
|
998
|
+
: null;
|
|
999
|
+
}
|
|
1000
|
+
/** The line at an edge: across the item and centred on the edge, inside the
|
|
1001
|
+
* item so a scrolling list carries it, logical edges so RTL costs nothing.
|
|
1002
|
+
* A combining item is outlined instead — the drop lands *on* it, and a line
|
|
1003
|
+
* beside it would say the opposite. */
|
|
1004
|
+
function indicatorStyle(mark, orientation, theme) {
|
|
1005
|
+
const base = {
|
|
1006
|
+
position: 'absolute',
|
|
1007
|
+
zIndex: 1,
|
|
1008
|
+
pointerEvents: 'none',
|
|
1009
|
+
};
|
|
1010
|
+
if (mark.combine) {
|
|
1011
|
+
return {
|
|
1012
|
+
...base,
|
|
1013
|
+
top: 0,
|
|
1014
|
+
bottom: 0,
|
|
1015
|
+
left: 0,
|
|
1016
|
+
right: 0,
|
|
1017
|
+
borderWidth: INDICATOR,
|
|
1018
|
+
borderColor: theme.accent,
|
|
1019
|
+
borderRadius: theme.radius,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const line = { ...base, backgroundColor: theme.accent };
|
|
1023
|
+
// `offset` walks the line back into the middle of the gap; at the ends,
|
|
1024
|
+
// where there is no gap, it is zero and the line is centred on the edge
|
|
1025
|
+
const at = -INDICATOR / 2 + mark.offset;
|
|
1026
|
+
if (orientation === 'vertical') {
|
|
1027
|
+
return {
|
|
1028
|
+
...line,
|
|
1029
|
+
left: 0,
|
|
1030
|
+
right: 0,
|
|
1031
|
+
height: INDICATOR,
|
|
1032
|
+
...(mark.edge === 'before' ? { top: at } : { bottom: at }),
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
...line,
|
|
1037
|
+
top: 0,
|
|
1038
|
+
bottom: 0,
|
|
1039
|
+
width: INDICATOR,
|
|
1040
|
+
...(mark.edge === 'before' ? { start: at } : { end: at }),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* What the default ghost is drawn on, *under* the item's own style.
|
|
1045
|
+
*
|
|
1046
|
+
* The ghost is the item, so it should look like the item: same padding, same
|
|
1047
|
+
* corners, same ground. An earlier cut drew a card of its own over the top —
|
|
1048
|
+
* a surface, a hairline and the theme's radius — and it was wrong in a way
|
|
1049
|
+
* that is easy to miss until an item has a look of its own. A chip lost its
|
|
1050
|
+
* rounded corners, and the border it gained ate two pixels of content box,
|
|
1051
|
+
* which is enough to wrap a word: "later" came out as "late" and "r".
|
|
1052
|
+
*
|
|
1053
|
+
* So the only thing here is a ground, and only when the item does not bring
|
|
1054
|
+
* one — a ghost with nothing behind it is a floating label, which is not
|
|
1055
|
+
* what anybody meant. `styles.preview` and `renderPreview` are where a
|
|
1056
|
+
* lifted-card look belongs, for an app that wants one.
|
|
1057
|
+
*/
|
|
1058
|
+
function previewStyle(theme, hasGround) {
|
|
1059
|
+
return hasGround
|
|
1060
|
+
? { flexGrow: 1 }
|
|
1061
|
+
: { flexGrow: 1, backgroundColor: theme.surface };
|
|
1062
|
+
}
|
|
1063
|
+
/** The badge a multi-drag's ghost wears: how many are travelling. */
|
|
1064
|
+
function badgeStyle(theme) {
|
|
1065
|
+
return {
|
|
1066
|
+
position: 'absolute',
|
|
1067
|
+
top: -8,
|
|
1068
|
+
end: -8,
|
|
1069
|
+
minWidth: 18,
|
|
1070
|
+
paddingLeft: 5,
|
|
1071
|
+
paddingRight: 5,
|
|
1072
|
+
alignItems: 'center',
|
|
1073
|
+
justifyContent: 'center',
|
|
1074
|
+
borderRadius: 9,
|
|
1075
|
+
backgroundColor: theme.accent,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* `<ReorderItem id>` — one item. Draggable by any part of itself, unless a
|
|
1080
|
+
* `<ReorderHandle>` inside it takes that over; a tab stop, with the keyboard
|
|
1081
|
+
* model on it, for the same "unless".
|
|
1082
|
+
*/
|
|
1083
|
+
export function ReorderItem(props) {
|
|
1084
|
+
const { id, disabled: ownDisabled = false, dragData, dragActions, dragFromInteractive = false, 'aria-label': ariaLabel, style, 'data-testname': testname, children, } = props;
|
|
1085
|
+
const list = useList('ReorderItem');
|
|
1086
|
+
const theme = useTheme();
|
|
1087
|
+
const nodeRef = useRef(null);
|
|
1088
|
+
const handleRef = useRef(null);
|
|
1089
|
+
const [mark, setMark] = useState(null);
|
|
1090
|
+
/** Whether the drag in flight is inside this item — this one being
|
|
1091
|
+
* dragged, or a whole column of a board whose card is. */
|
|
1092
|
+
const carrying = useCarryingDrag(nodeRef);
|
|
1093
|
+
const [hasHandle, setHasHandle] = useState(false);
|
|
1094
|
+
const disabled = ownDisabled || list.disabled;
|
|
1095
|
+
const lifted = list.liftedId === id;
|
|
1096
|
+
const liftedRef = useRef(lifted);
|
|
1097
|
+
liftedRef.current = lifted;
|
|
1098
|
+
/** Whether *this* item is the one being dragged, rather than one that
|
|
1099
|
+
* merely contains it — see the header. `useDragSource`'s own
|
|
1100
|
+
* `isDragging` cannot tell, because the events bubble. */
|
|
1101
|
+
const [holdingOwn, setHoldingOwn] = useState(false);
|
|
1102
|
+
/** The live payload, from the press that started a drag to its end. */
|
|
1103
|
+
const payload = useRef(null);
|
|
1104
|
+
/** Where in the item the press landed, so the ghost appears under it
|
|
1105
|
+
* rather than jumping to the cursor; and how big the item was. */
|
|
1106
|
+
const grab = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
1107
|
+
/** What the press landed on, for the interactive-element question, and
|
|
1108
|
+
* where the window is on screen, for the drop flight. */
|
|
1109
|
+
const pressed = useRef(null);
|
|
1110
|
+
const windowOrigin = useRef({ x: 0, y: 0 });
|
|
1111
|
+
/** Where the ghost was last seen, in screen coordinates, and where the
|
|
1112
|
+
* landing copy is now — an offset from the item's own origin, easing to
|
|
1113
|
+
* nothing. */
|
|
1114
|
+
const lastGhost = useRef(null);
|
|
1115
|
+
const [flight, setFlight] = useState(null);
|
|
1116
|
+
const ticks = useRef({
|
|
1117
|
+
layout: null,
|
|
1118
|
+
timer: null,
|
|
1119
|
+
});
|
|
1120
|
+
const label = useCallback(() => ariaLabel ?? labelOf(nodeRef.current), [ariaLabel]);
|
|
1121
|
+
useEffect(() => list.register({
|
|
1122
|
+
id,
|
|
1123
|
+
node: nodeRef,
|
|
1124
|
+
setMark,
|
|
1125
|
+
focus: () => {
|
|
1126
|
+
(handleRef.current?.current ?? nodeRef.current)?.focus();
|
|
1127
|
+
},
|
|
1128
|
+
label,
|
|
1129
|
+
}), [id, list.register, label]);
|
|
1130
|
+
/** Nothing may outlive the item: a flight tick that fires after unmount
|
|
1131
|
+
* would set state on a component that is gone. */
|
|
1132
|
+
useEffect(() => () => {
|
|
1133
|
+
cancelAfterLayout(ticks.current.layout);
|
|
1134
|
+
cancelLater(ticks.current.timer);
|
|
1135
|
+
}, []);
|
|
1136
|
+
/**
|
|
1137
|
+
* Should the drop be animated at all?
|
|
1138
|
+
*
|
|
1139
|
+
* The flight ends at where the item *is*, which is the right picture for
|
|
1140
|
+
* exactly two endings: a move inside this list, where it is somewhere new,
|
|
1141
|
+
* and a drag that landed nowhere, where flying back to an unchanged
|
|
1142
|
+
* position is precisely "it returned". Every other ending leaves the item
|
|
1143
|
+
* where it started while the drop *did* something — a merge into another
|
|
1144
|
+
* item, a copy taken by another list — and flying home there says the
|
|
1145
|
+
* opposite of what happened: it reads as a rejection. Those endings let
|
|
1146
|
+
* the ghost go where the pointer left it.
|
|
1147
|
+
*/
|
|
1148
|
+
const flies = (ev, carried) => {
|
|
1149
|
+
if (!ev.dropped || ev.action === null)
|
|
1150
|
+
return true; // it went nowhere
|
|
1151
|
+
const to = carried?.to;
|
|
1152
|
+
return Boolean(to && to.listUid === list.uid && to.combine === null);
|
|
1153
|
+
};
|
|
1154
|
+
/**
|
|
1155
|
+
* Fly a copy of the item from where the pointer let go to where the item
|
|
1156
|
+
* landed, then let it go.
|
|
1157
|
+
*
|
|
1158
|
+
* Two things make this the shape it is. The landing place is only real
|
|
1159
|
+
* once the layout that moved the item has run — hence the tick — and the
|
|
1160
|
+
* copy travels in the *item's own* coordinates, as an offset easing to
|
|
1161
|
+
* zero, so it is a box inside the list rather than a window over it (see
|
|
1162
|
+
* the header). The screen point the pointer let go at is turned into that
|
|
1163
|
+
* offset here, which is the only thing the window's origin is needed for.
|
|
1164
|
+
*/
|
|
1165
|
+
const flyHome = useCallback((from, ms) => {
|
|
1166
|
+
ticks.current.layout = afterLayout(() => {
|
|
1167
|
+
ticks.current.layout = null;
|
|
1168
|
+
const node = nodeRef.current;
|
|
1169
|
+
if (!node) {
|
|
1170
|
+
setFlight(null);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const scale = scaleOf(node);
|
|
1174
|
+
const dx = from.x - (windowOrigin.current.x + node.abs.x / scale);
|
|
1175
|
+
const dy = from.y - (windowOrigin.current.y + node.abs.y / scale);
|
|
1176
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
|
|
1177
|
+
setFlight(null);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const started = Date.now();
|
|
1181
|
+
const stepOn = () => {
|
|
1182
|
+
const t = Math.min((Date.now() - started) / ms, 1);
|
|
1183
|
+
if (t >= 1) {
|
|
1184
|
+
ticks.current.timer = null;
|
|
1185
|
+
setFlight(null);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const left = 1 - ease(t);
|
|
1189
|
+
setFlight({ x: dx * left, y: dy * left });
|
|
1190
|
+
ticks.current.timer = later(stepOn, FLIGHT_STEP_MS);
|
|
1191
|
+
};
|
|
1192
|
+
stepOn();
|
|
1193
|
+
});
|
|
1194
|
+
}, []);
|
|
1195
|
+
/** Is this event about this item's own drag source — the item box, or the
|
|
1196
|
+
* handle the drag props were spread on — rather than a descendant's? */
|
|
1197
|
+
const ownSource = (ev) => ev.target === nodeRef.current ||
|
|
1198
|
+
ev.target === (handleRef.current?.current ?? null);
|
|
1199
|
+
const { dragProps, isDragging, position } = useDragSource({
|
|
1200
|
+
data: { [list.type]: () => payload.current, ...dragData },
|
|
1201
|
+
actions: dragActions ?? ['move'],
|
|
1202
|
+
onDragStart: (ev) => {
|
|
1203
|
+
if (!ownSource(ev))
|
|
1204
|
+
return;
|
|
1205
|
+
// A press on a control inside the item belongs to the control, not to
|
|
1206
|
+
// the item around it — core arms from the nearest draggable ancestor,
|
|
1207
|
+
// so this is the layer's own answer. Cancelling here leaves the
|
|
1208
|
+
// gesture as ordinary mouse events, which is what the control wanted.
|
|
1209
|
+
if (!dragFromInteractive &&
|
|
1210
|
+
!hasHandle &&
|
|
1211
|
+
pressedControl(pressed.current, nodeRef.current)) {
|
|
1212
|
+
ev.preventDefault();
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const node = nodeRef.current;
|
|
1216
|
+
const scale = scaleOf(node);
|
|
1217
|
+
if (node) {
|
|
1218
|
+
const { x, y, width, height } = node.abs;
|
|
1219
|
+
grab.current = {
|
|
1220
|
+
x: ev.x - x / scale,
|
|
1221
|
+
y: ev.y - y / scale,
|
|
1222
|
+
width: width / scale,
|
|
1223
|
+
height: height / scale,
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
// the window's own origin on screen, which the ghost's flight home
|
|
1227
|
+
// needs and no ref reports: the same point in both spaces, once.
|
|
1228
|
+
windowOrigin.current = { x: ev.screenX - ev.x, y: ev.screenY - ev.y };
|
|
1229
|
+
const { index, ids } = list.dragStarted(id);
|
|
1230
|
+
payload.current = {
|
|
1231
|
+
listUid: list.uid,
|
|
1232
|
+
list: list.id,
|
|
1233
|
+
id,
|
|
1234
|
+
ids,
|
|
1235
|
+
index,
|
|
1236
|
+
};
|
|
1237
|
+
activeDrag = {
|
|
1238
|
+
payload: payload.current,
|
|
1239
|
+
over: null,
|
|
1240
|
+
source: node,
|
|
1241
|
+
inline: ghostMode(list.preview) === 'inline',
|
|
1242
|
+
};
|
|
1243
|
+
setHoldingOwn(true);
|
|
1244
|
+
announceDragChanged();
|
|
1245
|
+
},
|
|
1246
|
+
onDrag: (ev) => {
|
|
1247
|
+
if (ownSource(ev))
|
|
1248
|
+
list.dragMoved(id);
|
|
1249
|
+
},
|
|
1250
|
+
onDragEnd: (ev) => {
|
|
1251
|
+
if (!ownSource(ev))
|
|
1252
|
+
return;
|
|
1253
|
+
setHoldingOwn(false);
|
|
1254
|
+
const carried = payload.current;
|
|
1255
|
+
payload.current = null;
|
|
1256
|
+
activeDrag = null;
|
|
1257
|
+
announceDragChanged();
|
|
1258
|
+
const from = lastGhost.current;
|
|
1259
|
+
lastGhost.current = null;
|
|
1260
|
+
list.dragEnded(id, ev, carried);
|
|
1261
|
+
if (from &&
|
|
1262
|
+
list.dropMs > 0 &&
|
|
1263
|
+
list.preview !== false &&
|
|
1264
|
+
flies(ev, carried)) {
|
|
1265
|
+
flyHome(from, list.dropMs);
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
// --- the keyboard --------------------------------------------------------
|
|
1270
|
+
const onKeyDown = (ev) => {
|
|
1271
|
+
if (disabled)
|
|
1272
|
+
return;
|
|
1273
|
+
const key = ev.keysym;
|
|
1274
|
+
const rtl = nodeRef.current?.direction === 'rtl';
|
|
1275
|
+
const [prev, next] = list.orientation === 'vertical'
|
|
1276
|
+
? [XK_UP, XK_DOWN]
|
|
1277
|
+
: rtl
|
|
1278
|
+
? [XK_RIGHT, XK_LEFT]
|
|
1279
|
+
: [XK_LEFT, XK_RIGHT];
|
|
1280
|
+
const isLifted = liftedRef.current;
|
|
1281
|
+
if (key === XK_RETURN || ev.codepoint === 32) {
|
|
1282
|
+
if (isLifted)
|
|
1283
|
+
list.drop();
|
|
1284
|
+
else
|
|
1285
|
+
list.lift(id);
|
|
1286
|
+
ev.preventDefault();
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (key === XK_ESCAPE) {
|
|
1290
|
+
if (!isLifted)
|
|
1291
|
+
return;
|
|
1292
|
+
list.cancel();
|
|
1293
|
+
ev.preventDefault();
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const delta = key === prev ? -1 : key === next ? 1 : 0;
|
|
1297
|
+
if (delta !== 0) {
|
|
1298
|
+
if (isLifted)
|
|
1299
|
+
list.step(id, delta);
|
|
1300
|
+
else
|
|
1301
|
+
list.focusNeighbour(id, delta);
|
|
1302
|
+
ev.preventDefault();
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (isLifted && (key === XK_HOME || key === XK_END)) {
|
|
1306
|
+
list.step(id, key === XK_HOME ? 'home' : 'end');
|
|
1307
|
+
ev.preventDefault();
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
/** Focus leaving a lifted item drops it where it is — a lifted item nobody
|
|
1311
|
+
* is holding is a list that will not sit still. */
|
|
1312
|
+
const onBlur = () => {
|
|
1313
|
+
if (liftedRef.current)
|
|
1314
|
+
list.drop();
|
|
1315
|
+
};
|
|
1316
|
+
const attachHandle = useCallback((ref) => {
|
|
1317
|
+
handleRef.current = ref;
|
|
1318
|
+
setHasHandle(true);
|
|
1319
|
+
return () => {
|
|
1320
|
+
if (handleRef.current === ref)
|
|
1321
|
+
handleRef.current = null;
|
|
1322
|
+
setHasHandle(false);
|
|
1323
|
+
};
|
|
1324
|
+
}, []);
|
|
1325
|
+
const drag = disabled ? NO_DRAG : dragProps;
|
|
1326
|
+
/** The hook reports a drag anywhere beneath this item; only ours counts. */
|
|
1327
|
+
const dragging = isDragging && holdingOwn;
|
|
1328
|
+
const moving = dragging || lifted ? list.movingIds(id) : [id];
|
|
1329
|
+
const state = {
|
|
1330
|
+
id,
|
|
1331
|
+
dragging,
|
|
1332
|
+
lifted,
|
|
1333
|
+
disabled,
|
|
1334
|
+
edge: mark?.edge ?? null,
|
|
1335
|
+
combining: Boolean(mark?.combine),
|
|
1336
|
+
accepted: (holdingOwn && position?.accepted) ?? false,
|
|
1337
|
+
preview: false,
|
|
1338
|
+
ids: moving,
|
|
1339
|
+
};
|
|
1340
|
+
// A new object per render, deliberately: `dragProps` is new per render
|
|
1341
|
+
// too, and what it keeps current is one handle box and whoever called
|
|
1342
|
+
// `useReorderItem()` — both of which want the state of *this* render.
|
|
1343
|
+
const itemShared = {
|
|
1344
|
+
state,
|
|
1345
|
+
disabled,
|
|
1346
|
+
dragProps: drag,
|
|
1347
|
+
onKeyDown,
|
|
1348
|
+
onBlur,
|
|
1349
|
+
styles: list.styles,
|
|
1350
|
+
attachHandle,
|
|
1351
|
+
};
|
|
1352
|
+
const previewState = { ...state, preview: true };
|
|
1353
|
+
const content = typeof children === 'function' ? children(state) : children;
|
|
1354
|
+
const size = typeof list.previewSize === 'function'
|
|
1355
|
+
? list.previewSize(previewState)
|
|
1356
|
+
: (list.previewSize ?? grab.current);
|
|
1357
|
+
const ghostAt = holdingOwn &&
|
|
1358
|
+
position && {
|
|
1359
|
+
x: position.x - grab.current.x,
|
|
1360
|
+
y: position.y - grab.current.y,
|
|
1361
|
+
};
|
|
1362
|
+
if (dragging && ghostAt)
|
|
1363
|
+
lastGhost.current = ghostAt;
|
|
1364
|
+
// Where the ghost is drawn. A popup can leave the window and draws over
|
|
1365
|
+
// other applications; an in-window copy cannot, and is the opt-in.
|
|
1366
|
+
const mode = ghostMode(list.preview);
|
|
1367
|
+
/** The item's own origin on screen, which turns a pointer position into an
|
|
1368
|
+
* offset inside the item — what an inline ghost is placed by, and what the
|
|
1369
|
+
* drop flight eases to zero. */
|
|
1370
|
+
const originOnScreen = () => {
|
|
1371
|
+
const node = nodeRef.current;
|
|
1372
|
+
if (!node)
|
|
1373
|
+
return null;
|
|
1374
|
+
const scale = scaleOf(node);
|
|
1375
|
+
return {
|
|
1376
|
+
x: windowOrigin.current.x + node.abs.x / scale,
|
|
1377
|
+
y: windowOrigin.current.y + node.abs.y / scale,
|
|
1378
|
+
};
|
|
1379
|
+
};
|
|
1380
|
+
const origin = mode === 'inline' && ghostAt ? originOnScreen() : null;
|
|
1381
|
+
/** The offset the in-window copy is drawn at: the pointer's while the item
|
|
1382
|
+
* is held, and the flight's easing to zero once it has landed. One box
|
|
1383
|
+
* either way. */
|
|
1384
|
+
const inlineAt = flight ??
|
|
1385
|
+
(origin && ghostAt
|
|
1386
|
+
? { x: ghostAt.x - origin.x, y: ghostAt.y - origin.y }
|
|
1387
|
+
: null);
|
|
1388
|
+
const showPopup = Boolean(ghostAt) && mode === 'popup';
|
|
1389
|
+
/** The look: the defaults, then the app's `style`. */
|
|
1390
|
+
const ownStyle = [itemStyle(state, hasHandle), style].filter((s) => Boolean(s));
|
|
1391
|
+
/** Whether the item paints its own ground, which decides whether the ghost
|
|
1392
|
+
* needs one of its own. */
|
|
1393
|
+
// `flattenStyle` takes one style or a flat list; ours may hold a nested
|
|
1394
|
+
// one, since `style` itself accepts an array.
|
|
1395
|
+
const hasGround = flattenStyle(ownStyle.flat()).backgroundColor !== undefined;
|
|
1396
|
+
/** The state, over the look: the lift, the wash, then the seam. */
|
|
1397
|
+
const stateStyle = [
|
|
1398
|
+
liftStyle(carrying),
|
|
1399
|
+
washStyle(state, theme),
|
|
1400
|
+
list.styles?.item?.(state) || null,
|
|
1401
|
+
].filter((s) => Boolean(s));
|
|
1402
|
+
/** The ghost's content, drawn twice: once in the popup that follows the
|
|
1403
|
+
* pointer, once in the copy that flies home after the drop. */
|
|
1404
|
+
const ghostBody = () => h(ItemContext.Provider, { value: inertItem(previewState, list.styles) }, list.renderPreview
|
|
1405
|
+
? list.renderPreview(previewState)
|
|
1406
|
+
: hx('box', {
|
|
1407
|
+
style: [
|
|
1408
|
+
// the ground goes *under* the item's own style, so a chip
|
|
1409
|
+
// keeps its corners and its width — see `previewStyle`
|
|
1410
|
+
previewStyle(theme, hasGround),
|
|
1411
|
+
...ownStyle,
|
|
1412
|
+
// the seam sees the ghost too, with `preview` set
|
|
1413
|
+
list.styles?.item?.(previewState) || null,
|
|
1414
|
+
list.styles?.preview,
|
|
1415
|
+
],
|
|
1416
|
+
}, typeof children === 'function' ? children(previewState) : children));
|
|
1417
|
+
return hx('box', {
|
|
1418
|
+
ref: nodeRef,
|
|
1419
|
+
role: 'listitem',
|
|
1420
|
+
'aria-label': ariaLabel,
|
|
1421
|
+
'aria-description': hasHandle || disabled ? undefined : HINT,
|
|
1422
|
+
'data-testname': testname,
|
|
1423
|
+
// Every press is remembered, whether or not it becomes a drag: at the
|
|
1424
|
+
// threshold there is no event saying where the gesture began. Capture
|
|
1425
|
+
// phase, so a child that stops propagation cannot hide it.
|
|
1426
|
+
onMouseDownCapture: (ev) => {
|
|
1427
|
+
pressed.current = ev.target;
|
|
1428
|
+
},
|
|
1429
|
+
// without a handle the item is the drag source and the tab stop
|
|
1430
|
+
...(hasHandle ? {} : drag),
|
|
1431
|
+
...(hasHandle
|
|
1432
|
+
? {}
|
|
1433
|
+
: {
|
|
1434
|
+
focusable: !disabled,
|
|
1435
|
+
onKeyDown: disabled ? undefined : onKeyDown,
|
|
1436
|
+
onBlur,
|
|
1437
|
+
}),
|
|
1438
|
+
style: [...ownStyle, ...stateStyle],
|
|
1439
|
+
}, h(ItemContext.Provider, { key: 'content', value: itemShared }, content), mark !== null &&
|
|
1440
|
+
hx('box', {
|
|
1441
|
+
key: 'indicator',
|
|
1442
|
+
'data-testname': testname ? `${testname}-indicator` : undefined,
|
|
1443
|
+
style: [
|
|
1444
|
+
indicatorStyle(mark, list.orientation, theme),
|
|
1445
|
+
list.styles?.indicator,
|
|
1446
|
+
],
|
|
1447
|
+
}), inlineAt !== null &&
|
|
1448
|
+
hx('box', {
|
|
1449
|
+
key: 'ghost',
|
|
1450
|
+
// the same box before and after the release: the preview while the
|
|
1451
|
+
// pointer holds it, the flight once it has landed
|
|
1452
|
+
'data-testname': testname
|
|
1453
|
+
? `${testname}-${flight ? 'flight' : 'preview'}`
|
|
1454
|
+
: undefined,
|
|
1455
|
+
style: {
|
|
1456
|
+
position: 'absolute',
|
|
1457
|
+
left: inlineAt.x,
|
|
1458
|
+
top: inlineAt.y,
|
|
1459
|
+
width: Math.max(1, Math.ceil(size.width)),
|
|
1460
|
+
height: Math.max(1, Math.ceil(size.height)),
|
|
1461
|
+
zIndex: 2,
|
|
1462
|
+
// the copy is a picture of the drop, never a target for the
|
|
1463
|
+
// press that follows it
|
|
1464
|
+
pointerEvents: 'none',
|
|
1465
|
+
},
|
|
1466
|
+
}, ghostBody()), showPopup &&
|
|
1467
|
+
ghostAt &&
|
|
1468
|
+
hx('popup', {
|
|
1469
|
+
key: 'preview',
|
|
1470
|
+
dragPreview: true,
|
|
1471
|
+
// a real window, and the card in it is rounded: see the header
|
|
1472
|
+
transparent: true,
|
|
1473
|
+
theme,
|
|
1474
|
+
x: Math.round(ghostAt.x),
|
|
1475
|
+
y: Math.round(ghostAt.y),
|
|
1476
|
+
width: Math.max(1, Math.ceil(size.width)),
|
|
1477
|
+
height: Math.max(1, Math.ceil(size.height)),
|
|
1478
|
+
'data-testname': testname ? `${testname}-preview` : undefined,
|
|
1479
|
+
}, ghostBody(), moving.length > 1 &&
|
|
1480
|
+
hx('box', {
|
|
1481
|
+
key: 'badge',
|
|
1482
|
+
'data-testname': testname ? `${testname}-count` : undefined,
|
|
1483
|
+
style: badgeStyle(theme),
|
|
1484
|
+
}, hx('text', { style: { fontSize: 11, color: theme.accentText } }, String(moving.length)))));
|
|
1485
|
+
}
|
|
1486
|
+
// --- the handle -------------------------------------------------------------
|
|
1487
|
+
/**
|
|
1488
|
+
* `<ReorderHandle>` — the one part of an item that drags it. Rendering one
|
|
1489
|
+
* inside a `<ReorderItem>` is the whole opt-in: the item stops being the
|
|
1490
|
+
* press target and the tab stop, and this is both.
|
|
1491
|
+
*/
|
|
1492
|
+
export function ReorderHandle(props) {
|
|
1493
|
+
const { 'aria-label': ariaLabel = 'Drag to reorder', style, 'data-testname': testname, children, } = props;
|
|
1494
|
+
const item = useContext(ItemContext);
|
|
1495
|
+
if (!item) {
|
|
1496
|
+
throw new Error('@react-x11/components: <ReorderHandle> has to be inside a <ReorderItem>.');
|
|
1497
|
+
}
|
|
1498
|
+
const ref = useRef(null);
|
|
1499
|
+
const { attachHandle } = item;
|
|
1500
|
+
useLayoutEffect(() => attachHandle(ref), [attachHandle]);
|
|
1501
|
+
return hx('box', {
|
|
1502
|
+
ref,
|
|
1503
|
+
role: 'button',
|
|
1504
|
+
'aria-label': ariaLabel,
|
|
1505
|
+
'aria-description': item.disabled ? undefined : HINT,
|
|
1506
|
+
'data-testname': testname,
|
|
1507
|
+
focusable: !item.disabled,
|
|
1508
|
+
...item.dragProps,
|
|
1509
|
+
onKeyDown: item.disabled ? undefined : item.onKeyDown,
|
|
1510
|
+
onBlur: item.onBlur,
|
|
1511
|
+
style: [
|
|
1512
|
+
{
|
|
1513
|
+
flexDirection: 'row',
|
|
1514
|
+
alignItems: 'center',
|
|
1515
|
+
alignSelf: 'center',
|
|
1516
|
+
padding: 2,
|
|
1517
|
+
borderRadius: 3,
|
|
1518
|
+
cursor: item.disabled ? undefined : 'grab',
|
|
1519
|
+
},
|
|
1520
|
+
!item.disabled && { ':hover': { backgroundColor: '$surfaceHover' } },
|
|
1521
|
+
item.styles?.handle,
|
|
1522
|
+
style,
|
|
1523
|
+
],
|
|
1524
|
+
}, children ??
|
|
1525
|
+
// two columns of core's three dots: the grip every list draws, made of
|
|
1526
|
+
// the affordance set rather than drawn here
|
|
1527
|
+
hx('box', { key: 'grip', style: { flexDirection: 'row' } }, h(Icon, {
|
|
1528
|
+
key: 'a',
|
|
1529
|
+
name: 'moreVertical',
|
|
1530
|
+
size: GRIP_DOT,
|
|
1531
|
+
color: '$textMuted',
|
|
1532
|
+
}), h(Icon, {
|
|
1533
|
+
key: 'b',
|
|
1534
|
+
name: 'moreVertical',
|
|
1535
|
+
size: GRIP_DOT,
|
|
1536
|
+
color: '$textMuted',
|
|
1537
|
+
})));
|
|
1538
|
+
}
|
|
1539
|
+
//# sourceMappingURL=index.js.map
|