@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.
- package/dist/lib/browser/chunk-7S4PIFKU.mjs +953 -0
- package/dist/lib/browser/chunk-7S4PIFKU.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +596 -70
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/playwright/index.mjs +138 -0
- package/dist/lib/browser/playwright/index.mjs.map +7 -0
- package/dist/lib/browser/testing/index.mjs +61 -232
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/chunk-MK6KU66S.mjs +955 -0
- package/dist/lib/node-esm/chunk-MK6KU66S.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +596 -70
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/playwright/index.mjs +140 -0
- package/dist/lib/node-esm/playwright/index.mjs.map +7 -0
- package/dist/lib/node-esm/testing/index.mjs +61 -232
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/components/Board/Board.d.ts +81 -0
- package/dist/types/src/components/Board/Board.d.ts.map +1 -0
- package/dist/types/src/components/{Mosaic/Mosaic.stories.d.ts → Board/Board.stories.d.ts} +15 -1
- package/dist/types/src/components/Board/Board.stories.d.ts.map +1 -0
- package/dist/types/src/components/Board/Column.d.ts +40 -0
- package/dist/types/src/components/Board/Column.d.ts.map +1 -0
- package/dist/types/src/components/Board/Item.d.ts +9 -0
- package/dist/types/src/components/Board/Item.d.ts.map +1 -0
- package/dist/types/src/components/Board/index.d.ts +2 -0
- package/dist/types/src/components/Board/index.d.ts.map +1 -0
- package/dist/types/src/components/Focus/Focus.d.ts +5 -4
- package/dist/types/src/components/Focus/Focus.d.ts.map +1 -1
- package/dist/types/src/components/Focus/index.d.ts +0 -1
- package/dist/types/src/components/Focus/index.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/Container.d.ts +42 -0
- package/dist/types/src/components/Mosaic/Container.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Mosaic.d.ts +35 -118
- package/dist/types/src/components/Mosaic/Mosaic.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/Placeholder.d.ts +21 -0
- package/dist/types/src/components/Mosaic/Placeholder.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Root.d.ts +36 -0
- package/dist/types/src/components/Mosaic/Root.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Stack.d.ts +23 -0
- package/dist/types/src/components/Mosaic/Stack.d.ts.map +1 -0
- package/dist/types/src/components/{Stack → Mosaic}/Stack.stories.d.ts +2 -2
- package/dist/types/src/components/Mosaic/Stack.stories.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/Tile.d.ts +48 -0
- package/dist/types/src/components/Mosaic/Tile.d.ts.map +1 -0
- package/dist/types/src/components/Mosaic/styles.d.ts.map +1 -1
- package/dist/types/src/components/Mosaic/types.d.ts +9 -2
- package/dist/types/src/components/Mosaic/types.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -4
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts +15 -0
- package/dist/types/src/hooks/useDefaultColumnEventHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useEventHandlerAdapter.d.ts +1 -1
- package/dist/types/src/hooks/useEventHandlerAdapter.d.ts.map +1 -1
- package/dist/types/src/hooks/useVisibleItems.d.ts +2 -2
- package/dist/types/src/hooks/useVisibleItems.d.ts.map +1 -1
- package/dist/types/src/playwright/board-manager.d.ts +45 -0
- package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
- package/dist/types/src/playwright/index.d.ts +9 -0
- package/dist/types/src/playwright/index.d.ts.map +1 -0
- package/dist/types/src/playwright/playwright.config.d.ts +3 -0
- package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
- package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
- package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
- package/dist/types/src/testing/CardContainer.d.ts +5 -4
- package/dist/types/src/testing/CardContainer.d.ts.map +1 -1
- package/dist/types/src/testing/DefaultStackTile.d.ts +2 -2
- package/dist/types/src/testing/DefaultStackTile.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -1
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/types.d.ts +21 -0
- package/dist/types/src/testing/types.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +5 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +43 -28
- package/src/components/Board/Board.stories.tsx +273 -0
- package/src/components/Board/Board.tsx +191 -0
- package/src/components/Board/Column.tsx +300 -0
- package/src/components/Board/Item.tsx +116 -0
- package/src/components/{Layout → Board}/index.ts +1 -1
- package/src/components/Focus/Focus.tsx +26 -17
- package/src/components/Focus/index.ts +0 -2
- package/src/components/Mosaic/Container.tsx +289 -0
- package/src/components/Mosaic/Mosaic.tsx +35 -913
- package/src/components/Mosaic/Placeholder.tsx +133 -0
- package/src/components/Mosaic/Root.tsx +292 -0
- package/src/components/{Stack → Mosaic}/Stack.stories.tsx +30 -26
- package/src/components/Mosaic/Stack.tsx +246 -0
- package/src/components/Mosaic/Tile.tsx +242 -0
- package/src/components/Mosaic/extension.ts +1 -1
- package/src/components/Mosaic/styles.ts +9 -8
- package/src/components/Mosaic/types.ts +15 -3
- package/src/components/index.ts +1 -4
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useContainerDebug.tsx +4 -2
- package/src/hooks/useDefaultColumnEventHandler.ts +35 -0
- package/src/hooks/useEventHandlerAdapter.ts +15 -6
- package/src/hooks/useVisibleItems.ts +7 -2
- package/src/playwright/board-manager.ts +154 -0
- package/src/playwright/index.ts +13 -0
- package/src/playwright/playwright.config.ts +17 -0
- package/src/playwright/smoke.spec.ts +138 -0
- package/src/testing/CardContainer.tsx +30 -18
- package/src/testing/DefaultStackTile.tsx +27 -16
- package/src/testing/index.ts +1 -1
- package/src/testing/types.ts +38 -0
- package/src/translations.ts +5 -1
- package/dist/lib/browser/chunk-UMBPEG4F.mjs +0 -1637
- package/dist/lib/browser/chunk-UMBPEG4F.mjs.map +0 -7
- package/dist/lib/browser/index.css +0 -13
- package/dist/lib/browser/index.css.map +0 -7
- package/dist/lib/browser/testing/index.css +0 -13
- package/dist/lib/browser/testing/index.css.map +0 -7
- package/dist/lib/node-esm/chunk-X6GV7SMK.mjs +0 -1639
- package/dist/lib/node-esm/chunk-X6GV7SMK.mjs.map +0 -7
- package/dist/lib/node-esm/index.css +0 -13
- package/dist/lib/node-esm/index.css.map +0 -7
- package/dist/lib/node-esm/testing/index.css +0 -13
- package/dist/lib/node-esm/testing/index.css.map +0 -7
- package/dist/types/src/components/Card/Card.d.ts +0 -121
- package/dist/types/src/components/Card/Card.d.ts.map +0 -1
- package/dist/types/src/components/Card/Card.stories.d.ts +0 -21
- package/dist/types/src/components/Card/Card.stories.d.ts.map +0 -1
- package/dist/types/src/components/Card/CardDragPreview.d.ts +0 -6
- package/dist/types/src/components/Card/CardDragPreview.d.ts.map +0 -1
- package/dist/types/src/components/Card/index.d.ts +0 -4
- package/dist/types/src/components/Card/index.d.ts.map +0 -1
- package/dist/types/src/components/Card/styles.d.ts +0 -13
- package/dist/types/src/components/Card/styles.d.ts.map +0 -1
- package/dist/types/src/components/Focus/styles.d.ts +0 -6
- package/dist/types/src/components/Focus/styles.d.ts.map +0 -1
- package/dist/types/src/components/Image/Image.d.ts +0 -14
- package/dist/types/src/components/Image/Image.d.ts.map +0 -1
- package/dist/types/src/components/Image/Image.stories.d.ts +0 -33
- package/dist/types/src/components/Image/Image.stories.d.ts.map +0 -1
- package/dist/types/src/components/Image/index.d.ts +0 -2
- package/dist/types/src/components/Image/index.d.ts.map +0 -1
- package/dist/types/src/components/Layout/Layout.d.ts +0 -33
- package/dist/types/src/components/Layout/Layout.d.ts.map +0 -1
- package/dist/types/src/components/Layout/Layout.stories.d.ts +0 -10
- package/dist/types/src/components/Layout/Layout.stories.d.ts.map +0 -1
- package/dist/types/src/components/Layout/index.d.ts +0 -2
- package/dist/types/src/components/Layout/index.d.ts.map +0 -1
- package/dist/types/src/components/Mosaic/Mosaic.stories.d.ts.map +0 -1
- package/dist/types/src/components/Scrollable/Scrollable.d.ts +0 -24
- package/dist/types/src/components/Scrollable/Scrollable.d.ts.map +0 -1
- package/dist/types/src/components/Scrollable/Scrollable.stories.d.ts +0 -10
- package/dist/types/src/components/Scrollable/Scrollable.stories.d.ts.map +0 -1
- package/dist/types/src/components/Scrollable/index.d.ts +0 -2
- package/dist/types/src/components/Scrollable/index.d.ts.map +0 -1
- package/dist/types/src/components/Stack/Stack.d.ts +0 -22
- package/dist/types/src/components/Stack/Stack.d.ts.map +0 -1
- package/dist/types/src/components/Stack/Stack.stories.d.ts.map +0 -1
- package/dist/types/src/components/Stack/index.d.ts +0 -2
- package/dist/types/src/components/Stack/index.d.ts.map +0 -1
- package/dist/types/src/testing/Board.d.ts +0 -45
- package/dist/types/src/testing/Board.d.ts.map +0 -1
- package/dist/types/src/testing/Slot.stories.d.ts +0 -14
- package/dist/types/src/testing/Slot.stories.d.ts.map +0 -1
- package/dist/types/src/testing/Virtualizer.stories.d.ts +0 -12
- package/dist/types/src/testing/Virtualizer.stories.d.ts.map +0 -1
- package/src/components/Card/Card.stories.tsx +0 -148
- package/src/components/Card/Card.tsx +0 -458
- package/src/components/Card/CardDragPreview.tsx +0 -23
- package/src/components/Card/index.ts +0 -8
- package/src/components/Card/styles.ts +0 -23
- package/src/components/Focus/styles.ts +0 -24
- package/src/components/Image/Image.stories.tsx +0 -86
- package/src/components/Image/Image.tsx +0 -223
- package/src/components/Image/index.ts +0 -5
- package/src/components/Layout/Layout.stories.tsx +0 -59
- package/src/components/Layout/Layout.tsx +0 -108
- package/src/components/Mosaic/Mosaic.stories.tsx +0 -113
- package/src/components/Scrollable/Scrollable.stories.tsx +0 -59
- package/src/components/Scrollable/Scrollable.tsx +0 -88
- package/src/components/Scrollable/index.ts +0 -5
- package/src/components/Scrollable/scrollable.css +0 -20
- package/src/components/Stack/Stack.tsx +0 -210
- package/src/components/Stack/index.ts +0 -5
- package/src/testing/Board.tsx +0 -278
- package/src/testing/Slot.stories.tsx +0 -101
- package/src/testing/Virtualizer.stories.tsx +0 -133
|
@@ -11,7 +11,7 @@ export type UseVisibleItemsProps<TItem = any> = {
|
|
|
11
11
|
id: string;
|
|
12
12
|
|
|
13
13
|
/** Current items. */
|
|
14
|
-
items?: TItem[];
|
|
14
|
+
items?: readonly TItem[] | TItem[];
|
|
15
15
|
|
|
16
16
|
/** Currently dragging item. */
|
|
17
17
|
dragging: MosaicTileData<TItem> | undefined;
|
|
@@ -23,7 +23,12 @@ export type UseVisibleItemsProps<TItem = any> = {
|
|
|
23
23
|
/**
|
|
24
24
|
* Returns items with the dragging item removed.
|
|
25
25
|
*/
|
|
26
|
-
export const useVisibleItems = <TItem = any>({
|
|
26
|
+
export const useVisibleItems = <TItem = any>({
|
|
27
|
+
id,
|
|
28
|
+
items,
|
|
29
|
+
dragging,
|
|
30
|
+
getId,
|
|
31
|
+
}: UseVisibleItemsProps<TItem>): readonly TItem[] | TItem[] => {
|
|
27
32
|
const visibleItems = useMemo(() => {
|
|
28
33
|
if (!items || !dragging || id !== dragging.containerId) {
|
|
29
34
|
return items ?? [];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { Locator, Page } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
export class BoardManager {
|
|
8
|
+
private readonly _page: Page;
|
|
9
|
+
|
|
10
|
+
constructor(readonly locator: Locator) {
|
|
11
|
+
this._page = locator.page();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
columns(): Locator {
|
|
15
|
+
return this.locator.getByTestId('board-column');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
column(index: number): ColumnManager {
|
|
19
|
+
return new ColumnManager(this.locator.getByTestId('board-column').nth(index));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ColumnManager {
|
|
24
|
+
private readonly _page: Page;
|
|
25
|
+
|
|
26
|
+
constructor(readonly locator: Locator) {
|
|
27
|
+
this._page = locator.page();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
items(): Locator {
|
|
31
|
+
return this.locator.getByTestId('board-item');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
item(index: number): ItemManager {
|
|
35
|
+
return new ItemManager(this.locator.getByTestId('board-item').nth(index));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async addItem(): Promise<void> {
|
|
39
|
+
await this.locator.getByTestId('board-column-add-item').click();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
header(): Locator {
|
|
43
|
+
return this.locator.getByTestId('board-column-header');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
title(): Locator {
|
|
47
|
+
return this.header().getByRole('heading');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async dragTo(target: Locator, offset = { x: 0, y: 0 }): Promise<void> {
|
|
51
|
+
const handle = this.header().getByTestId('card-drag-handle');
|
|
52
|
+
const handleBox = await handle.boundingBox();
|
|
53
|
+
if (!handleBox) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await handle.hover();
|
|
58
|
+
await this._page.mouse.down();
|
|
59
|
+
// Small initial movement to trigger the native drag start.
|
|
60
|
+
await this._page.mouse.move(handleBox.x + handleBox.width / 2 + 1, handleBox.y + handleBox.height / 2, {
|
|
61
|
+
steps: 1,
|
|
62
|
+
});
|
|
63
|
+
// Allow the drag monitor to register and the DOM to settle
|
|
64
|
+
// (the dragged column is removed from visible items, shifting remaining columns).
|
|
65
|
+
await this._page.waitForTimeout(200);
|
|
66
|
+
const box = await target.boundingBox();
|
|
67
|
+
if (box) {
|
|
68
|
+
await this._page.mouse.move(offset.x + box.x + box.width / 2, offset.y + box.y + box.height / 2, { steps: 4 });
|
|
69
|
+
// Allow the drop target to process the hover before releasing.
|
|
70
|
+
await this._page.waitForTimeout(100);
|
|
71
|
+
await this._page.mouse.up();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class ItemManager {
|
|
77
|
+
private readonly _page: Page;
|
|
78
|
+
|
|
79
|
+
constructor(readonly locator: Locator) {
|
|
80
|
+
this._page = locator.page();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
title(): Locator {
|
|
84
|
+
return this.locator.getByRole('heading').first();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async dragTo(target: Locator, offset = { x: 0, y: 0 }): Promise<void> {
|
|
88
|
+
const handle = this.locator.getByTestId('card-drag-handle');
|
|
89
|
+
const box = await target.boundingBox();
|
|
90
|
+
if (box) {
|
|
91
|
+
await handle.hover();
|
|
92
|
+
await this._page.mouse.down();
|
|
93
|
+
// Allow the drag monitor to register the grab before moving.
|
|
94
|
+
await this._page.waitForTimeout(100);
|
|
95
|
+
await this._page.mouse.move(offset.x + box.x + box.width / 2, offset.y + box.y + box.height / 2, { steps: 4 });
|
|
96
|
+
// Allow the drop target to process the hover before releasing.
|
|
97
|
+
await this._page.waitForTimeout(100);
|
|
98
|
+
await this._page.mouse.up();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Drag this item to a column's scroll bottom, hold to trigger auto-scroll,
|
|
104
|
+
* then wait for the drop target to become visible and drop on it.
|
|
105
|
+
*
|
|
106
|
+
* @param holdTarget - The footer element; mouse is held ~20px above it to trigger auto-scroll.
|
|
107
|
+
* @param dropTarget - The element to drop on once it scrolls into view.
|
|
108
|
+
* @param dropOffset - Offset from the drop target center.
|
|
109
|
+
*/
|
|
110
|
+
async dragToEndWithAutoScroll(holdTarget: Locator, dropTarget: Locator, dropOffset = { x: 0, y: 0 }): Promise<void> {
|
|
111
|
+
const handle = this.locator.getByTestId('card-drag-handle');
|
|
112
|
+
const holdBox = await holdTarget.boundingBox();
|
|
113
|
+
if (!holdBox) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The hold position is ~20px above the footer to trigger pragmatic auto-scroll.
|
|
118
|
+
const holdX = holdBox.x + holdBox.width / 2;
|
|
119
|
+
const holdY = holdBox.y - 20;
|
|
120
|
+
|
|
121
|
+
await handle.hover();
|
|
122
|
+
await this._page.mouse.down();
|
|
123
|
+
// Allow the drag monitor to register the grab before moving.
|
|
124
|
+
await this._page.waitForTimeout(100);
|
|
125
|
+
|
|
126
|
+
// Move to hold position above the footer to trigger auto-scroll.
|
|
127
|
+
await this._page.mouse.move(holdX, holdY, { steps: 4 });
|
|
128
|
+
|
|
129
|
+
// Hold and nudge mouse periodically to keep auto-scroll active until drop target is visible.
|
|
130
|
+
for (let i = 0; i < 40; i++) {
|
|
131
|
+
// Wait between nudges to give auto-scroll time to advance.
|
|
132
|
+
await this._page.waitForTimeout(200);
|
|
133
|
+
// Small nudge to keep drag events firing (auto-scroll stalls without movement).
|
|
134
|
+
await this._page.mouse.move(holdX + (i % 2), holdY, { steps: 1 });
|
|
135
|
+
|
|
136
|
+
const dropBox = await dropTarget.boundingBox();
|
|
137
|
+
if (dropBox && dropBox.y + dropBox.height < holdY) {
|
|
138
|
+
// Drop target has scrolled into view, move to it and drop.
|
|
139
|
+
await this._page.mouse.move(
|
|
140
|
+
dropOffset.x + dropBox.x + dropBox.width / 2,
|
|
141
|
+
dropOffset.y + dropBox.y + dropBox.height / 2,
|
|
142
|
+
{ steps: 4 },
|
|
143
|
+
);
|
|
144
|
+
// Allow the drop target to process the hover before releasing.
|
|
145
|
+
await this._page.waitForTimeout(100);
|
|
146
|
+
await this._page.mouse.up();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fallback: drop wherever we are.
|
|
152
|
+
await this._page.mouse.up();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Playwright-only entry point for e2e helpers (e.g. BoardManager).
|
|
7
|
+
* Kept separate from the ./testing entry point so that Node can load this
|
|
8
|
+
* without pulling in the testing bundle and its browser-only dependencies
|
|
9
|
+
* (e.g. @atlaskit/pragmatic-drag-and-drop). Use ./playwright from Playwright
|
|
10
|
+
* specs; use ./testing for in-browser test helpers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * from './board-manager';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { defineConfig } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
import { e2ePreset } from '@dxos/test-utils/playwright';
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
...e2ePreset(import.meta.dirname),
|
|
11
|
+
// TODO(wittjosiah): Avoid hard-coding ports.
|
|
12
|
+
webServer: {
|
|
13
|
+
command: 'pnpm storybook dev --ci --quiet --port=9008 --config-dir=.storybook',
|
|
14
|
+
port: 9008,
|
|
15
|
+
reuseExistingServer: false,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Page, expect, test } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
|
|
8
|
+
|
|
9
|
+
import { BoardManager } from './board-manager';
|
|
10
|
+
|
|
11
|
+
const PORT = 9008;
|
|
12
|
+
const STORY_URL = storybookUrl('ui-react-ui-mosaic-board--spec', PORT);
|
|
13
|
+
|
|
14
|
+
test.describe('Board', () => {
|
|
15
|
+
let page: Page;
|
|
16
|
+
let board: BoardManager;
|
|
17
|
+
|
|
18
|
+
test.beforeEach(async ({ browser }) => {
|
|
19
|
+
({ page } = await setupPage(browser, { url: STORY_URL }));
|
|
20
|
+
await page.getByTestId('board-column').first().waitFor({ state: 'visible' });
|
|
21
|
+
board = new BoardManager(page.locator('body'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test.afterEach(async () => {
|
|
25
|
+
await page.close();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('rearrange columns', async () => {
|
|
29
|
+
const col0Label = await board.column(0).title().textContent();
|
|
30
|
+
const col1Label = await board.column(1).title().textContent();
|
|
31
|
+
|
|
32
|
+
// Drag column 0 to the right of column 1.
|
|
33
|
+
await board.column(0).dragTo(board.column(1).header());
|
|
34
|
+
|
|
35
|
+
await expect(board.column(0).title()).toHaveText(col1Label!);
|
|
36
|
+
await expect(board.column(1).title()).toHaveText(col0Label!);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('rearrange within column', async () => {
|
|
40
|
+
// Pick whichever column has more items for a reliable test.
|
|
41
|
+
const col0Count = await board.column(0).items().count();
|
|
42
|
+
const col1Count = await board.column(1).items().count();
|
|
43
|
+
const colIndex = col0Count >= col1Count ? 0 : 1;
|
|
44
|
+
const column = board.column(colIndex);
|
|
45
|
+
const countBefore = Math.max(col0Count, col1Count);
|
|
46
|
+
expect(countBefore).toBeGreaterThanOrEqual(2);
|
|
47
|
+
|
|
48
|
+
const firstLabel = await column.item(0).title().textContent();
|
|
49
|
+
const secondLabel = await column.item(1).title().textContent();
|
|
50
|
+
|
|
51
|
+
// Drag first item below the second item.
|
|
52
|
+
await column.item(0).dragTo(column.item(1).locator, { x: 0, y: 20 });
|
|
53
|
+
|
|
54
|
+
// Item count should stay the same.
|
|
55
|
+
await expect(column.items()).toHaveCount(countBefore);
|
|
56
|
+
|
|
57
|
+
// The first item should now be what was previously the second item.
|
|
58
|
+
await expect(column.item(0).title()).toHaveText(secondLabel!);
|
|
59
|
+
|
|
60
|
+
// The original first item should now be at index 1.
|
|
61
|
+
await expect(column.item(1).title()).toHaveText(firstLabel!);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('drag to beginning of another column', async () => {
|
|
65
|
+
const col0 = board.column(0);
|
|
66
|
+
const col1 = board.column(1);
|
|
67
|
+
|
|
68
|
+
const col0CountBefore = await col0.items().count();
|
|
69
|
+
const col1CountBefore = await col1.items().count();
|
|
70
|
+
const draggedLabel = await col0.item(0).title().textContent();
|
|
71
|
+
|
|
72
|
+
// Drag above the first item in the target column.
|
|
73
|
+
await col0.item(0).dragTo(col1.item(0).locator, { x: 0, y: -10 });
|
|
74
|
+
|
|
75
|
+
await expect(col0.items()).toHaveCount(col0CountBefore - 1);
|
|
76
|
+
await expect(col1.items()).toHaveCount(col1CountBefore + 1);
|
|
77
|
+
await expect(col1.item(0).title()).toHaveText(draggedLabel!);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('drag to middle of another column', async () => {
|
|
81
|
+
const col0 = board.column(0);
|
|
82
|
+
const col1 = board.column(1);
|
|
83
|
+
|
|
84
|
+
const col0CountBefore = await col0.items().count();
|
|
85
|
+
const col1CountBefore = await col1.items().count();
|
|
86
|
+
const draggedLabel = await col0.item(0).title().textContent();
|
|
87
|
+
|
|
88
|
+
// Drag to below the first item in the target column.
|
|
89
|
+
await col0.item(0).dragTo(col1.item(0).locator, { x: 0, y: 10 });
|
|
90
|
+
|
|
91
|
+
await expect(col0.items()).toHaveCount(col0CountBefore - 1);
|
|
92
|
+
await expect(col1.items()).toHaveCount(col1CountBefore + 1);
|
|
93
|
+
await expect(col1.items().getByRole('heading', { name: draggedLabel! })).toBeVisible();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('drag to end of another column', async () => {
|
|
97
|
+
const col0 = board.column(0);
|
|
98
|
+
const col1 = board.column(1);
|
|
99
|
+
|
|
100
|
+
const col0CountBefore = await col0.items().count();
|
|
101
|
+
const col1CountBefore = await col1.items().count();
|
|
102
|
+
const draggedLabel = await col0.item(0).title().textContent();
|
|
103
|
+
|
|
104
|
+
// Hold ~20px above the footer to trigger pragmatic auto-scroll,
|
|
105
|
+
// then drop on the last item once it scrolls into view.
|
|
106
|
+
const footer = col1.locator.getByTestId('board-column-add-item');
|
|
107
|
+
await col0.item(0).dragToEndWithAutoScroll(footer, col1.item(col1CountBefore - 1).locator, { x: 0, y: 20 });
|
|
108
|
+
|
|
109
|
+
await expect(col0.items()).toHaveCount(col0CountBefore - 1);
|
|
110
|
+
await expect(col1.items()).toHaveCount(col1CountBefore + 1);
|
|
111
|
+
await expect(col1.items().getByRole('heading', { name: draggedLabel! })).toBeVisible();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('drag into empty column', async () => {
|
|
115
|
+
const col0 = board.column(0);
|
|
116
|
+
const col2 = board.column(2);
|
|
117
|
+
|
|
118
|
+
const col0CountBefore = await col0.items().count();
|
|
119
|
+
await expect(col2.items()).toHaveCount(0);
|
|
120
|
+
const draggedLabel = await col0.item(0).title().textContent();
|
|
121
|
+
|
|
122
|
+
// Drag into the empty column body area.
|
|
123
|
+
await col0.item(0).dragTo(col2.header(), { x: 0, y: 40 });
|
|
124
|
+
|
|
125
|
+
await expect(col0.items()).toHaveCount(col0CountBefore - 1);
|
|
126
|
+
await expect(col2.items()).toHaveCount(1);
|
|
127
|
+
await expect(col2.item(0).title()).toHaveText(draggedLabel!);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('create new item', async () => {
|
|
131
|
+
const column = board.column(0);
|
|
132
|
+
const countBefore = await column.items().count();
|
|
133
|
+
|
|
134
|
+
await column.addItem();
|
|
135
|
+
|
|
136
|
+
await expect(column.items()).toHaveCount(countBefore + 1);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -15,44 +15,49 @@ const MIN_BLOCK_SIZE = 8;
|
|
|
15
15
|
// Card container.
|
|
16
16
|
//
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
role,
|
|
22
|
-
}: PropsWithChildren<{ icon?: string; role?: string }>) => {
|
|
18
|
+
export type CardContainerProps = PropsWithChildren<{ role?: 'popover' | 'intrinsic'; icon?: string }>;
|
|
19
|
+
|
|
20
|
+
export const CardContainer = ({ children, role, icon = 'ph--arrow-line-down--regular' }: CardContainerProps) => {
|
|
23
21
|
switch (role) {
|
|
24
|
-
case '
|
|
25
|
-
return
|
|
26
|
-
|
|
22
|
+
case 'popover':
|
|
23
|
+
return (
|
|
24
|
+
<div role='none' className='flex justify-center'>
|
|
25
|
+
<PopoverCardContainer icon={icon}>{children}</PopoverCardContainer>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
case 'intrinsic':
|
|
27
29
|
default:
|
|
28
30
|
return <IntrinsicCardContainer>{children}</IntrinsicCardContainer>;
|
|
29
31
|
}
|
|
30
32
|
};
|
|
31
33
|
|
|
32
|
-
const border = 'relative border border-dashed border-subduedSeparator p-4 rounded-lg overflow-x-auto';
|
|
33
|
-
|
|
34
34
|
//
|
|
35
35
|
// Popover
|
|
36
36
|
//
|
|
37
37
|
|
|
38
38
|
export type PopoverCardContainerProps = PropsWithChildren<{ icon?: string }>;
|
|
39
39
|
|
|
40
|
-
export const PopoverCardContainer = ({
|
|
40
|
+
export const PopoverCardContainer = ({
|
|
41
|
+
children,
|
|
42
|
+
icon = 'ph--arrow-line-down--regular',
|
|
43
|
+
}: PopoverCardContainerProps) => {
|
|
41
44
|
return (
|
|
42
45
|
<Popover.Root open>
|
|
43
|
-
<Popover.Content onOpenAutoFocus={(event) => event.preventDefault()}>
|
|
44
|
-
<Popover.Viewport classNames='popover-card-width'>{children}</Popover.Viewport>
|
|
45
|
-
<Popover.Arrow />
|
|
46
|
-
</Popover.Content>
|
|
47
46
|
<Popover.Trigger asChild>
|
|
48
47
|
<Icon icon={icon} size={5} />
|
|
49
48
|
</Popover.Trigger>
|
|
49
|
+
<Popover.Portal>
|
|
50
|
+
<Popover.Content onOpenAutoFocus={(event: Event) => event.preventDefault()}>
|
|
51
|
+
<Popover.Viewport classNames='dx-card-popover'>{children}</Popover.Viewport>
|
|
52
|
+
<Popover.Arrow />
|
|
53
|
+
</Popover.Content>
|
|
54
|
+
</Popover.Portal>
|
|
50
55
|
</Popover.Root>
|
|
51
56
|
);
|
|
52
57
|
};
|
|
53
58
|
|
|
54
59
|
//
|
|
55
|
-
// Intrinsic card container (size constrained by card).
|
|
60
|
+
// Intrinsic card container (size constrained by card itself).
|
|
56
61
|
//
|
|
57
62
|
|
|
58
63
|
export type IntrinsicCardContainerProps = PropsWithChildren<{
|
|
@@ -74,8 +79,15 @@ export const IntrinsicCardContainer = ({
|
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
return (
|
|
77
|
-
<div
|
|
78
|
-
|
|
82
|
+
<div
|
|
83
|
+
role='none'
|
|
84
|
+
className='relative p-2 grid overflow-hidden border-2 border-dashed border-green-500 rounded-lg'
|
|
85
|
+
style={sizeStyle(size, 'horizontal')}
|
|
86
|
+
{...resizeAttributes}
|
|
87
|
+
>
|
|
88
|
+
<div role='none' className='flex flex-col w-full h-full overflow-hidden'>
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
79
91
|
<ResizeHandle
|
|
80
92
|
side='inline-end'
|
|
81
93
|
fallbackSize={DEFAULT_BLOCK_SIZE}
|
|
@@ -2,30 +2,41 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { useRef, useState } from 'react';
|
|
5
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
import { Obj } from '@dxos/echo';
|
|
8
|
+
import { Card, Toolbar } from '@dxos/react-ui';
|
|
9
|
+
import { Menu, createMenuAction } from '@dxos/react-ui-menu';
|
|
8
10
|
import { Json } from '@dxos/react-ui-syntax-highlighter';
|
|
9
11
|
|
|
10
|
-
import {
|
|
12
|
+
import { Mosaic, type MosaicStackTileComponent } from '../components';
|
|
11
13
|
|
|
12
|
-
export const DefaultStackTile:
|
|
14
|
+
export const DefaultStackTile: MosaicStackTileComponent<Obj.Any> = (props) => {
|
|
13
15
|
const dragHandleRef = useRef<HTMLButtonElement>(null);
|
|
14
16
|
const [open, setOpen] = useState(false);
|
|
17
|
+
const menuItems = useMemo(
|
|
18
|
+
() => [
|
|
19
|
+
createMenuAction('toggle-details', () => setOpen((prev) => !prev), {
|
|
20
|
+
label: open ? 'Hide details' : 'Show details',
|
|
21
|
+
icon: 'ph--placeholder--regular',
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
[open],
|
|
25
|
+
);
|
|
26
|
+
|
|
15
27
|
return (
|
|
16
|
-
<Mosaic.Tile {...props} className='border border-separator rounded-
|
|
17
|
-
<
|
|
18
|
-
<Card.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</Card.Toolbar>
|
|
28
|
+
<Mosaic.Tile {...props} className='border border-separator rounded-xs font-mono'>
|
|
29
|
+
<Menu.Root>
|
|
30
|
+
<Card.Toolbar>
|
|
31
|
+
<Card.DragHandle ref={dragHandleRef} />
|
|
32
|
+
<Card.Title>{Obj.getLabel(props.data) ?? props.data.id}</Card.Title>
|
|
33
|
+
{/* TODO(wittjosiah): Reconcile with Card.Menu. */}
|
|
34
|
+
<Menu.Trigger asChild disabled={!menuItems?.length}>
|
|
35
|
+
<Toolbar.IconButton iconOnly variant='ghost' icon='ph--dots-three-vertical--regular' label='Menu' />
|
|
36
|
+
</Menu.Trigger>
|
|
37
|
+
<Menu.Content items={menuItems} />
|
|
38
|
+
</Card.Toolbar>
|
|
39
|
+
</Menu.Root>
|
|
29
40
|
{open && (
|
|
30
41
|
<Card.Row>
|
|
31
42
|
<Json data={props.data} classNames='text-xs' />
|
package/src/testing/index.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
|
+
|
|
7
|
+
import { Ref, Type } from '@dxos/echo';
|
|
8
|
+
import { ObjectId } from '@dxos/keys';
|
|
9
|
+
|
|
10
|
+
//
|
|
11
|
+
// Test Data
|
|
12
|
+
//
|
|
13
|
+
|
|
14
|
+
export const TestItem = Schema.Struct({
|
|
15
|
+
name: Schema.String,
|
|
16
|
+
description: Schema.optional(Schema.String),
|
|
17
|
+
label: Schema.optional(Schema.String),
|
|
18
|
+
}).pipe(
|
|
19
|
+
Type.object({
|
|
20
|
+
typename: 'example.com/type/Item',
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export interface TestItem extends Schema.Schema.Type<typeof TestItem> {}
|
|
26
|
+
|
|
27
|
+
export const TestColumn = Schema.Struct({
|
|
28
|
+
id: ObjectId,
|
|
29
|
+
name: Schema.String,
|
|
30
|
+
items: Schema.mutable(Schema.Array(Ref.Ref(TestItem))),
|
|
31
|
+
}).pipe(
|
|
32
|
+
Type.object({
|
|
33
|
+
typename: 'example.com/type/Column',
|
|
34
|
+
version: '0.1.0',
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export interface TestColumn extends Schema.Schema.Type<typeof TestColumn> {}
|
package/src/translations.ts
CHANGED
|
@@ -9,7 +9,11 @@ export const translationKey = '@dxos/react-ui-mosaic';
|
|
|
9
9
|
export const translations = [
|
|
10
10
|
{
|
|
11
11
|
'en-US': {
|
|
12
|
-
[translationKey]: {
|
|
12
|
+
[translationKey]: {
|
|
13
|
+
'action menu label': 'Actions',
|
|
14
|
+
'add item label': 'Add item',
|
|
15
|
+
'delete menu label': 'Delete',
|
|
16
|
+
},
|
|
13
17
|
},
|
|
14
18
|
},
|
|
15
19
|
] as const satisfies Resource[];
|