@dxos/react-ui-mosaic 0.8.4-main.bc674ce → 0.8.4-main.c85a9c8dae

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.
Files changed (186) hide show
  1. package/dist/lib/browser/chunk-7S4PIFKU.mjs +953 -0
  2. package/dist/lib/browser/chunk-7S4PIFKU.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +596 -70
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/playwright/index.mjs +138 -0
  7. package/dist/lib/browser/playwright/index.mjs.map +7 -0
  8. package/dist/lib/browser/testing/index.mjs +61 -232
  9. package/dist/lib/browser/testing/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/chunk-MK6KU66S.mjs +955 -0
  11. package/dist/lib/node-esm/chunk-MK6KU66S.mjs.map +7 -0
  12. package/dist/lib/node-esm/index.mjs +596 -70
  13. package/dist/lib/node-esm/index.mjs.map +4 -4
  14. package/dist/lib/node-esm/meta.json +1 -1
  15. package/dist/lib/node-esm/playwright/index.mjs +140 -0
  16. package/dist/lib/node-esm/playwright/index.mjs.map +7 -0
  17. package/dist/lib/node-esm/testing/index.mjs +61 -232
  18. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  19. package/dist/types/src/components/Board/Board.d.ts +81 -0
  20. package/dist/types/src/components/Board/Board.d.ts.map +1 -0
  21. package/dist/types/src/components/{Mosaic/Mosaic.stories.d.ts → Board/Board.stories.d.ts} +15 -1
  22. package/dist/types/src/components/Board/Board.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/Board/Column.d.ts +40 -0
  24. package/dist/types/src/components/Board/Column.d.ts.map +1 -0
  25. package/dist/types/src/components/Board/Item.d.ts +9 -0
  26. package/dist/types/src/components/Board/Item.d.ts.map +1 -0
  27. package/dist/types/src/components/Board/index.d.ts +2 -0
  28. package/dist/types/src/components/Board/index.d.ts.map +1 -0
  29. package/dist/types/src/components/Focus/Focus.d.ts +5 -4
  30. package/dist/types/src/components/Focus/Focus.d.ts.map +1 -1
  31. package/dist/types/src/components/Focus/index.d.ts +0 -1
  32. package/dist/types/src/components/Focus/index.d.ts.map +1 -1
  33. package/dist/types/src/components/Mosaic/Container.d.ts +42 -0
  34. package/dist/types/src/components/Mosaic/Container.d.ts.map +1 -0
  35. package/dist/types/src/components/Mosaic/Mosaic.d.ts +35 -118
  36. package/dist/types/src/components/Mosaic/Mosaic.d.ts.map +1 -1
  37. package/dist/types/src/components/Mosaic/Placeholder.d.ts +21 -0
  38. package/dist/types/src/components/Mosaic/Placeholder.d.ts.map +1 -0
  39. package/dist/types/src/components/Mosaic/Root.d.ts +36 -0
  40. package/dist/types/src/components/Mosaic/Root.d.ts.map +1 -0
  41. package/dist/types/src/components/Mosaic/Stack.d.ts +23 -0
  42. package/dist/types/src/components/Mosaic/Stack.d.ts.map +1 -0
  43. package/dist/types/src/components/{Stack → Mosaic}/Stack.stories.d.ts +2 -2
  44. package/dist/types/src/components/Mosaic/Stack.stories.d.ts.map +1 -0
  45. package/dist/types/src/components/Mosaic/Tile.d.ts +48 -0
  46. package/dist/types/src/components/Mosaic/Tile.d.ts.map +1 -0
  47. package/dist/types/src/components/Mosaic/styles.d.ts.map +1 -1
  48. package/dist/types/src/components/Mosaic/types.d.ts +9 -2
  49. package/dist/types/src/components/Mosaic/types.d.ts.map +1 -1
  50. package/dist/types/src/components/index.d.ts +1 -4
  51. package/dist/types/src/components/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/index.d.ts +1 -0
  53. package/dist/types/src/hooks/index.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts +15 -0
  55. package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts.map +1 -0
  56. package/dist/types/src/hooks/useEventHandlerAdapter.d.ts +1 -1
  57. package/dist/types/src/hooks/useEventHandlerAdapter.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useVisibleItems.d.ts +2 -2
  59. package/dist/types/src/hooks/useVisibleItems.d.ts.map +1 -1
  60. package/dist/types/src/playwright/board-manager.d.ts +45 -0
  61. package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
  62. package/dist/types/src/playwright/index.d.ts +9 -0
  63. package/dist/types/src/playwright/index.d.ts.map +1 -0
  64. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  65. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  66. package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
  67. package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
  68. package/dist/types/src/testing/CardContainer.d.ts +5 -4
  69. package/dist/types/src/testing/CardContainer.d.ts.map +1 -1
  70. package/dist/types/src/testing/DefaultStackTile.d.ts +2 -2
  71. package/dist/types/src/testing/DefaultStackTile.d.ts.map +1 -1
  72. package/dist/types/src/testing/index.d.ts +1 -1
  73. package/dist/types/src/testing/index.d.ts.map +1 -1
  74. package/dist/types/src/testing/types.d.ts +21 -0
  75. package/dist/types/src/testing/types.d.ts.map +1 -0
  76. package/dist/types/src/translations.d.ts +5 -1
  77. package/dist/types/src/translations.d.ts.map +1 -1
  78. package/dist/types/tsconfig.tsbuildinfo +1 -1
  79. package/package.json +43 -28
  80. package/src/components/Board/Board.stories.tsx +273 -0
  81. package/src/components/Board/Board.tsx +191 -0
  82. package/src/components/Board/Column.tsx +300 -0
  83. package/src/components/Board/Item.tsx +116 -0
  84. package/src/components/{Layout → Board}/index.ts +1 -1
  85. package/src/components/Focus/Focus.tsx +26 -17
  86. package/src/components/Focus/index.ts +0 -2
  87. package/src/components/Mosaic/Container.tsx +289 -0
  88. package/src/components/Mosaic/Mosaic.tsx +35 -913
  89. package/src/components/Mosaic/Placeholder.tsx +133 -0
  90. package/src/components/Mosaic/Root.tsx +292 -0
  91. package/src/components/{Stack → Mosaic}/Stack.stories.tsx +30 -26
  92. package/src/components/Mosaic/Stack.tsx +246 -0
  93. package/src/components/Mosaic/Tile.tsx +242 -0
  94. package/src/components/Mosaic/extension.ts +1 -1
  95. package/src/components/Mosaic/styles.ts +9 -8
  96. package/src/components/Mosaic/types.ts +15 -3
  97. package/src/components/index.ts +1 -4
  98. package/src/hooks/index.ts +1 -0
  99. package/src/hooks/useContainerDebug.tsx +4 -2
  100. package/src/hooks/useDefaultColumnEventHandler.ts +35 -0
  101. package/src/hooks/useEventHandlerAdapter.ts +15 -6
  102. package/src/hooks/useVisibleItems.ts +7 -2
  103. package/src/playwright/board-manager.ts +154 -0
  104. package/src/playwright/index.ts +13 -0
  105. package/src/playwright/playwright.config.ts +17 -0
  106. package/src/playwright/smoke.spec.ts +138 -0
  107. package/src/testing/CardContainer.tsx +30 -18
  108. package/src/testing/DefaultStackTile.tsx +27 -16
  109. package/src/testing/index.ts +1 -1
  110. package/src/testing/types.ts +38 -0
  111. package/src/translations.ts +5 -1
  112. package/dist/lib/browser/chunk-UMBPEG4F.mjs +0 -1637
  113. package/dist/lib/browser/chunk-UMBPEG4F.mjs.map +0 -7
  114. package/dist/lib/browser/index.css +0 -13
  115. package/dist/lib/browser/index.css.map +0 -7
  116. package/dist/lib/browser/testing/index.css +0 -13
  117. package/dist/lib/browser/testing/index.css.map +0 -7
  118. package/dist/lib/node-esm/chunk-X6GV7SMK.mjs +0 -1639
  119. package/dist/lib/node-esm/chunk-X6GV7SMK.mjs.map +0 -7
  120. package/dist/lib/node-esm/index.css +0 -13
  121. package/dist/lib/node-esm/index.css.map +0 -7
  122. package/dist/lib/node-esm/testing/index.css +0 -13
  123. package/dist/lib/node-esm/testing/index.css.map +0 -7
  124. package/dist/types/src/components/Card/Card.d.ts +0 -121
  125. package/dist/types/src/components/Card/Card.d.ts.map +0 -1
  126. package/dist/types/src/components/Card/Card.stories.d.ts +0 -21
  127. package/dist/types/src/components/Card/Card.stories.d.ts.map +0 -1
  128. package/dist/types/src/components/Card/CardDragPreview.d.ts +0 -6
  129. package/dist/types/src/components/Card/CardDragPreview.d.ts.map +0 -1
  130. package/dist/types/src/components/Card/index.d.ts +0 -4
  131. package/dist/types/src/components/Card/index.d.ts.map +0 -1
  132. package/dist/types/src/components/Card/styles.d.ts +0 -13
  133. package/dist/types/src/components/Card/styles.d.ts.map +0 -1
  134. package/dist/types/src/components/Focus/styles.d.ts +0 -6
  135. package/dist/types/src/components/Focus/styles.d.ts.map +0 -1
  136. package/dist/types/src/components/Image/Image.d.ts +0 -14
  137. package/dist/types/src/components/Image/Image.d.ts.map +0 -1
  138. package/dist/types/src/components/Image/Image.stories.d.ts +0 -33
  139. package/dist/types/src/components/Image/Image.stories.d.ts.map +0 -1
  140. package/dist/types/src/components/Image/index.d.ts +0 -2
  141. package/dist/types/src/components/Image/index.d.ts.map +0 -1
  142. package/dist/types/src/components/Layout/Layout.d.ts +0 -33
  143. package/dist/types/src/components/Layout/Layout.d.ts.map +0 -1
  144. package/dist/types/src/components/Layout/Layout.stories.d.ts +0 -10
  145. package/dist/types/src/components/Layout/Layout.stories.d.ts.map +0 -1
  146. package/dist/types/src/components/Layout/index.d.ts +0 -2
  147. package/dist/types/src/components/Layout/index.d.ts.map +0 -1
  148. package/dist/types/src/components/Mosaic/Mosaic.stories.d.ts.map +0 -1
  149. package/dist/types/src/components/Scrollable/Scrollable.d.ts +0 -24
  150. package/dist/types/src/components/Scrollable/Scrollable.d.ts.map +0 -1
  151. package/dist/types/src/components/Scrollable/Scrollable.stories.d.ts +0 -10
  152. package/dist/types/src/components/Scrollable/Scrollable.stories.d.ts.map +0 -1
  153. package/dist/types/src/components/Scrollable/index.d.ts +0 -2
  154. package/dist/types/src/components/Scrollable/index.d.ts.map +0 -1
  155. package/dist/types/src/components/Stack/Stack.d.ts +0 -22
  156. package/dist/types/src/components/Stack/Stack.d.ts.map +0 -1
  157. package/dist/types/src/components/Stack/Stack.stories.d.ts.map +0 -1
  158. package/dist/types/src/components/Stack/index.d.ts +0 -2
  159. package/dist/types/src/components/Stack/index.d.ts.map +0 -1
  160. package/dist/types/src/testing/Board.d.ts +0 -45
  161. package/dist/types/src/testing/Board.d.ts.map +0 -1
  162. package/dist/types/src/testing/Slot.stories.d.ts +0 -14
  163. package/dist/types/src/testing/Slot.stories.d.ts.map +0 -1
  164. package/dist/types/src/testing/Virtualizer.stories.d.ts +0 -12
  165. package/dist/types/src/testing/Virtualizer.stories.d.ts.map +0 -1
  166. package/src/components/Card/Card.stories.tsx +0 -148
  167. package/src/components/Card/Card.tsx +0 -458
  168. package/src/components/Card/CardDragPreview.tsx +0 -23
  169. package/src/components/Card/index.ts +0 -8
  170. package/src/components/Card/styles.ts +0 -23
  171. package/src/components/Focus/styles.ts +0 -24
  172. package/src/components/Image/Image.stories.tsx +0 -86
  173. package/src/components/Image/Image.tsx +0 -223
  174. package/src/components/Image/index.ts +0 -5
  175. package/src/components/Layout/Layout.stories.tsx +0 -59
  176. package/src/components/Layout/Layout.tsx +0 -108
  177. package/src/components/Mosaic/Mosaic.stories.tsx +0 -113
  178. package/src/components/Scrollable/Scrollable.stories.tsx +0 -59
  179. package/src/components/Scrollable/Scrollable.tsx +0 -88
  180. package/src/components/Scrollable/index.ts +0 -5
  181. package/src/components/Scrollable/scrollable.css +0 -20
  182. package/src/components/Stack/Stack.tsx +0 -210
  183. package/src/components/Stack/index.ts +0 -5
  184. package/src/testing/Board.tsx +0 -278
  185. package/src/testing/Slot.stories.tsx +0 -101
  186. package/src/testing/Virtualizer.stories.tsx +0 -133
