@dxos/react-ui-stack 0.8.4-main.f9ba587 → 0.8.4-main.fffef41
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-3F2KBXLP.mjs +1482 -0
- package/dist/lib/browser/chunk-3F2KBXLP.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +45 -1158
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/playwright/index.mjs +61 -0
- package/dist/lib/browser/playwright/index.mjs.map +7 -0
- package/dist/lib/browser/testing/index.mjs +25 -51
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/chunk-SYKFLQGK.mjs +1484 -0
- package/dist/lib/node-esm/chunk-SYKFLQGK.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +45 -1159
- 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 +63 -0
- package/dist/lib/node-esm/playwright/index.mjs.map +7 -0
- package/dist/lib/node-esm/testing/index.mjs +24 -51
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/components/Image/Image.d.ts +14 -0
- package/dist/types/src/components/Image/Image.d.ts.map +1 -0
- package/dist/types/src/components/Image/Image.stories.d.ts +33 -0
- package/dist/types/src/components/Image/Image.stories.d.ts.map +1 -0
- package/dist/types/src/components/Image/index.d.ts +2 -0
- package/dist/types/src/components/Image/index.d.ts.map +1 -0
- package/dist/types/src/components/Stack/Stack.d.ts +15 -7
- package/dist/types/src/components/Stack/Stack.d.ts.map +1 -1
- package/dist/types/src/components/Stack/Stack.stories.d.ts +12 -3
- package/dist/types/src/components/Stack/Stack.stories.d.ts.map +1 -1
- package/dist/types/src/components/StackContext.d.ts +2 -1
- package/dist/types/src/components/StackContext.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItem.d.ts +7 -6
- package/dist/types/src/components/StackItem/StackItem.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItem.stories.d.ts +13 -5
- package/dist/types/src/components/StackItem/StackItem.stories.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemContent.d.ts +20 -10
- package/dist/types/src/components/StackItem/StackItemContent.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemHeading.d.ts +1 -1
- package/dist/types/src/components/StackItem/StackItemHeading.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemResizeHandle.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemSigil.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/exemplars/Card/Card.d.ts +25 -13
- package/dist/types/src/exemplars/Card/Card.d.ts.map +1 -1
- package/dist/types/src/exemplars/Card/Card.stories.d.ts +12 -4
- package/dist/types/src/exemplars/Card/Card.stories.d.ts.map +1 -1
- package/dist/types/src/exemplars/Card/fragments.d.ts +3 -2
- package/dist/types/src/exemplars/Card/fragments.d.ts.map +1 -1
- package/dist/types/src/exemplars/CardStack/CardStack.d.ts +3 -1
- package/dist/types/src/exemplars/CardStack/CardStack.d.ts.map +1 -1
- package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts +9 -3
- package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useStackDropForElements.d.ts +3 -3
- package/dist/types/src/hooks/useStackDropForElements.d.ts.map +1 -1
- package/dist/types/src/playwright/index.d.ts +2 -0
- package/dist/types/src/playwright/index.d.ts.map +1 -0
- package/dist/types/src/playwright/stack-manager.d.ts.map +1 -0
- package/dist/types/src/testing/CardContainer.d.ts +6 -0
- package/dist/types/src/testing/CardContainer.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -30
- package/src/components/Image/Image.stories.tsx +84 -0
- package/src/components/Image/Image.tsx +222 -0
- package/src/components/Image/index.ts +5 -0
- package/src/components/Stack/Stack.stories.tsx +8 -9
- package/src/components/Stack/Stack.tsx +222 -25
- package/src/components/StackContext.tsx +2 -1
- package/src/components/StackItem/StackItem.stories.tsx +16 -14
- package/src/components/StackItem/StackItem.tsx +26 -18
- package/src/components/StackItem/StackItemContent.tsx +21 -9
- package/src/components/StackItem/StackItemHeading.tsx +4 -8
- package/src/components/StackItem/StackItemResizeHandle.tsx +2 -1
- package/src/components/StackItem/StackItemSigil.tsx +2 -1
- package/src/components/index.ts +2 -1
- package/src/exemplars/Card/Card.stories.tsx +29 -43
- package/src/exemplars/Card/Card.tsx +67 -24
- package/src/exemplars/Card/fragments.ts +3 -2
- package/src/exemplars/CardStack/CardStack.stories.tsx +11 -10
- package/src/exemplars/CardStack/CardStack.tsx +12 -9
- package/src/hooks/useStackDropForElements.ts +43 -36
- package/src/playwright/index.ts +5 -0
- package/src/playwright/smoke.spec.ts +1 -1
- package/src/testing/CardContainer.tsx +37 -0
- package/src/testing/index.ts +1 -1
- package/dist/types/src/testing/stack-manager.d.ts.map +0 -1
- /package/dist/types/src/{testing → playwright}/stack-manager.d.ts +0 -0
- /package/src/{testing → playwright}/stack-manager.ts +0 -0
|
@@ -2,40 +2,41 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
|
|
6
5
|
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
7
6
|
import React, {
|
|
8
|
-
Children,
|
|
9
7
|
type CSSProperties,
|
|
8
|
+
Children,
|
|
10
9
|
type ComponentPropsWithRef,
|
|
10
|
+
type KeyboardEvent,
|
|
11
11
|
forwardRef,
|
|
12
|
-
useState,
|
|
13
|
-
useMemo,
|
|
14
12
|
useCallback,
|
|
15
13
|
useEffect,
|
|
14
|
+
useMemo,
|
|
15
|
+
useState,
|
|
16
16
|
} from 'react';
|
|
17
17
|
|
|
18
|
-
import { type ThemedClassName,
|
|
18
|
+
import { ListItem, type ThemedClassName, useId } from '@dxos/react-ui';
|
|
19
19
|
import { mx } from '@dxos/react-ui-theme';
|
|
20
20
|
|
|
21
21
|
import { useStackDropForElements } from '../../hooks';
|
|
22
|
-
import { StackContext } from '../StackContext';
|
|
23
22
|
import { type StackContextValue } from '../defs';
|
|
23
|
+
import { StackContext } from '../StackContext';
|
|
24
24
|
|
|
25
25
|
export type Orientation = 'horizontal' | 'vertical';
|
|
26
|
-
export type Size = 'intrinsic' | 'contain' | 'contain-fit-content';
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Size is how Stack and its StackItems coordinate the dimensions of the items with the available space.
|
|
29
|
+
* - `intrinsic` signals to Stack and its StackItems to occupy their intrinsic size
|
|
30
|
+
* - Any other size will extrinsically fill the available space along the axis of its orientation and handle overflow:
|
|
31
|
+
* - `contain` causes StackItems to occupy their intrinsic size
|
|
32
|
+
* - `split` divides the Stack’s available space among the StackItems
|
|
33
|
+
*/
|
|
34
|
+
export type Size = 'intrinsic' | 'contain' | 'split';
|
|
34
35
|
|
|
35
36
|
export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
36
37
|
export const railGridVertical = 'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
37
38
|
|
|
38
|
-
// TODO(ZaymonFC): Magic 2px to stop overflow
|
|
39
|
+
// TODO(ZaymonFC): Magic 2px to stop overflow.
|
|
39
40
|
export const railGridHorizontalContainFitContent =
|
|
40
41
|
'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_fit-content(calc(100%-var(--rail-size)*2+2px))_[content-end]]';
|
|
41
42
|
export const railGridVerticalContainFitContent =
|
|
@@ -43,6 +44,24 @@ export const railGridVerticalContainFitContent =
|
|
|
43
44
|
|
|
44
45
|
export const autoScrollRootAttributes = { 'data-drag-autoscroll': 'idle' };
|
|
45
46
|
|
|
47
|
+
const PERPENDICULAR_FOCUS_THRESHHOLD = 128;
|
|
48
|
+
|
|
49
|
+
const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orientation']) => {
|
|
50
|
+
el.scrollIntoView({
|
|
51
|
+
behavior: 'instant',
|
|
52
|
+
[orientation === 'vertical' ? 'block' : 'inline']: 'center',
|
|
53
|
+
});
|
|
54
|
+
return el.focus();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
|
|
58
|
+
Partial<StackContextValue> & {
|
|
59
|
+
itemsCount?: number;
|
|
60
|
+
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
61
|
+
separatorOnScroll?: number;
|
|
62
|
+
circularFocus?: boolean;
|
|
63
|
+
};
|
|
64
|
+
|
|
46
65
|
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
47
66
|
(
|
|
48
67
|
{
|
|
@@ -50,23 +69,25 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
50
69
|
classNames,
|
|
51
70
|
style,
|
|
52
71
|
orientation = 'vertical',
|
|
53
|
-
rail = true,
|
|
72
|
+
rail = true, // TODO(burdon): Change default to false.
|
|
54
73
|
size = 'intrinsic',
|
|
55
74
|
onRearrange,
|
|
56
75
|
itemsCount = Children.count(children),
|
|
57
76
|
getDropElement,
|
|
58
77
|
separatorOnScroll,
|
|
78
|
+
circularFocus,
|
|
59
79
|
...props
|
|
60
80
|
},
|
|
61
81
|
forwardedRef,
|
|
62
82
|
) => {
|
|
83
|
+
const stackId = useId('stack', props.id);
|
|
63
84
|
const [stackElement, stackRef] = useState<HTMLDivElement | null>(null);
|
|
85
|
+
const [lastFocusedItem, setLastFocusedItem] = useState<string>();
|
|
64
86
|
const composedItemRef = composeRefs<HTMLDivElement>(stackRef, forwardedRef);
|
|
65
|
-
const arrowNavigationAttrs = useArrowNavigationGroup({ axis: orientation });
|
|
66
87
|
|
|
67
88
|
const styles: CSSProperties = {
|
|
68
89
|
[orientation === 'horizontal' ? 'gridTemplateColumns' : 'gridTemplateRows']:
|
|
69
|
-
`repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
90
|
+
size === 'split' ? `repeat(${itemsCount}, 1fr)` : `repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
70
91
|
...style,
|
|
71
92
|
};
|
|
72
93
|
|
|
@@ -97,14 +118,186 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
97
118
|
}
|
|
98
119
|
}, [stackElement, separatorOnScroll, orientation]);
|
|
99
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Handles blur events to track the last focused item within this stack.
|
|
123
|
+
*/
|
|
124
|
+
const handleBlur = useCallback(
|
|
125
|
+
(event: React.FocusEvent<HTMLDivElement>) => {
|
|
126
|
+
if (event.target) {
|
|
127
|
+
const target = event.target as HTMLElement;
|
|
128
|
+
const closestStackItem = target.closest(`[data-dx-item-id]`) as HTMLElement | null;
|
|
129
|
+
if (closestStackItem?.closest(`[data-dx-stack="${stackId}"]`)) {
|
|
130
|
+
setLastFocusedItem(closestStackItem?.getAttribute('data-dx-item-id') ?? undefined);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
props.onBlur?.(event);
|
|
134
|
+
},
|
|
135
|
+
[stackId, props.onBlur],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Handles moving focus using the arrow keys. Focus is only handled by the nearest stack;
|
|
140
|
+
* if the arrow key matches the orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item;
|
|
141
|
+
* or, if there is no such stack item, focus is passed to the adjacent empty stack if one can be found.
|
|
142
|
+
*/
|
|
143
|
+
const handleKeyDown = useCallback(
|
|
144
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
145
|
+
const target = event.target as HTMLElement;
|
|
146
|
+
if (
|
|
147
|
+
event.key.startsWith('Arrow') &&
|
|
148
|
+
!target.closest(
|
|
149
|
+
`input, textarea, [role="textbox"], [data-tabster*="mover"], [data-arrow-keys="all"], [data-arrow-keys~="${event.key.toLowerCase().slice(5)}"]`,
|
|
150
|
+
)
|
|
151
|
+
) {
|
|
152
|
+
const closestOwnedItem = target.closest(`[data-dx-stack-item="${stackId}"]`);
|
|
153
|
+
const closestStack = target.closest('[data-dx-stack]') as HTMLElement | null;
|
|
154
|
+
const closestStackItems = Array.from(
|
|
155
|
+
closestStack?.querySelectorAll(`[data-dx-stack-item="${stackId}"]`) ?? [],
|
|
156
|
+
);
|
|
157
|
+
const closestStackOrientation = closestStack?.getAttribute('aria-orientation') as Orientation;
|
|
158
|
+
const ancestorStack = closestStack?.parentElement?.closest('[data-dx-stack]') as HTMLElement | null;
|
|
159
|
+
if (closestOwnedItem && closestStack) {
|
|
160
|
+
const ancestorOrientation = ancestorStack?.getAttribute('aria-orientation') as Orientation | undefined;
|
|
161
|
+
const parallelDelta = (
|
|
162
|
+
closestStackOrientation === 'vertical' ? event.key === 'ArrowUp' : event.key === 'ArrowLeft'
|
|
163
|
+
)
|
|
164
|
+
? -1
|
|
165
|
+
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowDown' : event.key === 'ArrowRight')
|
|
166
|
+
? 1
|
|
167
|
+
: 0;
|
|
168
|
+
const perpendicularDelta = (
|
|
169
|
+
closestStackOrientation === 'vertical' ? event.key === 'ArrowLeft' : event.key === 'ArrowUp'
|
|
170
|
+
)
|
|
171
|
+
? -1
|
|
172
|
+
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowRight' : event.key === 'ArrowDown')
|
|
173
|
+
? 1
|
|
174
|
+
: 0;
|
|
175
|
+
if (parallelDelta !== 0) {
|
|
176
|
+
const currentIndex = closestStackItems.indexOf(closestOwnedItem);
|
|
177
|
+
const nextIndex = currentIndex + parallelDelta;
|
|
178
|
+
let adjacentItem: HTMLElement | undefined;
|
|
179
|
+
|
|
180
|
+
if (circularFocus) {
|
|
181
|
+
// Circular navigation: wrap around using modulo.
|
|
182
|
+
adjacentItem = closestStackItems[(nextIndex + closestStackItems.length) % closestStackItems.length] as
|
|
183
|
+
| HTMLElement
|
|
184
|
+
| undefined;
|
|
185
|
+
} else {
|
|
186
|
+
// Non-circular navigation: only move if within bounds.
|
|
187
|
+
if (nextIndex >= 0 && nextIndex < closestStackItems.length) {
|
|
188
|
+
adjacentItem = closestStackItems[nextIndex] as HTMLElement | undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (adjacentItem) {
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
scrollIntoViewAndFocus(adjacentItem, closestStackOrientation);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (perpendicularDelta !== 0) {
|
|
198
|
+
if (ancestorStack && ancestorOrientation !== closestStackOrientation) {
|
|
199
|
+
const siblingStacks = Array.from(
|
|
200
|
+
ancestorStack.querySelectorAll(
|
|
201
|
+
`[data-dx-stack-item="${ancestorStack.getAttribute('data-dx-stack')}"] [data-dx-stack]`,
|
|
202
|
+
),
|
|
203
|
+
) as HTMLElement[];
|
|
204
|
+
const currentStackIndex = siblingStacks.indexOf(closestStack);
|
|
205
|
+
const nextStackIndex = currentStackIndex + perpendicularDelta;
|
|
206
|
+
let adjacentStack: HTMLElement | undefined;
|
|
207
|
+
|
|
208
|
+
if (ancestorStack.getAttribute('data-dx-stack-circular-focus') === 'true') {
|
|
209
|
+
// Circular navigation: wrap around using modulo.
|
|
210
|
+
adjacentStack = siblingStacks[(nextStackIndex + siblingStacks.length) % siblingStacks.length] as
|
|
211
|
+
| HTMLElement
|
|
212
|
+
| undefined;
|
|
213
|
+
} else {
|
|
214
|
+
// Non-circular navigation: only move if within bounds.
|
|
215
|
+
if (nextStackIndex >= 0 && nextStackIndex < siblingStacks.length) {
|
|
216
|
+
adjacentStack = siblingStacks[nextStackIndex] as HTMLElement | undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const adjacentStackSelfItem = adjacentStack?.closest(
|
|
220
|
+
`[data-dx-stack-item=${ancestorStack.getAttribute('data-dx-stack')}]`,
|
|
221
|
+
) as HTMLElement | undefined;
|
|
222
|
+
const adjacentStackItems = adjacentStack
|
|
223
|
+
? (Array.from(
|
|
224
|
+
adjacentStack.querySelectorAll(
|
|
225
|
+
`[data-dx-stack-item="${adjacentStack.getAttribute('data-dx-stack')}"]`,
|
|
226
|
+
),
|
|
227
|
+
) as HTMLElement[])
|
|
228
|
+
: [];
|
|
229
|
+
if (adjacentStack && adjacentStackItems.length > 0) {
|
|
230
|
+
// Check if the adjacent stack has a last focused item recorded, otherwise find the closest item by position.
|
|
231
|
+
let closestItem = adjacentStackItems[0];
|
|
232
|
+
// Try to find an item with matching data-dx-stack-item value.
|
|
233
|
+
const lastFocusedItem = adjacentStack.querySelector(
|
|
234
|
+
`[data-dx-item-id="${adjacentStack.getAttribute('data-dx-last-focused-item') ?? 'never'}"]`,
|
|
235
|
+
);
|
|
236
|
+
if (lastFocusedItem) {
|
|
237
|
+
closestItem = lastFocusedItem as HTMLElement;
|
|
238
|
+
} else {
|
|
239
|
+
// Fall back to positional calculation
|
|
240
|
+
const ownedItemRect = closestOwnedItem.getBoundingClientRect();
|
|
241
|
+
const targetPosition =
|
|
242
|
+
closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
|
|
243
|
+
|
|
244
|
+
let closestDistance = Infinity;
|
|
245
|
+
for (const item of adjacentStackItems) {
|
|
246
|
+
const itemRect = item.getBoundingClientRect();
|
|
247
|
+
const itemPosition = closestStackOrientation === 'vertical' ? itemRect.top : itemRect.left;
|
|
248
|
+
const distance = Math.abs(itemPosition - targetPosition);
|
|
249
|
+
if (distance < closestDistance) {
|
|
250
|
+
closestDistance = distance;
|
|
251
|
+
closestItem = item;
|
|
252
|
+
}
|
|
253
|
+
if (closestDistance <= PERPENDICULAR_FOCUS_THRESHHOLD) {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
scrollIntoViewAndFocus(closestItem, closestStackOrientation);
|
|
261
|
+
} else if (adjacentStackSelfItem) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
scrollIntoViewAndFocus(adjacentStackSelfItem, ancestorOrientation);
|
|
264
|
+
}
|
|
265
|
+
} else if (closestOwnedItem) {
|
|
266
|
+
const closestOwnedItemStack = closestOwnedItem.querySelector('[data-dx-stack]');
|
|
267
|
+
const closestOwnedItemStackItems = closestOwnedItemStack
|
|
268
|
+
? (Array.from(
|
|
269
|
+
closestOwnedItemStack.querySelectorAll(
|
|
270
|
+
`[data-dx-stack-item="${closestOwnedItemStack.getAttribute('data-dx-stack')}"]`,
|
|
271
|
+
),
|
|
272
|
+
) as HTMLElement[])
|
|
273
|
+
: [];
|
|
274
|
+
if (closestOwnedItemStackItems.length > 0) {
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
scrollIntoViewAndFocus(
|
|
277
|
+
closestOwnedItemStackItems[
|
|
278
|
+
['ArrowUp', 'ArrowLeft'].includes(event.key) ? closestOwnedItemStackItems.length - 1 : 0
|
|
279
|
+
],
|
|
280
|
+
closestOwnedItemStack?.getAttribute('aria-orientation') as Orientation,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
props.onKeyDown?.(event);
|
|
288
|
+
},
|
|
289
|
+
[props.onKeyDown, stackId, circularFocus],
|
|
290
|
+
);
|
|
291
|
+
|
|
100
292
|
const gridClasses = useMemo(() => {
|
|
101
293
|
if (!rail) {
|
|
102
|
-
return orientation === 'horizontal' ? 'grid-rows-1 pli-
|
|
294
|
+
return orientation === 'horizontal' ? 'grid-rows-1 pli-[--stack-gap]' : 'grid-cols-1 plb-[--stack-gap]';
|
|
103
295
|
}
|
|
296
|
+
|
|
104
297
|
if (orientation === 'horizontal') {
|
|
105
|
-
return
|
|
298
|
+
return railGridHorizontal;
|
|
106
299
|
} else {
|
|
107
|
-
return
|
|
300
|
+
return railGridVertical;
|
|
108
301
|
}
|
|
109
302
|
}, [rail, orientation, size]);
|
|
110
303
|
|
|
@@ -125,19 +318,23 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
125
318
|
}, [stackElement, handleScroll]);
|
|
126
319
|
|
|
127
320
|
return (
|
|
128
|
-
<StackContext.Provider value={{ orientation, rail, size, onRearrange }}>
|
|
321
|
+
<StackContext.Provider value={{ orientation, rail, size, onRearrange, stackId }}>
|
|
129
322
|
<div
|
|
130
323
|
{...props}
|
|
131
|
-
{...arrowNavigationAttrs}
|
|
132
324
|
className={mx(
|
|
133
|
-
'grid relative',
|
|
325
|
+
'grid relative [--stack-gap:var(--dx-trimXs)]',
|
|
134
326
|
gridClasses,
|
|
135
|
-
|
|
327
|
+
size === 'contain' &&
|
|
136
328
|
(orientation === 'horizontal'
|
|
137
|
-
? 'overflow-x-auto min-bs-0 max-bs-full bs-full'
|
|
329
|
+
? 'overflow-x-auto overscroll-x-contain min-bs-0 max-bs-full bs-full'
|
|
138
330
|
: 'overflow-y-auto min-is-0 max-is-full is-full'),
|
|
139
331
|
classNames,
|
|
140
332
|
)}
|
|
333
|
+
onKeyDown={handleKeyDown}
|
|
334
|
+
onBlur={handleBlur}
|
|
335
|
+
data-dx-stack={stackId}
|
|
336
|
+
data-dx-stack-circular-focus={circularFocus}
|
|
337
|
+
data-dx-last-focused-item={lastFocusedItem}
|
|
141
338
|
data-rail={rail}
|
|
142
339
|
aria-orientation={orientation}
|
|
143
340
|
style={styles}
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { createContext, useContext } from 'react';
|
|
6
6
|
|
|
7
|
+
import { type StackItemRearrangeHandler, type StackItemSize } from './defs';
|
|
7
8
|
import { type Orientation, type Size } from './Stack';
|
|
8
|
-
import { type StackItemSize, type StackItemRearrangeHandler } from './defs';
|
|
9
9
|
|
|
10
10
|
export type StackContextValue = {
|
|
11
11
|
orientation: Orientation;
|
|
12
12
|
rail: boolean;
|
|
13
13
|
size: Size;
|
|
14
14
|
onRearrange?: StackItemRearrangeHandler;
|
|
15
|
+
stackId?: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export const StackContext = createContext<StackContextValue>({
|
|
@@ -2,21 +2,17 @@
|
|
|
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
|
-
import {
|
|
11
|
-
import { withTheme } from '@dxos/
|
|
8
|
+
import { DropdownMenu, Icon } from '@dxos/react-ui';
|
|
9
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
12
10
|
|
|
13
|
-
import { StackItem } from './StackItem';
|
|
11
|
+
import { StackItem, type StackItemRootProps } from './StackItem';
|
|
14
12
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
render: (args) => (
|
|
19
|
-
<StackItem.Root role='section' {...args} classNames='w-[20rem] border border-separator'>
|
|
13
|
+
const DefaultStory = (props: StackItemRootProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<StackItem.Root role='section' {...props} classNames='is-[20rem] border border-separator'>
|
|
20
16
|
<StackItem.Heading>
|
|
21
17
|
<span className='sr-only'>Title</span>
|
|
22
18
|
<div role='none' className='sticky -block-start-px bg-[--sticky-bg] p-1 is-full'>
|
|
@@ -29,18 +25,24 @@ const meta: Meta<typeof StackItem.Root> = {
|
|
|
29
25
|
</DropdownMenu.Root>
|
|
30
26
|
</div>
|
|
31
27
|
</StackItem.Heading>
|
|
32
|
-
<StackItem.Content
|
|
28
|
+
<StackItem.Content>Content</StackItem.Content>
|
|
33
29
|
</StackItem.Root>
|
|
34
|
-
)
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const meta = {
|
|
34
|
+
title: 'ui/react-ui-stack/StackItem',
|
|
35
|
+
component: StackItem.Root as any,
|
|
36
|
+
render: DefaultStory,
|
|
35
37
|
decorators: [withTheme],
|
|
36
38
|
parameters: {
|
|
37
39
|
layout: 'centered',
|
|
38
40
|
},
|
|
39
|
-
}
|
|
41
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
40
42
|
|
|
41
43
|
export default meta;
|
|
42
44
|
|
|
43
|
-
type Story = StoryObj<typeof
|
|
45
|
+
type Story = StoryObj<typeof meta>;
|
|
44
46
|
|
|
45
47
|
export const Default: Story = {
|
|
46
48
|
args: {
|
|
@@ -7,46 +7,47 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
|
|
|
7
7
|
import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
|
|
8
8
|
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
|
9
9
|
import {
|
|
10
|
+
type Edge,
|
|
10
11
|
attachClosestEdge,
|
|
11
12
|
extractClosestEdge,
|
|
12
|
-
type Edge,
|
|
13
13
|
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
14
14
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
15
15
|
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
16
16
|
import React, {
|
|
17
|
-
forwardRef,
|
|
18
|
-
useLayoutEffect,
|
|
19
|
-
useState,
|
|
20
17
|
type ComponentPropsWithRef,
|
|
21
|
-
useCallback,
|
|
22
18
|
type ReactNode,
|
|
19
|
+
forwardRef,
|
|
20
|
+
useCallback,
|
|
21
|
+
useLayoutEffect,
|
|
23
22
|
useMemo,
|
|
23
|
+
useState,
|
|
24
24
|
} from 'react';
|
|
25
25
|
import { createPortal } from 'react-dom';
|
|
26
26
|
|
|
27
|
-
import { type ThemedClassName
|
|
27
|
+
import { ListItem, type ThemedClassName } from '@dxos/react-ui';
|
|
28
28
|
import { resizeAttributes, sizeStyle } from '@dxos/react-ui-dnd';
|
|
29
29
|
import { mx } from '@dxos/react-ui-theme';
|
|
30
30
|
|
|
31
|
+
import { type StackItemData, type StackItemSize } from '../defs';
|
|
32
|
+
import { type ItemDragState, StackItemContext, idle, useStack, useStackItem } from '../StackContext';
|
|
33
|
+
|
|
31
34
|
import { StackItemContent, type StackItemContentProps } from './StackItemContent';
|
|
32
35
|
import { StackItemDragHandle, type StackItemDragHandleProps } from './StackItemDragHandle';
|
|
33
36
|
import {
|
|
34
37
|
StackItemHeading,
|
|
35
38
|
StackItemHeadingLabel,
|
|
36
|
-
type StackItemHeadingProps,
|
|
37
39
|
type StackItemHeadingLabelProps,
|
|
40
|
+
type StackItemHeadingProps,
|
|
38
41
|
StackItemHeadingStickyContent,
|
|
39
42
|
} from './StackItemHeading';
|
|
40
43
|
import { StackItemResizeHandle, type StackItemResizeHandleProps } from './StackItemResizeHandle';
|
|
41
44
|
import {
|
|
42
45
|
StackItemSigil,
|
|
43
|
-
type StackItemSigilProps,
|
|
44
46
|
type StackItemSigilAction,
|
|
45
|
-
type StackItemSigilButtonProps,
|
|
46
47
|
StackItemSigilButton,
|
|
48
|
+
type StackItemSigilButtonProps,
|
|
49
|
+
type StackItemSigilProps,
|
|
47
50
|
} from './StackItemSigil';
|
|
48
|
-
import { useStack, StackItemContext, idle, type ItemDragState, useStackItem } from '../StackContext';
|
|
49
|
-
import { type StackItemSize, type StackItemData } from '../defs';
|
|
50
51
|
|
|
51
52
|
// NOTE: 48rem fills the screen on a MacbookPro with the sidebars closed.
|
|
52
53
|
export const DEFAULT_HORIZONTAL_SIZE = 48 satisfies StackItemSize;
|
|
@@ -62,7 +63,7 @@ type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
|
|
|
62
63
|
onSizeChange?: (nextSize: StackItemSize) => void;
|
|
63
64
|
role?: 'article' | 'section';
|
|
64
65
|
disableRearrange?: boolean;
|
|
65
|
-
focusIndicatorVariant?: 'over-all' | 'group';
|
|
66
|
+
focusIndicatorVariant?: 'over-all' | 'group' | 'over-all-always' | 'group-always';
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
@@ -89,7 +90,7 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
89
90
|
const [closestEdge, setEdge] = useState<Edge | null>(null);
|
|
90
91
|
const [sourceId, setSourceId] = useState<string | null>(null);
|
|
91
92
|
const [dragState, setDragState] = useState<ItemDragState>(idle);
|
|
92
|
-
const { orientation, rail, onRearrange } = useStack();
|
|
93
|
+
const { orientation, rail, onRearrange, size: stackSize, stackId } = useStack();
|
|
93
94
|
const [size = orientation === 'horizontal' ? DEFAULT_HORIZONTAL_SIZE : DEFAULT_VERTICAL_SIZE, setInternalSize] =
|
|
94
95
|
useState(propsSize);
|
|
95
96
|
|
|
@@ -231,18 +232,25 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
231
232
|
'group/stack-item grid relative',
|
|
232
233
|
focusIndicatorVariant === 'over-all'
|
|
233
234
|
? 'dx-focus-ring-inset-over-all'
|
|
234
|
-
:
|
|
235
|
-
? 'dx-focus-ring-
|
|
236
|
-
: '
|
|
235
|
+
: focusIndicatorVariant === 'over-all-always'
|
|
236
|
+
? 'dx-focus-ring-inset-over-all-always'
|
|
237
|
+
: orientation === 'horizontal'
|
|
238
|
+
? focusIndicatorVariant === 'group-always'
|
|
239
|
+
? 'dx-focus-ring-group-x-always'
|
|
240
|
+
: 'dx-focus-ring-group-x'
|
|
241
|
+
: focusIndicatorVariant === 'group-always'
|
|
242
|
+
? 'dx-focus-ring-group-y-always'
|
|
243
|
+
: 'dx-focus-ring-group-y',
|
|
237
244
|
orientation === 'horizontal' ? 'grid-rows-subgrid' : 'grid-cols-subgrid',
|
|
238
245
|
rail && (orientation === 'horizontal' ? 'row-span-2' : 'col-span-2'),
|
|
239
246
|
role === 'section' && orientation !== 'horizontal' && 'border-be border-subduedSeparator',
|
|
240
247
|
classNames,
|
|
241
248
|
)}
|
|
242
|
-
data-dx-stack-item
|
|
249
|
+
data-dx-stack-item={stackId}
|
|
250
|
+
data-dx-item-id={item.id}
|
|
243
251
|
{...resizeAttributes}
|
|
244
252
|
style={{
|
|
245
|
-
...sizeStyle(size, orientation),
|
|
253
|
+
...(stackSize !== 'split' && sizeStyle(size, orientation)),
|
|
246
254
|
...(Number.isFinite(order) && {
|
|
247
255
|
[orientation === 'horizontal' ? 'gridColumn' : 'gridRow']: `${order}`,
|
|
248
256
|
}),
|
|
@@ -9,7 +9,8 @@ import { mx } from '@dxos/react-ui-theme';
|
|
|
9
9
|
|
|
10
10
|
import { useStack, useStackItem } from '../StackContext';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// TODO(burdon): Add prop for container-max-width?
|
|
13
|
+
export type StackItemContentProps = ThemedClassName<Omit<ComponentPropsWithoutRef<'div'>, 'role' | 'scrollable'>> & {
|
|
13
14
|
/**
|
|
14
15
|
* This flag is required in order to clarify a developer experience that seemed like it needed extra boilerplate
|
|
15
16
|
* (`row-span-2`) or was buggy. See the description of the StackItem.Content component itself for more information.
|
|
@@ -22,15 +23,21 @@ export type StackItemContentProps = ThemedClassName<ComponentPropsWithoutRef<'di
|
|
|
22
23
|
statusbar?: boolean;
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Whether
|
|
26
|
+
* Whether to support y-axis scrolling.
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
+
scrollable?: boolean;
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
* Whether to set a certain aspect ratio on the content, including the toolbar and statusbar.
|
|
31
|
-
* convenience and consistency; it can instead be specified by the `classNames` or `style` props as needed.
|
|
31
|
+
* Whether to set a certain aspect ratio on the content, including the toolbar and statusbar.
|
|
32
|
+
* This is provided for convenience and consistency; it can instead be specified by the `classNames` or `style` props as needed.
|
|
32
33
|
*/
|
|
33
34
|
size?: 'intrinsic' | 'video' | 'square';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether the consumer intends to do something custom and typical affordances should not apply.
|
|
38
|
+
* @deprecated Replace with override for gridTempateRows.
|
|
39
|
+
*/
|
|
40
|
+
layoutManaged?: boolean;
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
/**
|
|
@@ -38,7 +45,10 @@ export type StackItemContentProps = ThemedClassName<ComponentPropsWithoutRef<'di
|
|
|
38
45
|
* The `toolbar` flag must be provided since this component provides for the layout of content with the toolbar.
|
|
39
46
|
*/
|
|
40
47
|
export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps>(
|
|
41
|
-
(
|
|
48
|
+
(
|
|
49
|
+
{ children, toolbar, statusbar, layoutManaged, classNames, size = 'intrinsic', scrollable, ...props },
|
|
50
|
+
forwardedRef,
|
|
51
|
+
) => {
|
|
42
52
|
const { size: stackItemSize } = useStack();
|
|
43
53
|
const { role } = useStackItem();
|
|
44
54
|
const style = useMemo(
|
|
@@ -54,18 +64,20 @@ export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps
|
|
|
54
64
|
},
|
|
55
65
|
[toolbar, statusbar, layoutManaged],
|
|
56
66
|
);
|
|
67
|
+
|
|
57
68
|
return (
|
|
58
69
|
<div
|
|
59
70
|
role='none'
|
|
60
71
|
{...props}
|
|
61
72
|
className={mx(
|
|
62
|
-
'group grid grid-cols-[100%]',
|
|
63
|
-
stackItemSize === 'contain' && 'min-bs-0 overflow-hidden',
|
|
73
|
+
'group grid grid-cols-[100%] density-coarse',
|
|
64
74
|
size === 'video' ? 'aspect-video' : size === 'square' && 'aspect-square',
|
|
65
|
-
|
|
75
|
+
stackItemSize === 'contain' && 'min-bs-0 overflow-hidden',
|
|
76
|
+
scrollable ? 'min-bs-0 overflow-y-auto scrollbar-thin contain-layout' : 'overflow-hidden',
|
|
66
77
|
role === 'section' &&
|
|
67
78
|
toolbar &&
|
|
68
79
|
'[&_.dx-toolbar]:sticky [&_.dx-toolbar]:z-[1] [&_.dx-toolbar]:block-start-0 [&_.dx-toolbar]:-mbe-px [&_.dx-toolbar]:min-is-0',
|
|
80
|
+
toolbar && '[&>.dx-toolbar]:relative [&>.dx-toolbar]:border-be [&>.dx-toolbar]:border-subduedSeparator',
|
|
69
81
|
classNames,
|
|
70
82
|
)}
|
|
71
83
|
style={style}
|
|
@@ -2,17 +2,16 @@
|
|
|
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
|
-
type ComponentPropsWithoutRef,
|
|
9
7
|
type ComponentPropsWithRef,
|
|
10
|
-
|
|
8
|
+
type ComponentPropsWithoutRef,
|
|
11
9
|
type PropsWithChildren,
|
|
10
|
+
forwardRef,
|
|
12
11
|
} from 'react';
|
|
13
12
|
|
|
14
13
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
15
|
-
import {
|
|
14
|
+
import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-attention';
|
|
16
15
|
import { mx } from '@dxos/react-ui-theme';
|
|
17
16
|
|
|
18
17
|
import { useStack } from '../StackContext';
|
|
@@ -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',
|
|
@@ -6,9 +6,10 @@ import React from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { ResizeHandle } from '@dxos/react-ui-dnd';
|
|
8
8
|
|
|
9
|
-
import { DEFAULT_EXTRINSIC_SIZE } from './StackItem';
|
|
10
9
|
import { useStack, useStackItem } from '../StackContext';
|
|
11
10
|
|
|
11
|
+
import { DEFAULT_EXTRINSIC_SIZE } from './StackItem';
|
|
12
|
+
|
|
12
13
|
const MIN_WIDTH = 20;
|
|
13
14
|
const MIN_HEIGHT = 3;
|
|
14
15
|
|
|
@@ -11,9 +11,10 @@ import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-at
|
|
|
11
11
|
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
12
12
|
import { getHostPlatform } from '@dxos/util';
|
|
13
13
|
|
|
14
|
-
import { MenuSignifierHorizontal } from './MenuSignifier';
|
|
15
14
|
import { translationKey } from '../../translations';
|
|
16
15
|
|
|
16
|
+
import { MenuSignifierHorizontal } from './MenuSignifier';
|
|
17
|
+
|
|
17
18
|
export type KeyBinding = {
|
|
18
19
|
windows?: string;
|
|
19
20
|
macos?: string;
|