@dxos/react-ui-stack 0.8.4-main.b97322e → 0.8.4-main.bc674ce
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 +923 -50
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/playwright/index.mjs +4 -0
- package/dist/lib/browser/playwright/index.mjs.map +2 -2
- package/dist/lib/node-esm/index.mjs +924 -50
- 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 +4 -0
- 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/components/CardStack/CardStack.stories.d.ts +15 -0
- package/dist/types/src/components/CardStack/CardStack.stories.d.ts.map +1 -0
- package/dist/types/src/{exemplars → components}/CardStack/CardStackDragPreview.d.ts +1 -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 +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 +9 -12
- 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 +4 -37
- 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 +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 -1
- 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 +9 -7
- 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 +19 -18
- package/src/{exemplars → components}/CardStack/CardStack.tsx +114 -54
- package/src/{exemplars → components}/CardStack/CardStackDragPreview.tsx +3 -3
- package/src/components/Stack/Stack.stories.tsx +8 -9
- package/src/components/Stack/Stack.tsx +225 -26
- package/src/components/StackContext.tsx +2 -1
- package/src/components/StackItem/StackItem.stories.tsx +19 -15
- package/src/components/StackItem/StackItem.tsx +49 -29
- package/src/components/StackItem/StackItemContent.tsx +18 -39
- package/src/components/StackItem/StackItemHeading.tsx +5 -9
- package/src/components/StackItem/StackItemResizeHandle.tsx +2 -1
- package/src/components/StackItem/StackItemSigil.tsx +5 -4
- package/src/components/deprecated/LayoutControls.tsx +3 -0
- package/src/components/index.ts +2 -1
- package/src/hooks/useStackDropForElements.ts +59 -45
- 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-P3TQV4BA.mjs +0 -1198
- package/dist/lib/browser/chunk-P3TQV4BA.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-3WVEPAJ4.mjs +0 -1200
- package/dist/lib/node-esm/chunk-3WVEPAJ4.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/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 -13
- 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 -12
- 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 +0 -9
- 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/exemplars/Card/Card.stories.tsx +0 -78
- package/src/exemplars/Card/Card.tsx +0 -186
- package/src/exemplars/Card/CardDragPreview.tsx +0 -22
- package/src/exemplars/Card/fragments.ts +0 -23
- package/src/exemplars/Card/index.ts +0 -7
- package/src/exemplars/index.ts +0 -6
- package/src/testing/CardContainer.tsx +0 -34
- 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
|
@@ -2,40 +2,42 @@
|
|
|
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 FocusEvent,
|
|
11
|
+
type KeyboardEvent,
|
|
11
12
|
forwardRef,
|
|
12
|
-
useState,
|
|
13
|
-
useMemo,
|
|
14
13
|
useCallback,
|
|
15
14
|
useEffect,
|
|
15
|
+
useMemo,
|
|
16
|
+
useState,
|
|
16
17
|
} from 'react';
|
|
17
18
|
|
|
18
|
-
import { type ThemedClassName,
|
|
19
|
-
import { mx } from '@dxos/
|
|
19
|
+
import { ListItem, type ThemedClassName, useId } from '@dxos/react-ui';
|
|
20
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
21
|
|
|
21
22
|
import { useStackDropForElements } from '../../hooks';
|
|
22
23
|
import { StackContext } from '../StackContext';
|
|
23
|
-
import { type StackContextValue } from '../
|
|
24
|
+
import { type StackContextValue } from '../types';
|
|
24
25
|
|
|
25
26
|
export type Orientation = 'horizontal' | 'vertical';
|
|
26
|
-
export type Size = 'intrinsic' | 'contain' | 'contain-fit-content';
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Size is how Stack and its StackItems coordinate the dimensions of the items with the available space.
|
|
30
|
+
* - `intrinsic` signals to Stack and its StackItems to occupy their intrinsic size
|
|
31
|
+
* - Any other size will extrinsically fill the available space along the axis of its orientation and handle overflow:
|
|
32
|
+
* - `contain` causes StackItems to occupy their intrinsic size
|
|
33
|
+
* - `split` divides the Stack’s available space among the StackItems
|
|
34
|
+
*/
|
|
35
|
+
export type Size = 'intrinsic' | 'contain' | 'split';
|
|
34
36
|
|
|
35
37
|
export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
36
38
|
export const railGridVertical = 'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
37
39
|
|
|
38
|
-
// TODO(ZaymonFC): Magic 2px to stop overflow
|
|
40
|
+
// TODO(ZaymonFC): Magic 2px to stop overflow.
|
|
39
41
|
export const railGridHorizontalContainFitContent =
|
|
40
42
|
'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_fit-content(calc(100%-var(--rail-size)*2+2px))_[content-end]]';
|
|
41
43
|
export const railGridVerticalContainFitContent =
|
|
@@ -43,6 +45,24 @@ export const railGridVerticalContainFitContent =
|
|
|
43
45
|
|
|
44
46
|
export const autoScrollRootAttributes = { 'data-drag-autoscroll': 'idle' };
|
|
45
47
|
|
|
48
|
+
const PERPENDICULAR_FOCUS_THRESHHOLD = 128;
|
|
49
|
+
|
|
50
|
+
const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orientation']) => {
|
|
51
|
+
el.scrollIntoView({
|
|
52
|
+
behavior: 'instant',
|
|
53
|
+
[orientation === 'vertical' ? 'block' : 'inline']: 'center',
|
|
54
|
+
});
|
|
55
|
+
return el.focus();
|
|
56
|
+
};
|
|
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
|
+
|
|
46
66
|
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
47
67
|
(
|
|
48
68
|
{
|
|
@@ -50,23 +70,25 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
50
70
|
classNames,
|
|
51
71
|
style,
|
|
52
72
|
orientation = 'vertical',
|
|
53
|
-
rail = true,
|
|
73
|
+
rail = true, // TODO(burdon): Change default to false.
|
|
54
74
|
size = 'intrinsic',
|
|
55
75
|
onRearrange,
|
|
56
76
|
itemsCount = Children.count(children),
|
|
57
77
|
getDropElement,
|
|
58
78
|
separatorOnScroll,
|
|
79
|
+
circularFocus,
|
|
59
80
|
...props
|
|
60
81
|
},
|
|
61
82
|
forwardedRef,
|
|
62
83
|
) => {
|
|
84
|
+
const stackId = useId('stack', props.id);
|
|
63
85
|
const [stackElement, stackRef] = useState<HTMLDivElement | null>(null);
|
|
86
|
+
const [lastFocusedItem, setLastFocusedItem] = useState<string>();
|
|
64
87
|
const composedItemRef = composeRefs<HTMLDivElement>(stackRef, forwardedRef);
|
|
65
|
-
const arrowNavigationAttrs = useArrowNavigationGroup({ axis: orientation });
|
|
66
88
|
|
|
67
89
|
const styles: CSSProperties = {
|
|
68
90
|
[orientation === 'horizontal' ? 'gridTemplateColumns' : 'gridTemplateRows']:
|
|
69
|
-
`repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
91
|
+
size === 'split' ? `repeat(${itemsCount}, 1fr)` : `repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
70
92
|
...style,
|
|
71
93
|
};
|
|
72
94
|
|
|
@@ -97,14 +119,187 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
97
119
|
}
|
|
98
120
|
}, [stackElement, separatorOnScroll, orientation]);
|
|
99
121
|
|
|
122
|
+
/**
|
|
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.
|
|
143
|
+
*/
|
|
144
|
+
const handleKeyDown = useCallback(
|
|
145
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
146
|
+
const target = event.target as HTMLElement;
|
|
147
|
+
if (
|
|
148
|
+
event.key.startsWith('Arrow') &&
|
|
149
|
+
!target.closest(
|
|
150
|
+
`input, textarea, [role="textbox"], [data-tabster*="mover"], [data-arrow-keys="all"], [data-arrow-keys~="${event.key.toLowerCase().slice(5)}"]`,
|
|
151
|
+
)
|
|
152
|
+
) {
|
|
153
|
+
const closestOwnedItem = target.closest(`[data-dx-stack-item="${stackId}"]`);
|
|
154
|
+
const closestStack = target.closest('[data-dx-stack]') as HTMLElement | null;
|
|
155
|
+
const closestStackItems = Array.from(
|
|
156
|
+
closestStack?.querySelectorAll(`[data-dx-stack-item="${stackId}"]`) ?? [],
|
|
157
|
+
);
|
|
158
|
+
const closestStackOrientation = closestStack?.getAttribute('aria-orientation') as Orientation;
|
|
159
|
+
const ancestorStack = closestStack?.parentElement?.closest('[data-dx-stack]') as HTMLElement | null;
|
|
160
|
+
if (closestOwnedItem && closestStack) {
|
|
161
|
+
const ancestorOrientation = ancestorStack?.getAttribute('aria-orientation') as Orientation | undefined;
|
|
162
|
+
const parallelDelta = (
|
|
163
|
+
closestStackOrientation === 'vertical' ? event.key === 'ArrowUp' : event.key === 'ArrowLeft'
|
|
164
|
+
)
|
|
165
|
+
? -1
|
|
166
|
+
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowDown' : event.key === 'ArrowRight')
|
|
167
|
+
? 1
|
|
168
|
+
: 0;
|
|
169
|
+
const perpendicularDelta = (
|
|
170
|
+
closestStackOrientation === 'vertical' ? event.key === 'ArrowLeft' : event.key === 'ArrowUp'
|
|
171
|
+
)
|
|
172
|
+
? -1
|
|
173
|
+
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowRight' : event.key === 'ArrowDown')
|
|
174
|
+
? 1
|
|
175
|
+
: 0;
|
|
176
|
+
if (parallelDelta !== 0) {
|
|
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
|
+
|
|
193
|
+
if (adjacentItem) {
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
scrollIntoViewAndFocus(adjacentItem, closestStackOrientation);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (perpendicularDelta !== 0) {
|
|
200
|
+
if (ancestorStack && ancestorOrientation !== closestStackOrientation) {
|
|
201
|
+
const siblingStacks = Array.from(
|
|
202
|
+
ancestorStack.querySelectorAll(
|
|
203
|
+
`[data-dx-stack-item="${ancestorStack.getAttribute('data-dx-stack')}"] [data-dx-stack]`,
|
|
204
|
+
),
|
|
205
|
+
) as HTMLElement[];
|
|
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
|
+
}
|
|
221
|
+
const adjacentStackSelfItem = adjacentStack?.closest(
|
|
222
|
+
`[data-dx-stack-item=${ancestorStack.getAttribute('data-dx-stack')}]`,
|
|
223
|
+
) as HTMLElement | undefined;
|
|
224
|
+
const adjacentStackItems = adjacentStack
|
|
225
|
+
? (Array.from(
|
|
226
|
+
adjacentStack.querySelectorAll(
|
|
227
|
+
`[data-dx-stack-item="${adjacentStack.getAttribute('data-dx-stack')}"]`,
|
|
228
|
+
),
|
|
229
|
+
) as HTMLElement[])
|
|
230
|
+
: [];
|
|
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.
|
|
233
|
+
let closestItem = adjacentStackItems[0];
|
|
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;
|
|
245
|
+
|
|
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
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
scrollIntoViewAndFocus(closestItem, closestStackOrientation);
|
|
263
|
+
} else if (adjacentStackSelfItem) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
scrollIntoViewAndFocus(adjacentStackSelfItem, ancestorOrientation);
|
|
266
|
+
}
|
|
267
|
+
} else if (closestOwnedItem) {
|
|
268
|
+
const closestOwnedItemStack = closestOwnedItem.querySelector('[data-dx-stack]');
|
|
269
|
+
const closestOwnedItemStackItems = closestOwnedItemStack
|
|
270
|
+
? (Array.from(
|
|
271
|
+
closestOwnedItemStack.querySelectorAll(
|
|
272
|
+
`[data-dx-stack-item="${closestOwnedItemStack.getAttribute('data-dx-stack')}"]`,
|
|
273
|
+
),
|
|
274
|
+
) as HTMLElement[])
|
|
275
|
+
: [];
|
|
276
|
+
if (closestOwnedItemStackItems.length > 0) {
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
scrollIntoViewAndFocus(
|
|
279
|
+
closestOwnedItemStackItems[
|
|
280
|
+
['ArrowUp', 'ArrowLeft'].includes(event.key) ? closestOwnedItemStackItems.length - 1 : 0
|
|
281
|
+
],
|
|
282
|
+
closestOwnedItemStack?.getAttribute('aria-orientation') as Orientation,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
props.onKeyDown?.(event);
|
|
290
|
+
},
|
|
291
|
+
[props.onKeyDown, stackId, circularFocus],
|
|
292
|
+
);
|
|
293
|
+
|
|
100
294
|
const gridClasses = useMemo(() => {
|
|
101
295
|
if (!rail) {
|
|
102
|
-
return orientation === 'horizontal' ? 'grid-rows-1 pli-
|
|
296
|
+
return orientation === 'horizontal' ? 'grid-rows-1 pli-[--stack-gap]' : 'grid-cols-1 plb-[--stack-gap]';
|
|
103
297
|
}
|
|
298
|
+
|
|
104
299
|
if (orientation === 'horizontal') {
|
|
105
|
-
return
|
|
300
|
+
return railGridHorizontal;
|
|
106
301
|
} else {
|
|
107
|
-
return
|
|
302
|
+
return railGridVertical;
|
|
108
303
|
}
|
|
109
304
|
}, [rail, orientation, size]);
|
|
110
305
|
|
|
@@ -125,19 +320,23 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
125
320
|
}, [stackElement, handleScroll]);
|
|
126
321
|
|
|
127
322
|
return (
|
|
128
|
-
<StackContext.Provider value={{ orientation, rail, size, onRearrange }}>
|
|
323
|
+
<StackContext.Provider value={{ orientation, rail, size, onRearrange, stackId }}>
|
|
129
324
|
<div
|
|
130
325
|
{...props}
|
|
131
|
-
{...arrowNavigationAttrs}
|
|
132
326
|
className={mx(
|
|
133
|
-
'grid relative',
|
|
327
|
+
'grid relative [--stack-gap:var(--dx-trimXs)]',
|
|
134
328
|
gridClasses,
|
|
135
|
-
|
|
329
|
+
size === 'contain' &&
|
|
136
330
|
(orientation === 'horizontal'
|
|
137
|
-
? 'overflow-x-auto min-bs-0 max-bs-full bs-full'
|
|
331
|
+
? 'overflow-x-auto overscroll-x-contain min-bs-0 max-bs-full bs-full'
|
|
138
332
|
: 'overflow-y-auto min-is-0 max-is-full is-full'),
|
|
139
333
|
classNames,
|
|
140
334
|
)}
|
|
335
|
+
onKeyDown={handleKeyDown}
|
|
336
|
+
onBlur={handleBlur}
|
|
337
|
+
data-dx-stack={stackId}
|
|
338
|
+
data-dx-stack-circular-focus={circularFocus}
|
|
339
|
+
data-dx-last-focused-item={lastFocusedItem}
|
|
141
340
|
data-rail={rail}
|
|
142
341
|
aria-orientation={orientation}
|
|
143
342
|
style={styles}
|
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
import { createContext, useContext } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type Orientation, type Size } from './Stack';
|
|
8
|
-
import { type
|
|
8
|
+
import { type StackItemRearrangeHandler, type StackItemSize } from './types';
|
|
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,22 +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
|
-
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
|
-
|
|
19
|
-
<StackItem.Root role='section' {...args} classNames='w-[20rem] border border-separator'>
|
|
20
|
-
<StackItem.Heading>
|
|
13
|
+
const DefaultStory = (props: StackItemRootProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<StackItem.Root role='section' {...props} classNames='is-[20rem] border border-separator'>
|
|
16
|
+
<StackItem.Heading classNames='is-full border-be border-separator'>
|
|
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'>
|
|
23
19
|
<DropdownMenu.Root>
|
|
@@ -29,18 +25,26 @@ const meta: Meta<typeof StackItem.Root> = {
|
|
|
29
25
|
</DropdownMenu.Root>
|
|
30
26
|
</div>
|
|
31
27
|
</StackItem.Heading>
|
|
32
|
-
<StackItem.Content
|
|
28
|
+
<StackItem.Content>
|
|
29
|
+
<div className='p-4 text-center'>Content</div>
|
|
30
|
+
</StackItem.Content>
|
|
33
31
|
</StackItem.Root>
|
|
34
|
-
)
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const meta = {
|
|
36
|
+
title: 'ui/react-ui-stack/StackItem',
|
|
37
|
+
component: StackItem.Root as any,
|
|
38
|
+
render: DefaultStory,
|
|
35
39
|
decorators: [withTheme],
|
|
36
40
|
parameters: {
|
|
37
41
|
layout: 'centered',
|
|
38
42
|
},
|
|
39
|
-
}
|
|
43
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
40
44
|
|
|
41
45
|
export default meta;
|
|
42
46
|
|
|
43
|
-
type Story = StoryObj<typeof
|
|
47
|
+
type Story = StoryObj<typeof meta>;
|
|
44
48
|
|
|
45
49
|
export const Default: Story = {
|
|
46
50
|
args: {
|
|
@@ -7,52 +7,57 @@ 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
|
-
import { mx } from '@dxos/
|
|
29
|
+
import { mx } from '@dxos/ui-theme';
|
|
30
|
+
|
|
31
|
+
import { type ItemDragState, StackItemContext, idle, useStack, useStackItem } from '../StackContext';
|
|
32
|
+
import { type StackItemData, type StackItemSize } from '../types';
|
|
30
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;
|
|
53
54
|
export const DEFAULT_VERTICAL_SIZE = 'min-content' satisfies StackItemSize;
|
|
54
55
|
export const DEFAULT_EXTRINSIC_SIZE = DEFAULT_HORIZONTAL_SIZE satisfies StackItemSize;
|
|
55
56
|
|
|
57
|
+
//
|
|
58
|
+
// StackItemRoot
|
|
59
|
+
//
|
|
60
|
+
|
|
56
61
|
type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
|
|
57
62
|
item: Omit<StackItemData, 'type'>;
|
|
58
63
|
order?: number;
|
|
@@ -62,7 +67,7 @@ type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
|
|
|
62
67
|
onSizeChange?: (nextSize: StackItemSize) => void;
|
|
63
68
|
role?: 'article' | 'section';
|
|
64
69
|
disableRearrange?: boolean;
|
|
65
|
-
focusIndicatorVariant?: 'over-all' | 'group';
|
|
70
|
+
focusIndicatorVariant?: 'over-all' | 'group' | 'over-all-always' | 'group-always';
|
|
66
71
|
};
|
|
67
72
|
|
|
68
73
|
const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
@@ -85,18 +90,17 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
85
90
|
forwardedRef,
|
|
86
91
|
) => {
|
|
87
92
|
const [itemElement, itemRef] = useState<HTMLDivElement | null>(null);
|
|
93
|
+
const composedItemRef = composeRefs<HTMLDivElement>(itemRef, forwardedRef);
|
|
88
94
|
const [selfDragHandleElement, selfDragHandleRef] = useState<HTMLDivElement | null>(null);
|
|
89
95
|
const [closestEdge, setEdge] = useState<Edge | null>(null);
|
|
90
96
|
const [sourceId, setSourceId] = useState<string | null>(null);
|
|
91
97
|
const [dragState, setDragState] = useState<ItemDragState>(idle);
|
|
92
|
-
const { orientation, rail, onRearrange } = useStack();
|
|
98
|
+
const { orientation, rail, onRearrange, size: stackSize, stackId } = useStack();
|
|
93
99
|
const [size = orientation === 'horizontal' ? DEFAULT_HORIZONTAL_SIZE : DEFAULT_VERTICAL_SIZE, setInternalSize] =
|
|
94
100
|
useState(propsSize);
|
|
95
101
|
|
|
96
102
|
const Root = role ?? 'div';
|
|
97
103
|
|
|
98
|
-
const composedItemRef = composeRefs<HTMLDivElement>(itemRef, forwardedRef);
|
|
99
|
-
|
|
100
104
|
const setSize = useCallback(
|
|
101
105
|
(nextSize: StackItemSize, commit?: boolean) => {
|
|
102
106
|
setInternalSize(nextSize);
|
|
@@ -109,6 +113,7 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
109
113
|
|
|
110
114
|
const type = orientation === 'horizontal' ? 'column' : 'card';
|
|
111
115
|
|
|
116
|
+
// TODO(burdon): Factor out?
|
|
112
117
|
useLayoutEffect(() => {
|
|
113
118
|
if (!itemElement || !onRearrange || disableRearrange) {
|
|
114
119
|
return;
|
|
@@ -182,18 +187,18 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
182
187
|
|
|
183
188
|
const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
|
|
184
189
|
|
|
185
|
-
// Determine if the drop would result in any changes
|
|
190
|
+
// Determine if the drop would result in any changes.
|
|
186
191
|
const shouldShowDropIndicator = () => {
|
|
187
192
|
if (!closestEdge || !sourceId) {
|
|
188
193
|
return false;
|
|
189
194
|
}
|
|
190
195
|
|
|
191
|
-
// Don't show indicator when dragged item is over itself
|
|
196
|
+
// Don't show indicator when dragged item is over itself.
|
|
192
197
|
if (sourceId === item.id) {
|
|
193
198
|
return false;
|
|
194
199
|
}
|
|
195
200
|
|
|
196
|
-
// 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.
|
|
197
202
|
const isTrailingEdgeOfPrevSibling =
|
|
198
203
|
prevSiblingId !== undefined &&
|
|
199
204
|
sourceId === prevSiblingId &&
|
|
@@ -231,18 +236,25 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
231
236
|
'group/stack-item grid relative',
|
|
232
237
|
focusIndicatorVariant === 'over-all'
|
|
233
238
|
? 'dx-focus-ring-inset-over-all'
|
|
234
|
-
:
|
|
235
|
-
? 'dx-focus-ring-
|
|
236
|
-
: '
|
|
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',
|
|
237
248
|
orientation === 'horizontal' ? 'grid-rows-subgrid' : 'grid-cols-subgrid',
|
|
238
249
|
rail && (orientation === 'horizontal' ? 'row-span-2' : 'col-span-2'),
|
|
239
250
|
role === 'section' && orientation !== 'horizontal' && 'border-be border-subduedSeparator',
|
|
240
251
|
classNames,
|
|
241
252
|
)}
|
|
242
|
-
data-dx-stack-item
|
|
253
|
+
data-dx-stack-item={stackId}
|
|
254
|
+
data-dx-item-id={item.id}
|
|
243
255
|
{...resizeAttributes}
|
|
244
256
|
style={{
|
|
245
|
-
...sizeStyle(size, orientation),
|
|
257
|
+
...(stackSize !== 'split' && sizeStyle(size, orientation)),
|
|
246
258
|
...(Number.isFinite(order) && {
|
|
247
259
|
[orientation === 'horizontal' ? 'gridColumn' : 'gridRow']: `${order}`,
|
|
248
260
|
}),
|
|
@@ -260,37 +272,45 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
|
260
272
|
},
|
|
261
273
|
);
|
|
262
274
|
|
|
275
|
+
//
|
|
276
|
+
// StackItemDragPreview
|
|
277
|
+
//
|
|
278
|
+
|
|
263
279
|
type StackItemDragPreviewProps = {
|
|
264
280
|
children: ({ item }: { item: any }) => ReactNode;
|
|
265
281
|
};
|
|
266
282
|
|
|
267
|
-
|
|
283
|
+
const StackItemDragPreview = ({ children }: StackItemDragPreviewProps) => {
|
|
268
284
|
const { state } = useStackItem();
|
|
269
285
|
return state?.type === 'preview' ? createPortal(children({ item: state.item }), state.container) : null;
|
|
270
286
|
};
|
|
271
287
|
|
|
288
|
+
//
|
|
289
|
+
// StackItem
|
|
290
|
+
//
|
|
291
|
+
|
|
272
292
|
export const StackItem = {
|
|
273
293
|
Root: StackItemRoot,
|
|
274
294
|
Content: StackItemContent,
|
|
295
|
+
DragHandle: StackItemDragHandle,
|
|
296
|
+
DragPreview: StackItemDragPreview,
|
|
275
297
|
Heading: StackItemHeading,
|
|
276
298
|
HeadingLabel: StackItemHeadingLabel,
|
|
277
299
|
HeadingStickyContent: StackItemHeadingStickyContent,
|
|
278
300
|
ResizeHandle: StackItemResizeHandle,
|
|
279
|
-
DragHandle: StackItemDragHandle,
|
|
280
301
|
Sigil: StackItemSigil,
|
|
281
302
|
SigilButton: StackItemSigilButton,
|
|
282
|
-
DragPreview: StackItemDragPreview,
|
|
283
303
|
};
|
|
284
304
|
|
|
285
305
|
export type {
|
|
286
306
|
StackItemRootProps,
|
|
287
307
|
StackItemContentProps,
|
|
308
|
+
StackItemDragHandleProps,
|
|
309
|
+
StackItemDragPreviewProps,
|
|
288
310
|
StackItemHeadingProps,
|
|
289
311
|
StackItemHeadingLabelProps,
|
|
290
312
|
StackItemResizeHandleProps,
|
|
291
|
-
StackItemDragHandleProps,
|
|
292
313
|
StackItemSigilProps,
|
|
293
314
|
StackItemSigilButtonProps,
|
|
294
315
|
StackItemSigilAction,
|
|
295
|
-
StackItemDragPreviewProps,
|
|
296
316
|
};
|