@@ -2,928 +2,50 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
6
5
  import {
7
- type ElementDragPayload,
8
- draggable,
9
- dropTargetForElements,
10
- monitorForElements,
11
- } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
12
- import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
13
- import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
14
- import { type DragLocationHistory, type DropTargetRecord } from '@atlaskit/pragmatic-drag-and-drop/types';
15
- import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
6
+ MosaicContainer,
7
+ type MosaicContainerProps,
8
+ type MosaicContainerState,
9
+ useMosaicContainerContext,
10
+ } from './Container';
16
11
  import {
17
- type Edge,
18
- attachClosestEdge,
19
- extractClosestEdge,
20
- } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
21
- import {
22
- DropIndicator as NativeDropIndicator,
23
- type DropIndicatorProps as NativeDropIndicatorProps,
24
- } from '@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box';
25
- import { composeRefs, useComposedRefs } from '@radix-ui/react-compose-refs';
26
- import { createContext } from '@radix-ui/react-context';
27
- import { Primitive } from '@radix-ui/react-primitive';
28
- import { Slot } from '@radix-ui/react-slot';
29
- import { bind } from 'bind-event-listener';
30
- import React, {
31
- type CSSProperties,
32
- type PropsWithChildren,
33
- type ReactNode,
34
- forwardRef,
35
- useCallback,
36
- useEffect,
37
- useLayoutEffect,
38
- useMemo,
39
- useRef,
40
- useState,
41
- } from 'react';
42
- import { createPortal } from 'react-dom';
43
-
44
- import { log } from '@dxos/log';
45
- import { type SlottableClassName, type ThemedClassName } from '@dxos/react-ui';
46
- import { mx } from '@dxos/ui-theme';
47
- import { isTruthy } from '@dxos/util';
48
-
49
- import { useFocus } from '../Focus';
50
- import { Scrollable, type ScrollableProps } from '../Scrollable';
51
- import { Stack, type StackProps, VirtualStack } from '../Stack';
52
-
53
- import {
54
- type AllowedAxis,
55
- type Axis,
56
- type MosaicContainerData,
57
- type MosaicData,
58
- type MosaicEventHandler,
59
- type MosaicPlaceholderData,
60
- type MosaicTileData,
61
- } from './types';
62
-
63
- //
64
- // Mosaic Drag-and-drop
65
- //
66
- // Drop targets:
67
- // - Placeholders exist to allow gaps but prevent deadspace when dragging within a container.
68
- // - When dragging over a Tile, the placeholder next to the closest edge is activated.
69
- // - Placeholders expand when active; event handlers are disabled while the container is scrolling.
70
- //
71
- // [Container]
72
- // - [Placeholder 0.5]
73
- // - [Tile 1 ]
74
- // - [Placeholder 1.5]
75
- // - [Tile 2 ]
76
- // - [Placeholder 2.5]
77
- // - [Tile 3 ]
78
- // - [Placeholder 3.5]
79
- //
80
- // Implementation Notes
81
- // - We use [Radix composition](https://www.radix-ui.com/primitives/docs/guides/composition) to factor out composible aspects (e.g., Focus, Mosaic, etc.)
82
- // - NOTE: Use Slottable only if needed to disambiguate; otherwise a suspected Radix bug causes compositional problems.
83
-
84
- //
85
- // Types
86
- //
87
-
88
- const getSourceData = <TData = any, TLocation = any>(
89
- source: ElementDragPayload,
90
- ): MosaicTileData<TData, TLocation> | null => {
91
- return source.data.type === 'tile' ? (source.data as MosaicTileData<TData, TLocation>) : null;
92
- };
93
-
94
- //
95
- // Context
96
- //
97
-
98
- type DraggingSource = {
99
- data: MosaicTileData;
100
- handler?: MosaicEventHandler;
101
- container?: Element;
102
- };
103
-
104
- type DraggingTarget = {
105
- data: MosaicData;
106
- handler?: MosaicEventHandler;
107
- };
108
-
109
- type DraggingState = {
110
- source: DraggingSource;
111
- target?: DraggingTarget;
112
- };
113
-
114
- type RootContextValue = {
115
- containers: Record<string, MosaicEventHandler>;
116
- addContainer: (container: MosaicEventHandler) => void;
117
- removeContainer: (id: string) => void;
118
- dragging?: DraggingState;
119
- };
120
-
121
- const [RootContextProvider, useRootContext] = createContext<RootContextValue>('MosaicRoot');
122
-
123
- //
124
- // Root
125
- //
126
-
127
- // State attribute: [&:has(>_[data-mosaic-debug=true])]
128
- const ROOT_DEBUG_ATTR = 'mosaic-debug';
129
-
130
- type RootProps = ThemedClassName<PropsWithChildren<{ asChild?: boolean; debug?: boolean }>>;
131
-
132
- const Root = forwardRef<HTMLDivElement, RootProps>(({ classNames, children, asChild, debug }, forwardedRef) => {
133
- const rootRef = useRef<HTMLDivElement>(null);
134
- const composedRef = useComposedRefs<HTMLDivElement>(rootRef, forwardedRef);
135
- const Root = asChild ? Slot : Primitive.div;
136
-
137
- const [handlers, setHandlers] = useState<Record<string, MosaicEventHandler>>({});
138
- const [dragging, setDragging] = useState<DraggingState | undefined>();
139
-
140
- const getSourceHandler = useCallback(
141
- (source: ElementDragPayload): { data: MosaicTileData; handler?: MosaicEventHandler } => {
142
- const data = source.data as MosaicTileData;
143
- return { data, handler: handlers[data.containerId] };
144
- },
145
- [handlers],
146
- );
147
-
148
- const getTargetHandler = useCallback(
149
- (location: DragLocationHistory): { data?: MosaicData; handler?: MosaicEventHandler } => {
150
- for (const target of location.current.dropTargets) {
151
- const data = target.data as MosaicData;
152
- let containerId: string;
153
- switch (data.type) {
154
- case 'tile':
155
- case 'placeholder':
156
- containerId = data.containerId;
157
- break;
158
- case 'container':
159
- containerId = data.id;
160
- break;
161
- }
162
-
163
- const handler = handlers[containerId];
164
- if (handler) {
165
- return { data, handler };
166
- }
167
- }
168
-
169
- return {};
170
- },
171
- [handlers],
172
- );
173
-
174
- useEffect(() => {
175
- const handleChange = ({ source, location }: { source: ElementDragPayload; location: DragLocationHistory }) => {
176
- const { data: sourceData } = getSourceHandler(source);
177
- const { data: targetData, handler } = getTargetHandler(location);
178
- setDragging((dragging) => {
179
- dragging?.target?.handler?.onCancel?.();
180
- return {
181
- source: {
182
- data: sourceData,
183
- handler: handlers[sourceData.containerId],
184
- // TOOD(burdon): Check id matches.
185
- container: location.initial.dropTargets.find((target) => target.data.type === 'container')?.element,
186
- },
187
- target: targetData && {
188
- data: targetData,
189
- handler,
190
- },
191
- };
192
- });
193
- };
194
-
195
- const handleCancel = () => {
196
- setDragging((dragging) => {
197
- requestAnimationFrame(() => {
198
- dragging?.target?.handler?.onCancel?.();
199
- dragging?.source?.container?.dispatchEvent(new CustomEvent('dnd:cancel', { bubbles: true }));
200
- });
201
-
202
- return undefined;
203
- });
204
- };
205
-
206
- let noTargetTimeout: NodeJS.Timeout;
207
-
208
- // Main controller.
209
- return combine(
210
- monitorForElements({
211
- /**
212
- * Dragging started within any container.
213
- */
214
- onDragStart: ({ source, location }) => {
215
- log('Root.onDragStart', {
216
- source: source.data,
217
- location: location.current.dropTargets.map((target) => target.data),
218
- });
219
-
220
- handleChange({ source, location });
221
- },
222
-
223
- /**
224
- * Dragging entered a new container.
225
- */
226
- onDropTargetChange: ({ source, location }) => {
227
- clearTimeout(noTargetTimeout);
228
- log('Root.onDropTargetChange', {
229
- source: source.data,
230
- location: location.current.dropTargets.map((target) => target.data),
231
- });
232
-
233
- // Stop dragging if there are no drop targets (or we are cancelling).
234
- if (location.current.dropTargets.length === 0) {
235
- noTargetTimeout = setTimeout(() => setDragging(undefined), 1_000);
236
- } else {
237
- handleChange({ source, location });
238
- }
239
- },
240
-
241
- /**
242
- * Dragging within any container.
243
- */
244
- onDrag: ({ source, location }) => {
245
- const { data } = getSourceHandler(source);
246
- const { handler } = getTargetHandler(location);
247
- if (handler) {
248
- const { clientX: x, clientY: y } = location.current.input;
249
- handler.onDrag?.({ source: data, position: { x, y } });
250
- }
251
- },
252
-
253
- /**
254
- * Dragging ended.
255
- */
256
- onDrop: ({ source, location }) => {
257
- log.info('Root.onDrop', {
258
- source: source.data,
259
- location: location.current.dropTargets.map((target) => target.data),
260
- });
261
-
262
- // Get the source container.
263
- const { data: sourceData, handler: sourceHandler } = getSourceHandler(source);
264
- if (!sourceHandler) {
265
- log.warn('invalid source', {
266
- source: sourceData,
267
- handlers: Object.keys(handlers),
268
- });
269
- return;
270
- }
271
-
272
- // NOTE: When dragging is cancelled (e.g., user presses ESC) onDrop is only called after a subsequent event.
273
- // NOTE: pDND blocks ESC event propagation while dragging.
274
- // - ESC only flips internal state.
275
- // - Completion happens on the next processed input event.
276
- // - This avoids reentrancy and keeps pointer/keyboard behavior consistent.
277
- // - We set dragging to undefined in onDropTargetChange after a delay if there are no drop targets.
278
- try {
279
- if (location.current.dropTargets.length === 0) {
280
- log.info('cancelled');
281
- } else {
282
- // Get the target container.
283
- const { data: targetData, handler: targetHandler } = getTargetHandler(location);
284
- if (!targetHandler) {
285
- log.warn('invalid target', {
286
- source: sourceData,
287
- location,
288
- handlers: Object.keys(handlers),
289
- });
290
- return;
291
- }
292
-
293
- // TODO(burdon): Check object doesn't already exist in the collection.
294
- if (sourceHandler === targetHandler) {
295
- targetHandler.onDrop?.({
296
- source: sourceData,
297
- target: targetData,
298
- });
299
- } else {
300
- if (!sourceHandler.onTake) {
301
- log.warn('invalid source', { source: sourceData });
302
- return;
303
- }
304
-
305
- sourceHandler.onTake?.({ source: sourceData }, async (object) => {
306
- targetHandler.onDrop?.({
307
- source: { ...sourceData, data: object },
308
- target: targetData,
309
- });
310
-
311
- return true;
312
- });
313
- }
314
- }
315
- } finally {
316
- handleCancel();
317
- }
318
- },
319
- }),
320
- );
321
- }, [handlers, getSourceHandler, getTargetHandler]);
322
-
323
- return (
324
- <RootContextProvider
325
- containers={handlers}
326
- addContainer={(container) =>
327
- setHandlers((containers) => ({
328
- ...containers,
329
- [container.id]: container,
330
- }))
331
- }
332
- removeContainer={(id) =>
333
- setHandlers((containers) => {
334
- delete containers[id];
335
- return { ...containers };
336
- })
337
- }
338
- dragging={dragging}
339
- >
340
- <Root
341
- className={mx('group overflow-hidden', classNames)}
342
- {...{
343
- [`data-${ROOT_DEBUG_ATTR}`]: debug,
344
- }}
345
- ref={composedRef}
346
- >
347
- {children}
348
- </Root>
349
- </RootContextProvider>
350
- );
351
- });
352
-
353
- //
354
- // Container
355
- //
356
-
357
- type ContainerState = { type: 'idle' } | { type: 'active'; bounds?: DOMRect };
358
-
359
- type ContainerContextValue<TData = any, Location = LocationType> = {
360
- id: string;
361
- eventHandler: MosaicEventHandler<TData>;
362
- axis?: AllowedAxis;
363
- dragging?: DraggingState;
364
- scrolling?: boolean;
365
- state: ContainerState;
366
-
367
- /** Active drop location. */
368
- activeLocation?: Location;
369
- setActiveLocation: (location: Location | undefined) => void;
370
- };
371
-
372
- const [ContainerContextProvider, useContainerContext] = createContext<ContainerContextValue>('MosaicContainer');
373
-
374
- // State attribute: [&:has(>_[data-mosaic-container-state=active])]
375
- const CONTAINER_STATE_ATTR = 'mosaic-container-state';
376
-
377
- // CSS variables: [var(--mosaic-placeholder-xxx)]
378
- const CONTAINER_PLACEHOLDER_WIDTH = '--mosaic-placeholder-width';
379
- const CONTAINER_PLACEHOLDER_HEIGHT = '--mosaic-placeholder-height';
380
-
381
- type ContainerProps = SlottableClassName<
382
- PropsWithChildren<
383
- Partial<Pick<ContainerContextValue, 'eventHandler' | 'axis'>> & {
384
- asChild?: boolean;
385
- autoScroll?: HTMLElement | null;
386
- withFocus?: boolean;
387
- debug?: () => ReactNode;
388
- }
389
- >
390
- >;
391
-
392
- let counter = 0;
393
-
394
- // TODO(burdon): Make generic.
395
- const Container = forwardRef<HTMLDivElement, ContainerProps>(
396
- (
397
- {
398
- className,
399
- classNames,
400
- children,
401
- eventHandler: eventHandlerProp,
402
- axis = 'vertical',
403
- asChild,
404
- autoScroll: autoscrollElement,
405
- withFocus,
406
- debug,
407
- ...props
408
- }: ContainerProps,
409
- forwardedRef,
410
- ) => {
411
- const rootRef = useRef<HTMLDivElement>(null);
412
- const composedRef = useComposedRefs<HTMLDivElement>(rootRef, forwardedRef);
413
- const Root = asChild ? Slot : Primitive.div;
414
-
415
- // Handler.
416
- const eventHandler = useMemo(
417
- () =>
418
- eventHandlerProp ?? {
419
- id: `mosaic-container-${counter++}`,
420
- },
421
- [eventHandlerProp],
422
- );
423
-
424
- // State.
425
- const { dragging } = useRootContext(Container.displayName!);
426
- const [state, setState] = useState<ContainerState>({ type: 'idle' });
427
- const [activeLocation, setActiveLocation] = useState<LocationType | undefined>();
428
- const [scrolling, setScrolling] = useState(false);
429
-
430
- // Focus container.
431
- const { setFocus } = useFocus();
432
- useEffect(() => {
433
- if (withFocus) {
434
- setFocus?.(state.type === 'active' ? 'active' : undefined);
435
- }
436
- }, [setFocus, withFocus, state]);
437
-
438
- // Register handler.
439
- const { addContainer, removeContainer } = useRootContext(eventHandler.id);
440
- useEffect(() => {
441
- addContainer(eventHandler);
442
- return () => removeContainer(eventHandler.id);
443
- }, [eventHandler]);
444
-
445
- const data = useMemo<MosaicContainerData>(
446
- () =>
447
- ({
448
- type: 'container',
449
- id: eventHandler.id,
450
- }) satisfies MosaicContainerData,
451
- [eventHandler.id],
452
- );
453
-
454
- useEffect(() => {
455
- if (!rootRef.current) {
456
- return;
457
- }
458
-
459
- // Determine if scrolling (pause drag/drop handlers).
460
- let timeout: ReturnType<typeof setTimeout>;
461
- const handleScroll = () => {
462
- setScrolling(true);
463
- clearTimeout(timeout);
464
- timeout = setTimeout(() => {
465
- setScrolling(false);
466
- }, 500);
467
- };
468
-
469
- return combine(
470
- ...[
471
- /**
472
- * Custom event for dragging cancellation.
473
- */
474
- bind(rootRef.current, {
475
- type: 'dnd:cancel',
476
- listener: () => {
477
- setState({ type: 'idle' });
478
- },
479
- }),
480
-
481
- // Target.
482
- dropTargetForElements({
483
- element: rootRef.current,
484
- getData: () => data,
485
-
486
- /**
487
- * Test if permitted to drop here.
488
- * NOTE: Contained Tile and Placeholder elements do the same.
489
- */
490
- canDrop: ({ source }) => {
491
- const data = getSourceData(source);
492
- return (data && eventHandler.canDrop?.({ source: data })) || false;
493
- },
494
-
495
- // TODO(burdon): Provide semantic intent to onDrop.
496
- // getDropEffect: () => {
497
- // return 'move';
498
- // },
499
-
500
- /**
501
- * Dragging started in this container.
502
- */
503
- onDragStart: ({ source }) => {
504
- const sourceData = source.data as MosaicTileData;
505
- setState({ type: 'active', bounds: sourceData.bounds });
506
- },
507
- /**
508
- * Dragging entered this container.
509
- */
510
- onDragEnter: ({ source }) => {
511
- const sourceData = source.data as MosaicTileData;
512
- setState({ type: 'active', bounds: sourceData.bounds });
513
- },
514
- /**
515
- * Dragging left this container.
516
- * NOTE: If the container isn't full-height, then when the dragged item is temporarily removed the container will shrink,
517
- * triggering `onDragLeave`, which then causes the item to be added and removed continually (flickering).
518
- */
519
- onDragLeave: ({ source }) => {
520
- const sourceData = source.data as MosaicTileData;
521
- if (sourceData.containerId !== eventHandler.id) {
522
- setState({ type: 'idle' });
523
- }
524
- },
525
- /**
526
- * Dropped in this container.
527
- */
528
- onDrop: () => {
529
- setState({ type: 'idle' });
530
- },
531
- }),
532
-
533
- // Autoscroll.
534
- // NOTE: When used with Scrollable we get a spurious warning (in dev mode).
535
- // "Auto scrolling has been attached to an element that appears not to be scrollable."
536
- autoscrollElement && [
537
- autoScrollForElements({
538
- element: autoscrollElement,
539
- // canScroll: ({ element: _ }) => {
540
- // return true;
541
- // },
542
- getAllowedAxis: () => axis,
543
- getConfiguration: () => ({
544
- maxScrollSpeed: 'fast',
545
- }),
546
- }),
547
-
548
- bind(autoscrollElement, {
549
- type: 'scroll',
550
- listener: handleScroll,
551
- }),
552
-
553
- () => setScrolling(false),
554
- ],
555
- ]
556
- .filter(isTruthy)
557
- .flatMap((x) => x),
558
- );
559
- }, [rootRef, eventHandler, data, autoscrollElement]);
560
-
561
- return (
562
- <ContainerContextProvider
563
- id={eventHandler.id}
564
- eventHandler={eventHandler}
565
- axis={axis}
566
- state={state}
567
- dragging={state.type === 'active' ? dragging : undefined}
568
- scrolling={scrolling}
569
- activeLocation={activeLocation}
570
- setActiveLocation={setActiveLocation}
571
- >
572
- <Root
573
- className={mx('bs-full', className, classNames)}
574
- style={
575
- {
576
- [CONTAINER_PLACEHOLDER_WIDTH]:
577
- state.type === 'active' && state.bounds ? `${state.bounds.width}px` : '0px',
578
- [CONTAINER_PLACEHOLDER_HEIGHT]:
579
- state.type === 'active' && state.bounds ? `${state.bounds.height}px` : '0px',
580
- } as CSSProperties
581
- }
582
- {...props}
583
- {...{
584
- [`data-${CONTAINER_STATE_ATTR}`]: state.type,
585
- }}
586
- ref={composedRef}
587
- >
588
- {children}
589
- </Root>
590
- {debug?.()}
591
- </ContainerContextProvider>
592
- );
593
- },
594
- );
595
-
596
- Container.displayName = 'MosaicContainer';
597
-
598
- //
599
- // Tile
600
- //
601
-
602
- /** Must implement value equivalence. */
603
- type LocationType = string | number;
604
-
605
- type TileState =
606
- | { type: 'idle' }
607
- | { type: 'preview'; container: HTMLElement; rect: DOMRect }
608
- | { type: 'dragging' }
609
- | { type: 'target'; closestEdge: Edge | null };
610
-
611
- type TileContextValue = {
612
- state: TileState;
613
- };
614
-
615
- const [TileContextProvider, useTileContext] = createContext<TileContextValue>('MosaicTile');
616
-
617
- // State attribute: data-[mosaic-tile-state=dragging]
618
- const TILE_STATE_ATTR = 'mosaic-tile-state';
619
-
620
- type TileProps<TData = any, TLocation = LocationType> = SlottableClassName<
621
- PropsWithChildren<{
622
- asChild?: boolean;
623
- dragHandle?: HTMLElement | null;
624
- allowedEdges?: Edge[];
625
- id: string;
626
- data: TData;
627
- location: TLocation;
628
- draggable?: boolean; // TODO(burdon): Not currently implemented.
629
- debug?: boolean;
630
- }>
631
- >;
632
-
633
- const Tile = forwardRef<HTMLDivElement, TileProps>(
634
- (
635
- {
636
- className,
637
- classNames,
638
- children,
639
- asChild,
640
- dragHandle,
641
- allowedEdges: allowedEdgesProp,
642
- location,
643
- id,
644
- data: dataProp,
645
- debug: _,
646
- }: TileProps,
647
- forwardedRef,
648
- ) => {
649
- const rootRef = useRef<HTMLDivElement>(null);
650
- const composedRef = composeRefs<HTMLDivElement>(rootRef, forwardedRef);
651
- const Root = asChild ? Slot : Primitive.div;
652
-
653
- // State.
654
- const {
655
- id: containerId,
656
- eventHandler,
657
- axis,
658
- scrolling,
659
- setActiveLocation,
660
- } = useContainerContext(Tile.displayName!);
661
- const [state, setState] = useState<TileState>({ type: 'idle' });
662
-
663
- const allowedEdges = useMemo<Edge[]>(
664
- () => allowedEdgesProp || (axis === 'vertical' ? ['top', 'bottom'] : ['left', 'right']),
665
- [allowedEdgesProp, axis],
666
- );
667
-
668
- const data = useMemo<MosaicTileData>(
669
- () =>
670
- ({
671
- type: 'tile',
672
- id,
673
- containerId,
674
- data: dataProp,
675
- location,
676
- }) satisfies MosaicTileData,
677
- [containerId, location, dataProp],
678
- );
679
-
680
- useLayoutEffect(() => {
681
- const root = rootRef.current;
682
- if (!root || !containerId || scrolling) {
683
- return;
684
- }
685
-
686
- const handleChange = ({ self, source }: { self: DropTargetRecord; source: ElementDragPayload }) => {
687
- if (source.data.id !== dataProp.id) {
688
- const closestEdge = extractClosestEdge(self.data);
689
- const location = data.location + (closestEdge === 'top' || closestEdge === 'left' ? -0.5 : 0.5);
690
- setActiveLocation(location);
691
- setState({ type: 'target', closestEdge });
692
- }
693
- };
694
-
695
- return combine(
696
- // Source.
697
- draggable({
698
- element: root,
699
- dragHandle: dragHandle && state.type !== 'preview' ? dragHandle : undefined,
700
- canDrag: () => true,
701
- getInitialData: () => data,
702
- onGenerateDragPreview: ({ location, nativeSetDragImage }) => {
703
- setCustomNativeDragPreview({
704
- nativeSetDragImage,
705
- getOffset: preserveOffsetOnSource({
706
- element: root,
707
- input: location.current.input,
708
- }),
709
- render: ({ container }) => {
710
- data.bounds = root.getBoundingClientRect();
711
- setState({
712
- type: 'preview',
713
- container,
714
- rect: root.getBoundingClientRect(),
715
- });
716
- return () => setState({ type: 'dragging' });
717
- },
718
- });
719
- },
720
-
721
- onDragStart: () => {
722
- setState({ type: 'dragging' });
723
- },
724
- onDrop: () => {
725
- setState({ type: 'idle' });
726
- },
727
- }),
728
-
729
- // Target.
730
- dropTargetForElements({
731
- element: root,
732
- getData: ({ input, element }) => attachClosestEdge(data, { input, element, allowedEdges }),
733
- canDrop: ({ source }) => {
734
- const data = getSourceData(source);
735
- return (data && eventHandler.canDrop?.({ source: data })) || false;
736
- },
737
- onDragEnter: ({ self, source }) => {
738
- handleChange({ self, source });
739
- },
740
- onDragLeave: () => {
741
- setState({ type: 'idle' });
742
- setActiveLocation(undefined);
743
- },
744
- onDrag: ({ self, source }) => {
745
- handleChange({ self, source });
746
- },
747
- onDrop: () => {
748
- setState({ type: 'idle' });
749
- setActiveLocation(undefined);
750
- },
751
- }),
752
- );
753
- }, [rootRef, dragHandle, eventHandler, data, scrolling, allowedEdges, setActiveLocation]);
754
-
755
- // NOTE: Ensure no gaps between cells (prevent drop indicators flickering).
756
- // NOTE: Ensure padding doesn't change position of cursor when dragging (no margins).
757
- return (
758
- <TileContextProvider state={state}>
759
- <Root
760
- {...{
761
- [`data-${TILE_STATE_ATTR}`]: state.type,
762
- }}
763
- role='listitem'
764
- className={mx('relative', className, classNames)}
765
- ref={composedRef}
766
- >
767
- {children}
768
- </Root>
769
-
770
- {state.type === 'preview' &&
771
- createPortal(
772
- <Root
773
- {...{
774
- // NOTE: Use to control appearance while dragging.
775
- [`data-${TILE_STATE_ATTR}`]: state.type,
776
- }}
777
- // TODO(burdon): Configure drop animation.
778
- className={mx('relative', className, classNames)}
779
- style={
780
- {
781
- width: `${state.rect.width}px`,
782
- height: `${state.rect.height}px`,
783
- } as CSSProperties
784
- }
785
- >
786
- {children}
787
- </Root>,
788
- state.container,
789
- )}
790
- </TileContextProvider>
791
- );
792
- },
793
- );
794
-
795
- Tile.displayName = 'MosaicTile';
796
-
797
- //
798
- // Placeholder
799
- //
800
-
801
- // Axis: data-[mosaic-placeholder-axis=vertical]
802
- const PLACEHOLDER_AXIS_ATTR = 'mosaic-placeholder-axis';
803
-
804
- // State attribute: data-[mosaic-placeholder-state=active]
805
- const PLACEHOLDER_STATE_ATTR = 'mosaic-placeholder-state';
806
-
807
- type PlaceholderProps<Location = LocationType> = ThemedClassName<
808
- PropsWithChildren<{
809
- asChild?: boolean;
810
- axis?: Axis;
811
- location: Location;
812
- }>
813
- >;
814
-
815
- const Placeholder = <Location extends LocationType = LocationType>({
816
- classNames,
817
- children,
818
- asChild,
819
- axis = 'vertical',
820
- location,
821
- }: PlaceholderProps<Location>) => {
822
- const rootRef = useRef<HTMLDivElement>(null);
823
- const Root = asChild ? Slot : Primitive.div;
824
- const {
825
- id: containerId,
826
- eventHandler,
827
- scrolling,
828
- activeLocation,
829
- setActiveLocation,
830
- } = useContainerContext(Placeholder.displayName!);
831
-
832
- const data = useMemo<MosaicPlaceholderData<Location>>(
833
- () =>
834
- ({
835
- type: 'placeholder',
836
- containerId,
837
- location,
838
- }) satisfies MosaicPlaceholderData<Location>,
839
- [containerId, location],
840
- );
841
-
842
- useLayoutEffect(() => {
843
- const root = rootRef.current;
844
- if (!root || scrolling) {
845
- return;
846
- }
847
-
848
- return dropTargetForElements({
849
- element: root,
850
- getData: () => data,
851
- canDrop: ({ source }) => {
852
- const data = getSourceData(source);
853
- return (data && eventHandler.canDrop?.({ source: data })) || false;
854
- },
855
- onDragEnter: () => {
856
- setActiveLocation(data.location);
857
- },
858
- onDragLeave: () => {
859
- setActiveLocation(undefined);
860
- },
861
- onDrop: () => {
862
- setActiveLocation(undefined);
863
- },
864
- });
865
- }, [rootRef, data, scrolling, setActiveLocation]);
866
-
867
- return (
868
- <Root
869
- {...{
870
- [`data-${PLACEHOLDER_AXIS_ATTR}`]: axis,
871
- [`data-${PLACEHOLDER_STATE_ATTR}`]: data.location === activeLocation ? 'active' : 'idle',
872
- }}
873
- role='none'
874
- className={mx('relative', classNames)}
875
- ref={rootRef}
876
- >
877
- {children}
878
- </Root>
879
- );
880
- };
881
-
882
- Placeholder.displayName = 'MosaicPlaceholder';
883
-
884
- //
885
- // DropIndicator
886
- // TODO(burdon): Support DropIndicator or Placeholder variants.
887
- //
888
-
889
- type DropIndicatorProps = Omit<NativeDropIndicatorProps, 'edge'>;
890
-
891
- const DropIndicator = (props: DropIndicatorProps) => {
892
- const { state } = useTileContext(DropIndicator.displayName!);
893
- return state.type === 'target' && state.closestEdge ? (
894
- <NativeDropIndicator {...props} edge={state.closestEdge} />
895
- ) : null;
896
- };
897
-
898
- DropIndicator.displayName = 'MosaicDropIndicator';
12
+ MosaicDropIndicator,
13
+ type MosaicDropIndicatorProps,
14
+ MosaicPlaceholder,
15
+ type MosaicPlaceholderProps,
16
+ } from './Placeholder';
17
+ import { MosaicRoot, type MosaicRootProps, useMosaicRootContext } from './Root';
18
+ import { MosaicStack, type MosaicStackProps, type MosaicStackTileComponent, MosaicVirtualStack } from './Stack';
19
+ import { MosaicTile, type MosaicTileProps, type MosaicTileState, useMosaicTileContext } from './Tile';
899
20
 
