@dxos/react-ui-stack 0.8.4-main.1f223c7 → 0.8.4-main.2244d791bb
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/index.mjs +921 -54
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/playwright/index.mjs +10 -23
- package/dist/lib/browser/playwright/index.mjs.map +2 -2
- package/dist/lib/node-esm/index.mjs +922 -54
- 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 +10 -23
- package/dist/lib/node-esm/playwright/index.mjs.map +2 -2
- package/dist/types/src/{exemplars → components}/CardStack/CardStack.d.ts +20 -13
- package/dist/types/src/components/CardStack/CardStack.d.ts.map +1 -0
- package/dist/types/src/{exemplars → components}/CardStack/CardStack.stories.d.ts +3 -1
- package/dist/types/src/components/CardStack/CardStack.stories.d.ts.map +1 -0
- package/dist/types/src/{exemplars → components}/CardStack/CardStackDragPreview.d.ts +4 -1
- package/dist/types/src/components/CardStack/CardStackDragPreview.d.ts.map +1 -0
- package/dist/types/src/components/CardStack/index.d.ts.map +1 -0
- package/dist/types/src/components/Stack/Stack.d.ts +7 -6
- package/dist/types/src/components/Stack/Stack.d.ts.map +1 -1
- package/dist/types/src/components/Stack/Stack.stories.d.ts +1 -2
- package/dist/types/src/components/Stack/Stack.stories.d.ts.map +1 -1
- package/dist/types/src/components/StackContext.d.ts +1 -1
- package/dist/types/src/components/StackContext.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItem.d.ts +7 -10
- package/dist/types/src/components/StackItem/StackItem.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItem.stories.d.ts +0 -1
- package/dist/types/src/components/StackItem/StackItem.stories.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemContent.d.ts +4 -37
- package/dist/types/src/components/StackItem/StackItemContent.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemHeading.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemSigil.d.ts +2 -2
- package/dist/types/src/components/StackItem/StackItemSigil.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/LayoutControls.d.ts +3 -0
- package/dist/types/src/components/deprecated/LayoutControls.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/{defs.d.ts → types.d.ts} +1 -1
- package/dist/types/src/components/types.d.ts.map +1 -0
- package/dist/types/src/hooks/useStackDropForElements.d.ts +8 -6
- package/dist/types/src/hooks/useStackDropForElements.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -38
- package/src/{exemplars → components}/CardStack/CardStack.stories.tsx +14 -13
- package/src/{exemplars → components}/CardStack/CardStack.tsx +113 -54
- package/src/{exemplars → components}/CardStack/CardStackDragPreview.tsx +12 -9
- package/src/components/Stack/Stack.stories.tsx +3 -5
- package/src/components/Stack/Stack.tsx +97 -40
- package/src/components/StackContext.tsx +1 -1
- package/src/components/StackItem/StackItem.stories.tsx +7 -7
- package/src/components/StackItem/StackItem.tsx +35 -16
- package/src/components/StackItem/StackItemContent.tsx +21 -41
- package/src/components/StackItem/StackItemHeading.tsx +2 -6
- package/src/components/StackItem/StackItemSigil.tsx +3 -3
- package/src/components/deprecated/LayoutControls.tsx +3 -0
- package/src/components/index.ts +2 -2
- package/src/hooks/useStackDropForElements.ts +58 -44
- package/src/index.ts +0 -3
- package/src/playwright/playwright.config.ts +1 -1
- package/src/translations.ts +1 -1
- package/dist/lib/browser/chunk-3V2YUQK5.mjs +0 -1375
- package/dist/lib/browser/chunk-3V2YUQK5.mjs.map +0 -7
- package/dist/lib/browser/testing/index.mjs +0 -31
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HE3BRF7A.mjs +0 -1377
- package/dist/lib/node-esm/chunk-HE3BRF7A.mjs.map +0 -7
- package/dist/lib/node-esm/testing/index.mjs +0 -32
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/components/Image/Image.d.ts +0 -11
- package/dist/types/src/components/Image/Image.d.ts.map +0 -1
- package/dist/types/src/components/Image/Image.stories.d.ts +0 -31
- 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/defs.d.ts.map +0 -1
- package/dist/types/src/exemplars/Card/Card.d.ts +0 -58
- package/dist/types/src/exemplars/Card/Card.d.ts.map +0 -1
- package/dist/types/src/exemplars/Card/Card.stories.d.ts +0 -44
- package/dist/types/src/exemplars/Card/Card.stories.d.ts.map +0 -1
- package/dist/types/src/exemplars/Card/CardDragPreview.d.ts +0 -6
- package/dist/types/src/exemplars/Card/CardDragPreview.d.ts.map +0 -1
- package/dist/types/src/exemplars/Card/fragments.d.ts +0 -13
- package/dist/types/src/exemplars/Card/fragments.d.ts.map +0 -1
- package/dist/types/src/exemplars/Card/index.d.ts +0 -4
- package/dist/types/src/exemplars/Card/index.d.ts.map +0 -1
- package/dist/types/src/exemplars/CardStack/CardStack.d.ts.map +0 -1
- package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts.map +0 -1
- package/dist/types/src/exemplars/CardStack/CardStackDragPreview.d.ts.map +0 -1
- package/dist/types/src/exemplars/CardStack/index.d.ts.map +0 -1
- package/dist/types/src/exemplars/index.d.ts +0 -3
- package/dist/types/src/exemplars/index.d.ts.map +0 -1
- package/dist/types/src/testing/CardContainer.d.ts +0 -6
- package/dist/types/src/testing/CardContainer.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -2
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/src/components/Image/Image.stories.tsx +0 -58
- package/src/components/Image/Image.tsx +0 -137
- package/src/components/Image/index.ts +0 -5
- package/src/exemplars/Card/Card.stories.tsx +0 -88
- package/src/exemplars/Card/Card.tsx +0 -186
- package/src/exemplars/Card/CardDragPreview.tsx +0 -22
- package/src/exemplars/Card/fragments.ts +0 -24
- package/src/exemplars/Card/index.ts +0 -7
- package/src/exemplars/index.ts +0 -6
- package/src/testing/CardContainer.tsx +0 -37
- package/src/testing/index.ts +0 -5
- /package/dist/types/src/{exemplars → components}/CardStack/index.d.ts +0 -0
- /package/src/{exemplars → components}/CardStack/index.ts +0 -0
- /package/src/components/{defs.ts → types.ts} +0 -0
|
@@ -7,6 +7,7 @@ import React, {
|
|
|
7
7
|
type CSSProperties,
|
|
8
8
|
Children,
|
|
9
9
|
type ComponentPropsWithRef,
|
|
10
|
+
type FocusEvent,
|
|
10
11
|
type KeyboardEvent,
|
|
11
12
|
forwardRef,
|
|
12
13
|
useCallback,
|
|
@@ -16,13 +17,14 @@ import React, {
|
|
|
16
17
|
} from 'react';
|
|
17
18
|
|
|
18
19
|
import { ListItem, type ThemedClassName, useId } from '@dxos/react-ui';
|
|
19
|
-
import { mx } from '@dxos/
|
|
20
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
21
|
|
|
21
22
|
import { useStackDropForElements } from '../../hooks';
|
|
22
|
-
import { type StackContextValue } from '../defs';
|
|
23
23
|
import { StackContext } from '../StackContext';
|
|
24
|
+
import { type StackContextValue } from '../types';
|
|
24
25
|
|
|
25
26
|
export type Orientation = 'horizontal' | 'vertical';
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
29
|
* Size is how Stack and its StackItems coordinate the dimensions of the items with the available space.
|
|
28
30
|
* - `intrinsic` signals to Stack and its StackItems to occupy their intrinsic size
|
|
@@ -32,13 +34,6 @@ export type Orientation = 'horizontal' | 'vertical';
|
|
|
32
34
|
*/
|
|
33
35
|
export type Size = 'intrinsic' | 'contain' | 'split';
|
|
34
36
|
|
|
35
|
-
export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
|
|
36
|
-
Partial<StackContextValue> & {
|
|
37
|
-
itemsCount?: number;
|
|
38
|
-
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
39
|
-
separatorOnScroll?: number;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
37
|
export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
43
38
|
export const railGridVertical = 'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
44
39
|
|
|
@@ -60,6 +55,14 @@ const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orient
|
|
|
60
55
|
return el.focus();
|
|
61
56
|
};
|
|
62
57
|
|
|
58
|
+
export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
|
|
59
|
+
Partial<StackContextValue> & {
|
|
60
|
+
itemsCount?: number;
|
|
61
|
+
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
62
|
+
separatorOnScroll?: number;
|
|
63
|
+
circularFocus?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
63
66
|
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
64
67
|
(
|
|
65
68
|
{
|
|
@@ -67,18 +70,20 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
67
70
|
classNames,
|
|
68
71
|
style,
|
|
69
72
|
orientation = 'vertical',
|
|
70
|
-
rail = true,
|
|
73
|
+
rail = true, // TODO(burdon): Change default to false.
|
|
71
74
|
size = 'intrinsic',
|
|
72
75
|
onRearrange,
|
|
73
76
|
itemsCount = Children.count(children),
|
|
74
77
|
getDropElement,
|
|
75
78
|
separatorOnScroll,
|
|
79
|
+
circularFocus,
|
|
76
80
|
...props
|
|
77
81
|
},
|
|
78
82
|
forwardedRef,
|
|
79
83
|
) => {
|
|
80
84
|
const stackId = useId('stack', props.id);
|
|
81
85
|
const [stackElement, stackRef] = useState<HTMLDivElement | null>(null);
|
|
86
|
+
const [lastFocusedItem, setLastFocusedItem] = useState<string>();
|
|
82
87
|
const composedItemRef = composeRefs<HTMLDivElement>(stackRef, forwardedRef);
|
|
83
88
|
|
|
84
89
|
const styles: CSSProperties = {
|
|
@@ -115,9 +120,26 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
115
120
|
}, [stackElement, separatorOnScroll, orientation]);
|
|
116
121
|
|
|
117
122
|
/**
|
|
118
|
-
* Handles
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
* Handles blur events to track the last focused item within this stack.
|
|
124
|
+
*/
|
|
125
|
+
const handleBlur = useCallback(
|
|
126
|
+
(event: FocusEvent<HTMLDivElement>) => {
|
|
127
|
+
if (event.target) {
|
|
128
|
+
const target = event.target as HTMLElement;
|
|
129
|
+
const closestStackItem = target.closest(`[data-dx-item-id]`) as HTMLElement | null;
|
|
130
|
+
if (closestStackItem?.closest(`[data-dx-stack="${stackId}"]`)) {
|
|
131
|
+
setLastFocusedItem(closestStackItem?.getAttribute('data-dx-item-id') ?? undefined);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
props.onBlur?.(event);
|
|
135
|
+
},
|
|
136
|
+
[stackId, props.onBlur],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Handles moving focus using the arrow keys. Focus is only handled by the nearest stack;
|
|
141
|
+
* if the arrow key matches the orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item;
|
|
142
|
+
* or, if there is no such stack item, focus is passed to the adjacent empty stack if one can be found.
|
|
121
143
|
*/
|
|
122
144
|
const handleKeyDown = useCallback(
|
|
123
145
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
@@ -152,15 +174,28 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
152
174
|
? 1
|
|
153
175
|
: 0;
|
|
154
176
|
if (parallelDelta !== 0) {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
const currentIndex = closestStackItems.indexOf(closestOwnedItem);
|
|
178
|
+
const nextIndex = currentIndex + parallelDelta;
|
|
179
|
+
let adjacentItem: HTMLElement | undefined;
|
|
180
|
+
|
|
181
|
+
if (circularFocus) {
|
|
182
|
+
// Circular navigation: wrap around using modulo.
|
|
183
|
+
adjacentItem = closestStackItems[(nextIndex + closestStackItems.length) % closestStackItems.length] as
|
|
184
|
+
| HTMLElement
|
|
185
|
+
| undefined;
|
|
186
|
+
} else {
|
|
187
|
+
// Non-circular navigation: only move if within bounds.
|
|
188
|
+
if (nextIndex >= 0 && nextIndex < closestStackItems.length) {
|
|
189
|
+
adjacentItem = closestStackItems[nextIndex] as HTMLElement | undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
159
193
|
if (adjacentItem) {
|
|
160
194
|
event.preventDefault();
|
|
161
195
|
scrollIntoViewAndFocus(adjacentItem, closestStackOrientation);
|
|
162
196
|
}
|
|
163
197
|
}
|
|
198
|
+
|
|
164
199
|
if (perpendicularDelta !== 0) {
|
|
165
200
|
if (ancestorStack && ancestorOrientation !== closestStackOrientation) {
|
|
166
201
|
const siblingStacks = Array.from(
|
|
@@ -168,10 +203,21 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
168
203
|
`[data-dx-stack-item="${ancestorStack.getAttribute('data-dx-stack')}"] [data-dx-stack]`,
|
|
169
204
|
),
|
|
170
205
|
) as HTMLElement[];
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
206
|
+
const currentStackIndex = siblingStacks.indexOf(closestStack);
|
|
207
|
+
const nextStackIndex = currentStackIndex + perpendicularDelta;
|
|
208
|
+
let adjacentStack: HTMLElement | undefined;
|
|
209
|
+
|
|
210
|
+
if (ancestorStack.getAttribute('data-dx-stack-circular-focus') === 'true') {
|
|
211
|
+
// Circular navigation: wrap around using modulo.
|
|
212
|
+
adjacentStack = siblingStacks[(nextStackIndex + siblingStacks.length) % siblingStacks.length] as
|
|
213
|
+
| HTMLElement
|
|
214
|
+
| undefined;
|
|
215
|
+
} else {
|
|
216
|
+
// Non-circular navigation: only move if within bounds.
|
|
217
|
+
if (nextStackIndex >= 0 && nextStackIndex < siblingStacks.length) {
|
|
218
|
+
adjacentStack = siblingStacks[nextStackIndex] as HTMLElement | undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
175
221
|
const adjacentStackSelfItem = adjacentStack?.closest(
|
|
176
222
|
`[data-dx-stack-item=${ancestorStack.getAttribute('data-dx-stack')}]`,
|
|
177
223
|
) as HTMLElement | undefined;
|
|
@@ -182,26 +228,33 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
182
228
|
),
|
|
183
229
|
) as HTMLElement[])
|
|
184
230
|
: [];
|
|
185
|
-
if (adjacentStackItems.length > 0) {
|
|
186
|
-
//
|
|
187
|
-
const ownedItemRect = closestOwnedItem.getBoundingClientRect();
|
|
188
|
-
const targetPosition =
|
|
189
|
-
closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
|
|
190
|
-
|
|
231
|
+
if (adjacentStack && adjacentStackItems.length > 0) {
|
|
232
|
+
// Check if the adjacent stack has a last focused item recorded, otherwise find the closest item by position.
|
|
191
233
|
let closestItem = adjacentStackItems[0];
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
234
|
+
// Try to find an item with matching data-dx-stack-item value.
|
|
235
|
+
const lastFocusedItem = adjacentStack.querySelector(
|
|
236
|
+
`[data-dx-item-id="${adjacentStack.getAttribute('data-dx-last-focused-item') ?? 'never'}"]`,
|
|
237
|
+
);
|
|
238
|
+
if (lastFocusedItem) {
|
|
239
|
+
closestItem = lastFocusedItem as HTMLElement;
|
|
240
|
+
} else {
|
|
241
|
+
// Fall back to positional calculation
|
|
242
|
+
const ownedItemRect = closestOwnedItem.getBoundingClientRect();
|
|
243
|
+
const targetPosition =
|
|
244
|
+
closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
|
|
198
245
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
246
|
+
let closestDistance = Infinity;
|
|
247
|
+
for (const item of adjacentStackItems) {
|
|
248
|
+
const itemRect = item.getBoundingClientRect();
|
|
249
|
+
const itemPosition = closestStackOrientation === 'vertical' ? itemRect.top : itemRect.left;
|
|
250
|
+
const distance = Math.abs(itemPosition - targetPosition);
|
|
251
|
+
if (distance < closestDistance) {
|
|
252
|
+
closestDistance = distance;
|
|
253
|
+
closestItem = item;
|
|
254
|
+
}
|
|
255
|
+
if (closestDistance <= PERPENDICULAR_FOCUS_THRESHHOLD) {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
205
258
|
}
|
|
206
259
|
}
|
|
207
260
|
|
|
@@ -235,13 +288,14 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
235
288
|
}
|
|
236
289
|
props.onKeyDown?.(event);
|
|
237
290
|
},
|
|
238
|
-
[props.onKeyDown, stackId],
|
|
291
|
+
[props.onKeyDown, stackId, circularFocus],
|
|
239
292
|
);
|
|
240
293
|
|
|
241
294
|
const gridClasses = useMemo(() => {
|
|
242
295
|
if (!rail) {
|
|
243
296
|
return orientation === 'horizontal' ? 'grid-rows-1 pli-[--stack-gap]' : 'grid-cols-1 plb-[--stack-gap]';
|
|
244
297
|
}
|
|
298
|
+
|
|
245
299
|
if (orientation === 'horizontal') {
|
|
246
300
|
return railGridHorizontal;
|
|
247
301
|
} else {
|
|
@@ -279,7 +333,10 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
279
333
|
classNames,
|
|
280
334
|
)}
|
|
281
335
|
onKeyDown={handleKeyDown}
|
|
336
|
+
onBlur={handleBlur}
|
|
282
337
|
data-dx-stack={stackId}
|
|
338
|
+
data-dx-stack-circular-focus={circularFocus}
|
|
339
|
+
data-dx-last-focused-item={lastFocusedItem}
|
|
283
340
|
data-rail={rail}
|
|
284
341
|
aria-orientation={orientation}
|
|
285
342
|
style={styles}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { createContext, useContext } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type StackItemRearrangeHandler, type StackItemSize } from './defs';
|
|
8
7
|
import { type Orientation, type Size } from './Stack';
|
|
8
|
+
import { type StackItemRearrangeHandler, type StackItemSize } from './types';
|
|
9
9
|
|
|
10
10
|
export type StackContextValue = {
|
|
11
11
|
orientation: Orientation;
|
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React from 'react';
|
|
9
7
|
|
|
10
8
|
import { DropdownMenu, Icon } from '@dxos/react-ui';
|
|
11
|
-
import { withTheme } from '@dxos/
|
|
9
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
12
10
|
|
|
13
11
|
import { StackItem, type StackItemRootProps } from './StackItem';
|
|
14
12
|
|
|
15
13
|
const DefaultStory = (props: StackItemRootProps) => {
|
|
16
14
|
return (
|
|
17
|
-
<StackItem.Root role='section' {...props} classNames='
|
|
18
|
-
<StackItem.Heading>
|
|
15
|
+
<StackItem.Root role='section' {...props} classNames='is-[20rem] border border-separator'>
|
|
16
|
+
<StackItem.Heading classNames='is-full border-be border-separator'>
|
|
19
17
|
<span className='sr-only'>Title</span>
|
|
20
18
|
<div role='none' className='sticky -block-start-px bg-[--sticky-bg] p-1 is-full'>
|
|
21
19
|
<DropdownMenu.Root>
|
|
@@ -27,7 +25,9 @@ const DefaultStory = (props: StackItemRootProps) => {
|
|
|
27
25
|
</DropdownMenu.Root>
|
|
28
26
|
</div>
|
|
29
27
|
</StackItem.Heading>
|
|
30
|
-
<StackItem.Content
|
|
28
|
+
<StackItem.Content>
|
|
29
|
+
<div className='p-4 text-center'>Content</div>
|
|
30
|
+
</StackItem.Content>
|
|
31
31
|
</StackItem.Root>
|
|
32
32
|
);
|
|
33
33
|
};
|
|
@@ -36,7 +36,7 @@ const meta = {
|
|
|
36
36
|
title: 'ui/react-ui-stack/StackItem',
|
|
37
37
|
component: StackItem.Root as any,
|
|
38
38
|
render: DefaultStory,
|
|
39
|
-
decorators: [withTheme],
|
|
39
|
+
decorators: [withTheme()],
|
|
40
40
|
parameters: {
|
|
41
41
|
layout: 'centered',
|
|
42
42
|
},
|
|
@@ -26,10 +26,10 @@ import { createPortal } from 'react-dom';
|
|
|
26
26
|
|
|
27
27
|
import { ListItem, type ThemedClassName } from '@dxos/react-ui';
|
|
28
28
|
import { resizeAttributes, sizeStyle } from '@dxos/react-ui-dnd';
|
|
29
|
-
import { mx } from '@dxos/
|
|
29
|
+
import { mx } from '@dxos/ui-theme';
|
|
30
30
|
|
|
31
|
-
import { type StackItemData, type StackItemSize } from '../defs';
|
|
32
31
|
import { type ItemDragState, StackItemContext, idle, useStack, useStackItem } from '../StackContext';
|
|
32
|
+
import { type StackItemData, type StackItemSize } from '../types';
|
|
33
33
|
|
|
34
34
|
import { StackItemContent, type StackItemContentProps } from './StackItemContent';
|
|
35
35
|
import { StackItemDragHandle, type StackItemDragHandleProps } from './StackItemDragHandle';
|
|
@@ -54,6 +54,10 @@ export const DEFAULT_HORIZONTAL_SIZE = 48 satisfies StackItemSize;
|
|
|
54
54
|
export const DEFAULT_VERTICAL_SIZE = 'min-content' satisfies StackItemSize;
|
|
55
55
|
export const DEFAULT_EXTRINSIC_SIZE = DEFAULT_HORIZONTAL_SIZE satisfies StackItemSize;
|
|
56
56
|
|
|
57
|
+
//
|
|
58
|
+
// StackItemRoot
|
|
59
|
+
//
|
|
60
|
+
|
|
57
61
|
type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
|
|
58
62
|
item: Omit<StackItemData, 'type'>;
|
|
59
63
|
order?: number;
|
|
@@ -63,7 +67,7 @@ type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
|
|
|
63
67
|
onSizeChange?: (nextSize: StackItemSize) => void;
|
|
64
68
|
role?: 'article' | 'section';
|
|
65
69
|
disableRearrange?: boolean;
|
|
66
|
-
focusIndicatorVariant?: 'over-all' | 'group';
|
|
70
|
+
focusIndicatorVariant?: 'over-all' | 'group' | 'over-all-always' | 'group-always';
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
@@ -86,6 +90,7 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
86
90
|
forwardedRef,
|
|
87
91
|
) => {
|
|
88
92
|
const [itemElement, itemRef] = useState<HTMLDivElement | null>(null);
|
|
93
|
+
const composedItemRef = composeRefs<HTMLDivElement>(itemRef, forwardedRef);
|
|
89
94
|
const [selfDragHandleElement, selfDragHandleRef] = useState<HTMLDivElement | null>(null);
|
|
90
95
|
const [closestEdge, setEdge] = useState<Edge | null>(null);
|
|
91
96
|
const [sourceId, setSourceId] = useState<string | null>(null);
|
|
@@ -96,8 +101,6 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
96
101
|
|
|
97
102
|
const Root = role ?? 'div';
|
|
98
103
|
|
|
99
|
-
const composedItemRef = composeRefs<HTMLDivElement>(itemRef, forwardedRef);
|
|
100
|
-
|
|
101
104
|
const setSize = useCallback(
|
|
102
105
|
(nextSize: StackItemSize, commit?: boolean) => {
|
|
103
106
|
setInternalSize(nextSize);
|
|
@@ -110,6 +113,7 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
110
113
|
|
|
111
114
|
const type = orientation === 'horizontal' ? 'column' : 'card';
|
|
112
115
|
|
|
116
|
+
// TODO(burdon): Factor out?
|
|
113
117
|
useLayoutEffect(() => {
|
|
114
118
|
if (!itemElement || !onRearrange || disableRearrange) {
|
|
115
119
|
return;
|
|
@@ -183,18 +187,18 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
183
187
|
|
|
184
188
|
const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
|
|
185
189
|
|
|
186
|
-
// Determine if the drop would result in any changes
|
|
190
|
+
// Determine if the drop would result in any changes.
|
|
187
191
|
const shouldShowDropIndicator = () => {
|
|
188
192
|
if (!closestEdge || !sourceId) {
|
|
189
193
|
return false;
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
// Don't show indicator when dragged item is over itself
|
|
196
|
+
// Don't show indicator when dragged item is over itself.
|
|
193
197
|
if (sourceId === item.id) {
|
|
194
198
|
return false;
|
|
195
199
|
}
|
|
196
200
|
|
|
197
|
-
// Don't show indicator when dragged item is over the trailing edge of its previous sibling
|
|
201
|
+
// Don't show indicator when dragged item is over the trailing edge of its previous sibling.
|
|
198
202
|
const isTrailingEdgeOfPrevSibling =
|
|
199
203
|
prevSiblingId !== undefined &&
|
|
200
204
|
sourceId === prevSiblingId &&
|
|
@@ -232,15 +236,22 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
232
236
|
'group/stack-item grid relative',
|
|
233
237
|
focusIndicatorVariant === 'over-all'
|
|
234
238
|
? 'dx-focus-ring-inset-over-all'
|
|
235
|
-
:
|
|
236
|
-
? 'dx-focus-ring-
|
|
237
|
-
: '
|
|
239
|
+
: focusIndicatorVariant === 'over-all-always'
|
|
240
|
+
? 'dx-focus-ring-inset-over-all-always'
|
|
241
|
+
: orientation === 'horizontal'
|
|
242
|
+
? focusIndicatorVariant === 'group-always'
|
|
243
|
+
? 'dx-focus-ring-group-x-always'
|
|
244
|
+
: 'dx-focus-ring-group-x'
|
|
245
|
+
: focusIndicatorVariant === 'group-always'
|
|
246
|
+
? 'dx-focus-ring-group-y-always'
|
|
247
|
+
: 'dx-focus-ring-group-y',
|
|
238
248
|
orientation === 'horizontal' ? 'grid-rows-subgrid' : 'grid-cols-subgrid',
|
|
239
249
|
rail && (orientation === 'horizontal' ? 'row-span-2' : 'col-span-2'),
|
|
240
250
|
role === 'section' && orientation !== 'horizontal' && 'border-be border-subduedSeparator',
|
|
241
251
|
classNames,
|
|
242
252
|
)}
|
|
243
253
|
data-dx-stack-item={stackId}
|
|
254
|
+
data-dx-item-id={item.id}
|
|
244
255
|
{...resizeAttributes}
|
|
245
256
|
style={{
|
|
246
257
|
...(stackSize !== 'split' && sizeStyle(size, orientation)),
|
|
@@ -261,37 +272,45 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
261
272
|
},
|
|
262
273
|
);
|
|
263
274
|
|
|
275
|
+
//
|
|
276
|
+
// StackItemDragPreview
|
|
277
|
+
//
|
|
278
|
+
|
|
264
279
|
type StackItemDragPreviewProps = {
|
|
265
280
|
children: ({ item }: { item: any }) => ReactNode;
|
|
266
281
|
};
|
|
267
282
|
|
|
268
|
-
|
|
283
|
+
const StackItemDragPreview = ({ children }: StackItemDragPreviewProps) => {
|
|
269
284
|
const { state } = useStackItem();
|
|
270
285
|
return state?.type === 'preview' ? createPortal(children({ item: state.item }), state.container) : null;
|
|
271
286
|
};
|
|
272
287
|
|
|
288
|
+
//
|
|
289
|
+
// StackItem
|
|
290
|
+
//
|
|
291
|
+
|
|
273
292
|
export const StackItem = {
|
|
274
293
|
Root: StackItemRoot,
|
|
275
294
|
Content: StackItemContent,
|
|
295
|
+
DragHandle: StackItemDragHandle,
|
|
296
|
+
DragPreview: StackItemDragPreview,
|
|
276
297
|
Heading: StackItemHeading,
|
|
277
298
|
HeadingLabel: StackItemHeadingLabel,
|
|
278
299
|
HeadingStickyContent: StackItemHeadingStickyContent,
|
|
279
300
|
ResizeHandle: StackItemResizeHandle,
|
|
280
|
-
DragHandle: StackItemDragHandle,
|
|
281
301
|
Sigil: StackItemSigil,
|
|
282
302
|
SigilButton: StackItemSigilButton,
|
|
283
|
-
DragPreview: StackItemDragPreview,
|
|
284
303
|
};
|
|
285
304
|
|
|
286
305
|
export type {
|
|
287
306
|
StackItemRootProps,
|
|
288
307
|
StackItemContentProps,
|
|
308
|
+
StackItemDragHandleProps,
|
|
309
|
+
StackItemDragPreviewProps,
|
|
289
310
|
StackItemHeadingProps,
|
|
290
311
|
StackItemHeadingLabelProps,
|
|
291
312
|
StackItemResizeHandleProps,
|
|
292
|
-
StackItemDragHandleProps,
|
|
293
313
|
StackItemSigilProps,
|
|
294
314
|
StackItemSigilButtonProps,
|
|
295
315
|
StackItemSigilAction,
|
|
296
|
-
StackItemDragPreviewProps,
|
|
297
316
|
};
|
|
@@ -5,71 +5,49 @@
|
|
|
5
5
|
import React, { type ComponentPropsWithoutRef, forwardRef, useMemo } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
-
import { mx } from '@dxos/
|
|
8
|
+
import { mx } from '@dxos/ui-theme';
|
|
9
9
|
|
|
10
10
|
import { useStack, useStackItem } from '../StackContext';
|
|
11
11
|
|
|
12
|
-
export type StackItemContentProps = ThemedClassName<Omit<ComponentPropsWithoutRef<'div'>, 'role'>> & {
|
|
13
|
-
/**
|
|
14
|
-
* This flag is required in order to clarify a developer experience that seemed like it needed extra boilerplate
|
|
15
|
-
* (`row-span-2`) or was buggy. See the description of the StackItem.Content component itself for more information.
|
|
16
|
-
*/
|
|
12
|
+
export type StackItemContentProps = ThemedClassName<Omit<ComponentPropsWithoutRef<'div'>, 'role' | 'scrollable'>> & {
|
|
17
13
|
toolbar?: boolean;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Whether to provide for the layout of a statusbar after the content.
|
|
21
|
-
*/
|
|
22
14
|
statusbar?: boolean;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Whether the consumer intends to do something custom and typical affordances should not apply
|
|
26
|
-
*/
|
|
27
|
-
layoutManaged?: boolean;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Whether to set a certain aspect ratio on the content, including the toolbar and statusbar. This is provided for
|
|
31
|
-
* convenience and consistency; it can instead be specified by the `classNames` or `style` props as needed.
|
|
32
|
-
*/
|
|
33
|
-
size?: 'intrinsic' | 'video' | 'square';
|
|
34
15
|
};
|
|
35
16
|
|
|
36
17
|
/**
|
|
37
|
-
* This component should be used by plugins for rendering content within a stack item
|
|
38
|
-
* The `toolbar` flag must be provided since this component provides for the layout of content with the toolbar.
|
|
18
|
+
* This component should be used by plugins for rendering content within a stack item (i.e., a “plank” or “section”).
|
|
39
19
|
*/
|
|
40
20
|
export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps>(
|
|
41
|
-
({ children, toolbar, statusbar,
|
|
21
|
+
({ classNames, children, toolbar, statusbar, ...props }, forwardedRef) => {
|
|
42
22
|
const { size: stackItemSize } = useStack();
|
|
43
23
|
const { role } = useStackItem();
|
|
44
24
|
const style = useMemo(
|
|
45
|
-
() =>
|
|
46
|
-
|
|
47
|
-
?
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
[toolbar, statusbar, layoutManaged],
|
|
25
|
+
() => ({
|
|
26
|
+
gridTemplateRows: [
|
|
27
|
+
toolbar && role === 'section' ? 'calc(var(--toolbar-size) - 1px)' : 'var(--toolbar-size)',
|
|
28
|
+
'1fr',
|
|
29
|
+
statusbar && 'var(--statusbar-size)',
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(' '),
|
|
33
|
+
}),
|
|
34
|
+
[toolbar, statusbar],
|
|
56
35
|
);
|
|
57
36
|
|
|
58
37
|
return (
|
|
59
38
|
<div
|
|
60
|
-
role='none'
|
|
61
39
|
{...props}
|
|
40
|
+
role='none'
|
|
41
|
+
style={style}
|
|
62
42
|
className={mx(
|
|
63
43
|
'group grid grid-cols-[100%] density-coarse',
|
|
64
44
|
stackItemSize === 'contain' && 'min-bs-0 overflow-hidden',
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
role === 'section' &&
|
|
68
|
-
toolbar &&
|
|
45
|
+
toolbar &&
|
|
46
|
+
role === 'section' &&
|
|
69
47
|
'[&_.dx-toolbar]:sticky [&_.dx-toolbar]:z-[1] [&_.dx-toolbar]:block-start-0 [&_.dx-toolbar]:-mbe-px [&_.dx-toolbar]:min-is-0',
|
|
48
|
+
toolbar && '[&>.dx-toolbar]:relative [&>.dx-toolbar]:border-be [&>.dx-toolbar]:border-subduedSeparator',
|
|
70
49
|
classNames,
|
|
71
50
|
)}
|
|
72
|
-
style={style}
|
|
73
51
|
data-popover-collision-boundary={true}
|
|
74
52
|
ref={forwardedRef}
|
|
75
53
|
>
|
|
@@ -78,3 +56,5 @@ export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps
|
|
|
78
56
|
);
|
|
79
57
|
},
|
|
80
58
|
);
|
|
59
|
+
|
|
60
|
+
StackItemContent.displayName = 'StackItemContent';
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
6
5
|
import { Slot } from '@radix-ui/react-slot';
|
|
7
6
|
import React, {
|
|
8
7
|
type ComponentPropsWithRef,
|
|
@@ -13,7 +12,7 @@ import React, {
|
|
|
13
12
|
|
|
14
13
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
15
14
|
import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-attention';
|
|
16
|
-
import { mx } from '@dxos/
|
|
15
|
+
import { mx } from '@dxos/ui-theme';
|
|
17
16
|
|
|
18
17
|
import { useStack } from '../StackContext';
|
|
19
18
|
|
|
@@ -30,7 +29,6 @@ export const StackItemHeading = ({
|
|
|
30
29
|
...props
|
|
31
30
|
}: StackItemHeadingProps) => {
|
|
32
31
|
const { orientation } = useStack();
|
|
33
|
-
const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
|
|
34
32
|
|
|
35
33
|
const Root = asChild ? Slot : 'div';
|
|
36
34
|
|
|
@@ -38,10 +36,8 @@ export const StackItemHeading = ({
|
|
|
38
36
|
<Root
|
|
39
37
|
role='heading'
|
|
40
38
|
{...props}
|
|
41
|
-
tabIndex={0}
|
|
42
|
-
{...focusableGroupAttrs}
|
|
43
39
|
className={mx(
|
|
44
|
-
'flex items-center
|
|
40
|
+
'flex items-center !border-is-0 bg-headerSurface',
|
|
45
41
|
separateOnScroll
|
|
46
42
|
? 'border-transparent [[data-scroll-separator="true"]_&]:border-subduedSeparator'
|
|
47
43
|
: 'border-subduedSeparator',
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { Fragment, type PropsWithChildren, forwardRef, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
7
|
+
import { type Node } from '@dxos/app-graph';
|
|
8
8
|
import { keySymbols } from '@dxos/keyboard';
|
|
9
9
|
import { Button, type ButtonProps, DropdownMenu, Icon, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
10
10
|
import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-attention';
|
|
11
|
-
import { descriptionText, mx } from '@dxos/
|
|
11
|
+
import { descriptionText, mx } from '@dxos/ui-theme';
|
|
12
12
|
import { getHostPlatform } from '@dxos/util';
|
|
13
13
|
|
|
14
14
|
import { translationKey } from '../../translations';
|
|
@@ -23,7 +23,7 @@ export type KeyBinding = {
|
|
|
23
23
|
unknown?: string;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
export type StackItemSigilAction = Pick<ActionLike, 'id' | 'properties' | 'data'>;
|
|
26
|
+
export type StackItemSigilAction = Pick<Node.ActionLike, 'id' | 'properties' | 'data'>;
|
|
27
27
|
|
|
28
28
|
export type StackItemSigilButtonProps = Omit<ButtonProps, 'variant'> &
|
|
29
29
|
AttendableId &
|
|
@@ -30,6 +30,9 @@ const LayoutControl = ({ icon, label, ...props }: Omit<ButtonProps, 'children'>
|
|
|
30
30
|
return <IconButton iconOnly icon={icon} label={label} tooltipSide='bottom' variant='ghost' {...props} />;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated
|
|
35
|
+
*/
|
|
33
36
|
export const LayoutControls = forwardRef<HTMLDivElement, LayoutControlsProps>(
|
|
34
37
|
(
|
|
35
38
|
{ onClick, variant = 'default', capabilities: can, isSolo, pin, close = false, children, ...props },
|