900
21
  //
901
22
  // Mosaic
902
23
  //
903
24
 
904
25
  export const Mosaic = {
905
- Root,
906
- Container,
907
- Tile,
908
- Placeholder,
909
- DropIndicator,
910
-
911
- // TODO(burdon): Move out of Mosaic namespace?
912
- Viewport: Scrollable,
913
- Stack,
914
- VirtualStack,
26
+ Root: MosaicRoot,
27
+ Container: MosaicContainer,
28
+ Tile: MosaicTile,
29
+ Placeholder: MosaicPlaceholder,
30
+ DropIndicator: MosaicDropIndicator,
31
+ Stack: MosaicStack,
32
+ VirtualStack: MosaicVirtualStack,
915
33
  };
916
34
 
917
35
  export type {
918
- RootProps as MosaicRootProps,
919
- ContainerProps as MosaicContainerProps,
920
- TileProps as MosaicTileProps,
921
- PlaceholderProps as MosiacPlaceholderProps,
922
- DropIndicatorProps as MosaicDropIndicatorProps,
923
-
924
- // TODO(burdon): Move out of Mosaic namespace?
925
- ScrollableProps as MosaicViewportProps,
926
- StackProps as MosaicStackProps,
36
+ MosaicRootProps,
37
+ MosaicContainerProps,
38
+ MosaicContainerState,
39
+ MosaicTileProps,
40
+ MosaicTileState,
41
+ MosaicPlaceholderProps,
42
+ MosaicDropIndicatorProps,
43
+ MosaicStackProps,
44
+ MosaicStackTileComponent,
45
+ };
46
+
47
+ export {
48
+ useMosaicRootContext as useMosaic,
49
+ useMosaicContainerContext as useMosaicContainer,
50
+ useMosaicTileContext as useMosaicTile,
927
51
  };
928
-
929
- export { useRootContext as useMosaic, useContainerContext as useMosaicContainer, useTileContext as useMosaicTile };