@dxos/react-ui-stack 0.8.4-main.c4373fc → 0.8.4-main.cb12b3f963
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 +706 -71
- 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/browser/translations.mjs +23 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +707 -71
- 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/lib/node-esm/translations.mjs +25 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/Stack/Stack.d.ts +5 -10
- package/dist/types/src/components/Stack/Stack.d.ts.map +1 -1
- 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/MenuSignifier.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItem.d.ts +10 -14
- package/dist/types/src/components/StackItem/StackItem.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItem.stories.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemContent.d.ts +2 -43
- package/dist/types/src/components/StackItem/StackItemContent.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemDragHandle.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 +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/index.d.ts +1 -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 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/playwright/playwright.config.d.ts.map +1 -1
- package/dist/types/src/playwright/stack-manager.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +10 -10
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +49 -47
- package/src/components/Stack/Stack.stories.tsx +10 -12
- package/src/components/Stack/Stack.tsx +207 -218
- package/src/components/StackContext.tsx +1 -1
- package/src/components/StackItem/MenuSignifier.tsx +2 -9
- package/src/components/StackItem/StackItem.stories.tsx +7 -5
- package/src/components/StackItem/StackItem.tsx +37 -27
- package/src/components/StackItem/StackItemContent.tsx +23 -54
- package/src/components/StackItem/StackItemDragHandle.tsx +4 -3
- package/src/components/StackItem/StackItemHeading.tsx +14 -13
- package/src/components/StackItem/StackItemResizeHandle.tsx +1 -2
- package/src/components/StackItem/StackItemSigil.tsx +10 -7
- package/src/components/index.ts +2 -2
- package/src/hooks/useStackDropForElements.ts +60 -46
- package/src/index.ts +0 -4
- package/src/playwright/playwright.config.ts +1 -1
- package/src/translations.ts +9 -9
- package/dist/lib/browser/chunk-SM27YTH3.mjs +0 -1418
- package/dist/lib/browser/chunk-SM27YTH3.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-MMAOXKOM.mjs +0 -1420
- package/dist/lib/node-esm/chunk-MMAOXKOM.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 -30
- 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/components/deprecated/LayoutControls.d.ts +0 -19
- package/dist/types/src/components/deprecated/LayoutControls.d.ts.map +0 -1
- package/dist/types/src/exemplars/Card/Card.d.ts +0 -65
- package/dist/types/src/exemplars/Card/Card.d.ts.map +0 -1
- package/dist/types/src/exemplars/Card/Card.stories.d.ts +0 -21
- 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 +0 -42
- package/dist/types/src/exemplars/CardStack/CardStack.d.ts.map +0 -1
- package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts +0 -15
- package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts.map +0 -1
- package/dist/types/src/exemplars/CardStack/CardStackDragPreview.d.ts +0 -9
- package/dist/types/src/exemplars/CardStack/CardStackDragPreview.d.ts.map +0 -1
- package/dist/types/src/exemplars/CardStack/index.d.ts +0 -3
- 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 -56
- package/src/components/Image/Image.tsx +0 -137
- package/src/components/Image/index.ts +0 -5
- package/src/components/deprecated/LayoutControls.tsx +0 -109
- package/src/exemplars/Card/Card.stories.tsx +0 -64
- package/src/exemplars/Card/Card.tsx +0 -204
- 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/CardStack/CardStack.stories.tsx +0 -173
- package/src/exemplars/CardStack/CardStack.tsx +0 -139
- package/src/exemplars/CardStack/CardStackDragPreview.tsx +0 -61
- package/src/exemplars/CardStack/index.ts +0 -6
- package/src/exemplars/index.ts +0 -6
- package/src/testing/CardContainer.tsx +0 -37
- package/src/testing/index.ts +0 -5
- /package/src/components/{defs.ts → types.ts} +0 -0
|
@@ -4,25 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
6
6
|
import React, {
|
|
7
|
-
type CSSProperties,
|
|
8
7
|
Children,
|
|
9
8
|
type ComponentPropsWithRef,
|
|
9
|
+
type FocusEvent,
|
|
10
10
|
type KeyboardEvent,
|
|
11
11
|
forwardRef,
|
|
12
12
|
useCallback,
|
|
13
13
|
useEffect,
|
|
14
|
-
useMemo,
|
|
15
14
|
useState,
|
|
16
15
|
} from 'react';
|
|
17
16
|
|
|
18
17
|
import { ListItem, type ThemedClassName, useId } from '@dxos/react-ui';
|
|
19
|
-
import { mx } from '@dxos/
|
|
18
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
19
|
|
|
21
20
|
import { useStackDropForElements } from '../../hooks';
|
|
22
|
-
import { type StackContextValue } from '../defs';
|
|
23
21
|
import { StackContext } from '../StackContext';
|
|
22
|
+
import { type StackContextValue } from '../types';
|
|
24
23
|
|
|
25
24
|
export type Orientation = 'horizontal' | 'vertical';
|
|
25
|
+
|
|
26
26
|
/**
|
|
27
27
|
* Size is how Stack and its StackItems coordinate the dimensions of the items with the available space.
|
|
28
28
|
* - `intrinsic` signals to Stack and its StackItems to occupy their intrinsic size
|
|
@@ -32,24 +32,8 @@ export type Orientation = 'horizontal' | 'vertical';
|
|
|
32
32
|
*/
|
|
33
33
|
export type Size = 'intrinsic' | 'contain' | 'split';
|
|
34
34
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
itemsCount?: number;
|
|
38
|
-
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
39
|
-
separatorOnScroll?: number;
|
|
40
|
-
circularFocus?: boolean;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
44
|
-
export const railGridVertical = 'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
45
|
-
|
|
46
|
-
// TODO(ZaymonFC): Magic 2px to stop overflow.
|
|
47
|
-
export const railGridHorizontalContainFitContent =
|
|
48
|
-
'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_fit-content(calc(100%-var(--rail-size)*2+2px))_[content-end]]';
|
|
49
|
-
export const railGridVerticalContainFitContent =
|
|
50
|
-
'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_fit-content(calc(100%-var(--rail-size)*2+2px))_[content-end]]';
|
|
51
|
-
|
|
52
|
-
export const autoScrollRootAttributes = { 'data-drag-autoscroll': 'idle' };
|
|
35
|
+
export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--dx-rail-size)_[content-start]_1fr_[content-end]]';
|
|
36
|
+
export const railGridVertical = 'grid-cols-[[rail-start]_var(--dx-rail-size)_[content-start]_1fr_[content-end]]';
|
|
53
37
|
|
|
54
38
|
const PERPENDICULAR_FOCUS_THRESHHOLD = 128;
|
|
55
39
|
|
|
@@ -61,39 +45,43 @@ const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orient
|
|
|
61
45
|
return el.focus();
|
|
62
46
|
};
|
|
63
47
|
|
|
48
|
+
export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
|
|
49
|
+
Partial<StackContextValue> & {
|
|
50
|
+
itemsCount?: number;
|
|
51
|
+
circularFocus?: boolean;
|
|
52
|
+
separatorOnScroll?: number;
|
|
53
|
+
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
54
|
+
};
|
|
55
|
+
|
|
64
56
|
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
65
57
|
(
|
|
66
58
|
{
|
|
67
59
|
children,
|
|
68
60
|
classNames,
|
|
61
|
+
id,
|
|
69
62
|
style,
|
|
70
63
|
orientation = 'vertical',
|
|
71
64
|
rail = true,
|
|
72
65
|
size = 'intrinsic',
|
|
73
|
-
onRearrange,
|
|
74
66
|
itemsCount = Children.count(children),
|
|
75
|
-
getDropElement,
|
|
76
|
-
separatorOnScroll,
|
|
77
67
|
circularFocus,
|
|
68
|
+
separatorOnScroll,
|
|
69
|
+
getDropElement,
|
|
70
|
+
onBlur,
|
|
71
|
+
onKeyDown,
|
|
72
|
+
onRearrange,
|
|
78
73
|
...props
|
|
79
74
|
},
|
|
80
75
|
forwardedRef,
|
|
81
76
|
) => {
|
|
82
|
-
const stackId = useId('stack',
|
|
77
|
+
const stackId = useId('stack', id);
|
|
83
78
|
const [stackElement, stackRef] = useState<HTMLDivElement | null>(null);
|
|
84
79
|
const [lastFocusedItem, setLastFocusedItem] = useState<string>();
|
|
85
80
|
const composedItemRef = composeRefs<HTMLDivElement>(stackRef, forwardedRef);
|
|
86
81
|
|
|
87
|
-
const
|
|
88
|
-
[orientation === 'horizontal' ? 'gridTemplateColumns' : 'gridTemplateRows']:
|
|
89
|
-
size === 'split' ? `repeat(${itemsCount}, 1fr)` : `repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
90
|
-
...style,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const selfDroppable = !!(itemsCount < 1 && onRearrange && props.id);
|
|
94
|
-
|
|
82
|
+
const selfDroppable = !!(itemsCount < 1 && onRearrange && id);
|
|
95
83
|
const { dropping } = useStackDropForElements({
|
|
96
|
-
id
|
|
84
|
+
id,
|
|
97
85
|
element: getDropElement && stackElement ? getDropElement(stackElement) : stackElement,
|
|
98
86
|
scrollElement: stackElement,
|
|
99
87
|
selfDroppable,
|
|
@@ -101,6 +89,7 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
101
89
|
onRearrange,
|
|
102
90
|
});
|
|
103
91
|
|
|
92
|
+
/** Updates scroll separator data attributes based on current scroll position. */
|
|
104
93
|
const handleScroll = useCallback(() => {
|
|
105
94
|
if (stackElement && Number.isFinite(separatorOnScroll)) {
|
|
106
95
|
const scrollPosition = orientation === 'horizontal' ? stackElement.scrollLeft : stackElement.scrollTop;
|
|
@@ -117,11 +106,9 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
117
106
|
}
|
|
118
107
|
}, [stackElement, separatorOnScroll, orientation]);
|
|
119
108
|
|
|
120
|
-
/**
|
|
121
|
-
* Handles blur events to track the last focused item within this stack.
|
|
122
|
-
*/
|
|
109
|
+
/** Handles blur events to track the last focused item within this stack. */
|
|
123
110
|
const handleBlur = useCallback(
|
|
124
|
-
(event:
|
|
111
|
+
(event: FocusEvent<HTMLDivElement>) => {
|
|
125
112
|
if (event.target) {
|
|
126
113
|
const target = event.target as HTMLElement;
|
|
127
114
|
const closestStackItem = target.closest(`[data-dx-item-id]`) as HTMLElement | null;
|
|
@@ -129,178 +116,15 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
129
116
|
setLastFocusedItem(closestStackItem?.getAttribute('data-dx-item-id') ?? undefined);
|
|
130
117
|
}
|
|
131
118
|
}
|
|
132
|
-
|
|
119
|
+
onBlur?.(event);
|
|
133
120
|
},
|
|
134
|
-
[stackId,
|
|
121
|
+
[stackId, onBlur],
|
|
135
122
|
);
|
|
136
123
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
* orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item; or, if there is no
|
|
140
|
-
* such stack item, focus is passed to the adjacent empty stack if one can be found.
|
|
141
|
-
*/
|
|
142
|
-
const handleKeyDown = useCallback(
|
|
143
|
-
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
144
|
-
const target = event.target as HTMLElement;
|
|
145
|
-
if (
|
|
146
|
-
event.key.startsWith('Arrow') &&
|
|
147
|
-
!target.closest(
|
|
148
|
-
`input, textarea, [role="textbox"], [data-tabster*="mover"], [data-arrow-keys="all"], [data-arrow-keys~="${event.key.toLowerCase().slice(5)}"]`,
|
|
149
|
-
)
|
|
150
|
-
) {
|
|
151
|
-
const closestOwnedItem = target.closest(`[data-dx-stack-item="${stackId}"]`);
|
|
152
|
-
const closestStack = target.closest('[data-dx-stack]') as HTMLElement | null;
|
|
153
|
-
const closestStackItems = Array.from(
|
|
154
|
-
closestStack?.querySelectorAll(`[data-dx-stack-item="${stackId}"]`) ?? [],
|
|
155
|
-
);
|
|
156
|
-
const closestStackOrientation = closestStack?.getAttribute('aria-orientation') as Orientation;
|
|
157
|
-
const ancestorStack = closestStack?.parentElement?.closest('[data-dx-stack]') as HTMLElement | null;
|
|
158
|
-
if (closestOwnedItem && closestStack) {
|
|
159
|
-
const ancestorOrientation = ancestorStack?.getAttribute('aria-orientation') as Orientation | undefined;
|
|
160
|
-
const parallelDelta = (
|
|
161
|
-
closestStackOrientation === 'vertical' ? event.key === 'ArrowUp' : event.key === 'ArrowLeft'
|
|
162
|
-
)
|
|
163
|
-
? -1
|
|
164
|
-
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowDown' : event.key === 'ArrowRight')
|
|
165
|
-
? 1
|
|
166
|
-
: 0;
|
|
167
|
-
const perpendicularDelta = (
|
|
168
|
-
closestStackOrientation === 'vertical' ? event.key === 'ArrowLeft' : event.key === 'ArrowUp'
|
|
169
|
-
)
|
|
170
|
-
? -1
|
|
171
|
-
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowRight' : event.key === 'ArrowDown')
|
|
172
|
-
? 1
|
|
173
|
-
: 0;
|
|
174
|
-
if (parallelDelta !== 0) {
|
|
175
|
-
const currentIndex = closestStackItems.indexOf(closestOwnedItem);
|
|
176
|
-
const nextIndex = currentIndex + parallelDelta;
|
|
177
|
-
let adjacentItem: HTMLElement | undefined;
|
|
178
|
-
|
|
179
|
-
if (circularFocus) {
|
|
180
|
-
// Circular navigation: wrap around using modulo.
|
|
181
|
-
adjacentItem = closestStackItems[(nextIndex + closestStackItems.length) % closestStackItems.length] as
|
|
182
|
-
| HTMLElement
|
|
183
|
-
| undefined;
|
|
184
|
-
} else {
|
|
185
|
-
// Non-circular navigation: only move if within bounds.
|
|
186
|
-
if (nextIndex >= 0 && nextIndex < closestStackItems.length) {
|
|
187
|
-
adjacentItem = closestStackItems[nextIndex] as HTMLElement | undefined;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (adjacentItem) {
|
|
192
|
-
event.preventDefault();
|
|
193
|
-
scrollIntoViewAndFocus(adjacentItem, closestStackOrientation);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (perpendicularDelta !== 0) {
|
|
197
|
-
if (ancestorStack && ancestorOrientation !== closestStackOrientation) {
|
|
198
|
-
const siblingStacks = Array.from(
|
|
199
|
-
ancestorStack.querySelectorAll(
|
|
200
|
-
`[data-dx-stack-item="${ancestorStack.getAttribute('data-dx-stack')}"] [data-dx-stack]`,
|
|
201
|
-
),
|
|
202
|
-
) as HTMLElement[];
|
|
203
|
-
const currentStackIndex = siblingStacks.indexOf(closestStack);
|
|
204
|
-
const nextStackIndex = currentStackIndex + perpendicularDelta;
|
|
205
|
-
let adjacentStack: HTMLElement | undefined;
|
|
206
|
-
|
|
207
|
-
if (ancestorStack.getAttribute('data-dx-stack-circular-focus') === 'true') {
|
|
208
|
-
// Circular navigation: wrap around using modulo.
|
|
209
|
-
adjacentStack = siblingStacks[(nextStackIndex + siblingStacks.length) % siblingStacks.length] as
|
|
210
|
-
| HTMLElement
|
|
211
|
-
| undefined;
|
|
212
|
-
} else {
|
|
213
|
-
// Non-circular navigation: only move if within bounds.
|
|
214
|
-
if (nextStackIndex >= 0 && nextStackIndex < siblingStacks.length) {
|
|
215
|
-
adjacentStack = siblingStacks[nextStackIndex] as HTMLElement | undefined;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const adjacentStackSelfItem = adjacentStack?.closest(
|
|
219
|
-
`[data-dx-stack-item=${ancestorStack.getAttribute('data-dx-stack')}]`,
|
|
220
|
-
) as HTMLElement | undefined;
|
|
221
|
-
const adjacentStackItems = adjacentStack
|
|
222
|
-
? (Array.from(
|
|
223
|
-
adjacentStack.querySelectorAll(
|
|
224
|
-
`[data-dx-stack-item="${adjacentStack.getAttribute('data-dx-stack')}"]`,
|
|
225
|
-
),
|
|
226
|
-
) as HTMLElement[])
|
|
227
|
-
: [];
|
|
228
|
-
if (adjacentStack && adjacentStackItems.length > 0) {
|
|
229
|
-
// Check if the adjacent stack has a last focused item recorded, otherwise find the closest item by position.
|
|
230
|
-
let closestItem = adjacentStackItems[0];
|
|
231
|
-
// Try to find an item with matching data-dx-stack-item value.
|
|
232
|
-
const lastFocusedItem = adjacentStack.querySelector(
|
|
233
|
-
`[data-dx-item-id="${adjacentStack.getAttribute('data-dx-last-focused-item') ?? 'never'}"]`,
|
|
234
|
-
);
|
|
235
|
-
if (lastFocusedItem) {
|
|
236
|
-
closestItem = lastFocusedItem as HTMLElement;
|
|
237
|
-
} else {
|
|
238
|
-
// Fall back to positional calculation
|
|
239
|
-
const ownedItemRect = closestOwnedItem.getBoundingClientRect();
|
|
240
|
-
const targetPosition =
|
|
241
|
-
closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
|
|
242
|
-
|
|
243
|
-
let closestDistance = Infinity;
|
|
244
|
-
|
|
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
|
-
|
|
250
|
-
if (distance < closestDistance) {
|
|
251
|
-
closestDistance = distance;
|
|
252
|
-
closestItem = item;
|
|
253
|
-
}
|
|
254
|
-
if (closestDistance <= PERPENDICULAR_FOCUS_THRESHHOLD) {
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
event.preventDefault();
|
|
261
|
-
scrollIntoViewAndFocus(closestItem, closestStackOrientation);
|
|
262
|
-
} else if (adjacentStackSelfItem) {
|
|
263
|
-
event.preventDefault();
|
|
264
|
-
scrollIntoViewAndFocus(adjacentStackSelfItem, ancestorOrientation);
|
|
265
|
-
}
|
|
266
|
-
} else if (closestOwnedItem) {
|
|
267
|
-
const closestOwnedItemStack = closestOwnedItem.querySelector('[data-dx-stack]');
|
|
268
|
-
const closestOwnedItemStackItems = closestOwnedItemStack
|
|
269
|
-
? (Array.from(
|
|
270
|
-
closestOwnedItemStack.querySelectorAll(
|
|
271
|
-
`[data-dx-stack-item="${closestOwnedItemStack.getAttribute('data-dx-stack')}"]`,
|
|
272
|
-
),
|
|
273
|
-
) as HTMLElement[])
|
|
274
|
-
: [];
|
|
275
|
-
if (closestOwnedItemStackItems.length > 0) {
|
|
276
|
-
event.preventDefault();
|
|
277
|
-
scrollIntoViewAndFocus(
|
|
278
|
-
closestOwnedItemStackItems[
|
|
279
|
-
['ArrowUp', 'ArrowLeft'].includes(event.key) ? closestOwnedItemStackItems.length - 1 : 0
|
|
280
|
-
],
|
|
281
|
-
closestOwnedItemStack?.getAttribute('aria-orientation') as Orientation,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
props.onKeyDown?.(event);
|
|
289
|
-
},
|
|
290
|
-
[props.onKeyDown, stackId, circularFocus],
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
const gridClasses = useMemo(() => {
|
|
294
|
-
if (!rail) {
|
|
295
|
-
return orientation === 'horizontal' ? 'grid-rows-1 pli-[--stack-gap]' : 'grid-cols-1 plb-[--stack-gap]';
|
|
296
|
-
}
|
|
297
|
-
if (orientation === 'horizontal') {
|
|
298
|
-
return railGridHorizontal;
|
|
299
|
-
} else {
|
|
300
|
-
return railGridVertical;
|
|
301
|
-
}
|
|
302
|
-
}, [rail, orientation, size]);
|
|
124
|
+
/** Handles keyboard navigation within the stack. */
|
|
125
|
+
const handleKeyDown = useKeyDown(stackId, circularFocus, onKeyDown);
|
|
303
126
|
|
|
127
|
+
/** Observes DOM mutations to keep scroll separator state in sync. */
|
|
304
128
|
useEffect(() => {
|
|
305
129
|
if (!(stackElement && Number.isFinite(separatorOnScroll))) {
|
|
306
130
|
return;
|
|
@@ -311,35 +135,46 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
311
135
|
});
|
|
312
136
|
|
|
313
137
|
observer.observe(stackElement, { childList: true, subtree: true });
|
|
314
|
-
|
|
315
138
|
return () => {
|
|
316
139
|
observer.disconnect();
|
|
317
140
|
};
|
|
318
141
|
}, [stackElement, handleScroll]);
|
|
319
142
|
|
|
320
143
|
return (
|
|
321
|
-
<StackContext.Provider value={{ orientation, rail, size, onRearrange
|
|
144
|
+
<StackContext.Provider value={{ stackId, orientation, rail, size, onRearrange }}>
|
|
322
145
|
<div
|
|
323
146
|
{...props}
|
|
147
|
+
{...(Number.isFinite(separatorOnScroll) && { onScroll: handleScroll })}
|
|
324
148
|
className={mx(
|
|
325
|
-
'grid
|
|
326
|
-
gridClasses,
|
|
149
|
+
'relative grid [--stack-gap:var(--spacing-trim-xs)]',
|
|
327
150
|
size === 'contain' &&
|
|
328
151
|
(orientation === 'horizontal'
|
|
329
|
-
? 'overflow-x-auto overscroll-x-contain min-
|
|
330
|
-
: 'overflow-y-auto min-
|
|
152
|
+
? 'overflow-x-auto overscroll-x-contain min-h-0 max-h-full h-full'
|
|
153
|
+
: 'overflow-y-auto min-w-0 max-w-full w-full'),
|
|
154
|
+
rail
|
|
155
|
+
? orientation === 'horizontal'
|
|
156
|
+
? railGridHorizontal
|
|
157
|
+
: railGridVertical
|
|
158
|
+
: orientation === 'horizontal'
|
|
159
|
+
? 'grid-rows-1 px-(--stack-gap)'
|
|
160
|
+
: 'grid-cols-1 py-(--stack-gap)',
|
|
331
161
|
classNames,
|
|
332
162
|
)}
|
|
333
|
-
|
|
334
|
-
|
|
163
|
+
style={{
|
|
164
|
+
[orientation === 'horizontal' ? 'gridTemplateColumns' : 'gridTemplateRows']:
|
|
165
|
+
size === 'split'
|
|
166
|
+
? `repeat(${itemsCount}, 1fr)`
|
|
167
|
+
: `repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
168
|
+
...style,
|
|
169
|
+
}}
|
|
170
|
+
aria-orientation={orientation}
|
|
335
171
|
data-dx-stack={stackId}
|
|
336
172
|
data-dx-stack-circular-focus={circularFocus}
|
|
337
173
|
data-dx-last-focused-item={lastFocusedItem}
|
|
338
174
|
data-rail={rail}
|
|
339
|
-
|
|
340
|
-
|
|
175
|
+
onBlur={handleBlur}
|
|
176
|
+
onKeyDown={handleKeyDown}
|
|
341
177
|
ref={composedItemRef}
|
|
342
|
-
{...(Number.isFinite(separatorOnScroll) && { onScroll: handleScroll })}
|
|
343
178
|
>
|
|
344
179
|
{children}
|
|
345
180
|
{selfDroppable && dropping && (
|
|
@@ -359,3 +194,157 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
359
194
|
export { StackContext };
|
|
360
195
|
|
|
361
196
|
export type { StackContextValue };
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Handles moving focus using the arrow keys. Focus is only handled by the nearest stack.
|
|
200
|
+
* If the arrow key matches the orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item;
|
|
201
|
+
* Or if there is no such stack item, focus is passed to the adjacent empty stack if one can be found.
|
|
202
|
+
*/
|
|
203
|
+
// TODO(burdon): Replace with Mosaic.Stack which handles this automatically.
|
|
204
|
+
const useKeyDown = (stackId: string, circularFocus?: boolean, onKeyDown?: StackProps['onKeyDown']) =>
|
|
205
|
+
useCallback(
|
|
206
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
207
|
+
const target = event.target as HTMLElement;
|
|
208
|
+
if (
|
|
209
|
+
event.key.startsWith('Arrow') &&
|
|
210
|
+
!target.closest(
|
|
211
|
+
`input, textarea, [role="textbox"], [data-tabster*="mover"], [data-arrow-keys="all"], [data-arrow-keys~="${event.key.toLowerCase().slice(5)}"]`,
|
|
212
|
+
)
|
|
213
|
+
) {
|
|
214
|
+
const closestOwnedItem = target.closest(`[data-dx-stack-item="${stackId}"]`);
|
|
215
|
+
const closestStack = target.closest('[data-dx-stack]') as HTMLElement | null;
|
|
216
|
+
const closestStackItems = Array.from(closestStack?.querySelectorAll(`[data-dx-stack-item="${stackId}"]`) ?? []);
|
|
217
|
+
const closestStackOrientation = closestStack?.getAttribute('aria-orientation') as Orientation;
|
|
218
|
+
const ancestorStack = closestStack?.parentElement?.closest('[data-dx-stack]') as HTMLElement | null;
|
|
219
|
+
if (closestOwnedItem && closestStack) {
|
|
220
|
+
const ancestorOrientation = ancestorStack?.getAttribute('aria-orientation') as Orientation | undefined;
|
|
221
|
+
const parallelDelta = (
|
|
222
|
+
closestStackOrientation === 'vertical' ? event.key === 'ArrowUp' : event.key === 'ArrowLeft'
|
|
223
|
+
)
|
|
224
|
+
? -1
|
|
225
|
+
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowDown' : event.key === 'ArrowRight')
|
|
226
|
+
? 1
|
|
227
|
+
: 0;
|
|
228
|
+
const perpendicularDelta = (
|
|
229
|
+
closestStackOrientation === 'vertical' ? event.key === 'ArrowLeft' : event.key === 'ArrowUp'
|
|
230
|
+
)
|
|
231
|
+
? -1
|
|
232
|
+
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowRight' : event.key === 'ArrowDown')
|
|
233
|
+
? 1
|
|
234
|
+
: 0;
|
|
235
|
+
if (parallelDelta !== 0) {
|
|
236
|
+
const currentIndex = closestStackItems.indexOf(closestOwnedItem);
|
|
237
|
+
const nextIndex = currentIndex + parallelDelta;
|
|
238
|
+
let adjacentItem: HTMLElement | undefined;
|
|
239
|
+
if (circularFocus) {
|
|
240
|
+
// Circular navigation: wrap around using modulo.
|
|
241
|
+
adjacentItem = closestStackItems[(nextIndex + closestStackItems.length) % closestStackItems.length] as
|
|
242
|
+
| HTMLElement
|
|
243
|
+
| undefined;
|
|
244
|
+
} else {
|
|
245
|
+
// Non-circular navigation: only move if within bounds.
|
|
246
|
+
if (nextIndex >= 0 && nextIndex < closestStackItems.length) {
|
|
247
|
+
adjacentItem = closestStackItems[nextIndex] as HTMLElement | undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (adjacentItem) {
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
scrollIntoViewAndFocus(adjacentItem, closestStackOrientation);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (perpendicularDelta !== 0) {
|
|
258
|
+
if (ancestorStack && ancestorOrientation !== closestStackOrientation) {
|
|
259
|
+
const siblingStacks = Array.from(
|
|
260
|
+
ancestorStack.querySelectorAll(
|
|
261
|
+
`[data-dx-stack-item="${ancestorStack.getAttribute('data-dx-stack')}"] [data-dx-stack]`,
|
|
262
|
+
),
|
|
263
|
+
) as HTMLElement[];
|
|
264
|
+
const currentStackIndex = siblingStacks.indexOf(closestStack);
|
|
265
|
+
const nextStackIndex = currentStackIndex + perpendicularDelta;
|
|
266
|
+
let adjacentStack: HTMLElement | undefined;
|
|
267
|
+
|
|
268
|
+
if (ancestorStack.getAttribute('data-dx-stack-circular-focus') === 'true') {
|
|
269
|
+
// Circular navigation: wrap around using modulo.
|
|
270
|
+
adjacentStack = siblingStacks[(nextStackIndex + siblingStacks.length) % siblingStacks.length] as
|
|
271
|
+
| HTMLElement
|
|
272
|
+
| undefined;
|
|
273
|
+
} else {
|
|
274
|
+
// Non-circular navigation: only move if within bounds.
|
|
275
|
+
if (nextStackIndex >= 0 && nextStackIndex < siblingStacks.length) {
|
|
276
|
+
adjacentStack = siblingStacks[nextStackIndex] as HTMLElement | undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const adjacentStackSelfItem = adjacentStack?.closest(
|
|
280
|
+
`[data-dx-stack-item=${ancestorStack.getAttribute('data-dx-stack')}]`,
|
|
281
|
+
) as HTMLElement | undefined;
|
|
282
|
+
const adjacentStackItems = adjacentStack
|
|
283
|
+
? (Array.from(
|
|
284
|
+
adjacentStack.querySelectorAll(
|
|
285
|
+
`[data-dx-stack-item="${adjacentStack.getAttribute('data-dx-stack')}"]`,
|
|
286
|
+
),
|
|
287
|
+
) as HTMLElement[])
|
|
288
|
+
: [];
|
|
289
|
+
if (adjacentStack && adjacentStackItems.length > 0) {
|
|
290
|
+
// Check if the adjacent stack has a last focused item recorded, otherwise find the closest item by position.
|
|
291
|
+
let closestItem = adjacentStackItems[0];
|
|
292
|
+
// Try to find an item with matching data-dx-stack-item value.
|
|
293
|
+
const lastFocusedItem = adjacentStack.querySelector(
|
|
294
|
+
`[data-dx-item-id="${adjacentStack.getAttribute('data-dx-last-focused-item') ?? 'never'}"]`,
|
|
295
|
+
);
|
|
296
|
+
if (lastFocusedItem) {
|
|
297
|
+
closestItem = lastFocusedItem as HTMLElement;
|
|
298
|
+
} else {
|
|
299
|
+
// Fall back to positional calculation
|
|
300
|
+
const ownedItemRect = closestOwnedItem.getBoundingClientRect();
|
|
301
|
+
const targetPosition =
|
|
302
|
+
closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
|
|
303
|
+
|
|
304
|
+
let closestDistance = Infinity;
|
|
305
|
+
for (const item of adjacentStackItems) {
|
|
306
|
+
const itemRect = item.getBoundingClientRect();
|
|
307
|
+
const itemPosition = closestStackOrientation === 'vertical' ? itemRect.top : itemRect.left;
|
|
308
|
+
const distance = Math.abs(itemPosition - targetPosition);
|
|
309
|
+
if (distance < closestDistance) {
|
|
310
|
+
closestDistance = distance;
|
|
311
|
+
closestItem = item;
|
|
312
|
+
}
|
|
313
|
+
if (closestDistance <= PERPENDICULAR_FOCUS_THRESHHOLD) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
scrollIntoViewAndFocus(closestItem, closestStackOrientation);
|
|
321
|
+
} else if (adjacentStackSelfItem) {
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
scrollIntoViewAndFocus(adjacentStackSelfItem, ancestorOrientation);
|
|
324
|
+
}
|
|
325
|
+
} else if (closestOwnedItem) {
|
|
326
|
+
const closestOwnedItemStack = closestOwnedItem.querySelector('[data-dx-stack]');
|
|
327
|
+
const closestOwnedItemStackItems = closestOwnedItemStack
|
|
328
|
+
? (Array.from(
|
|
329
|
+
closestOwnedItemStack.querySelectorAll(
|
|
330
|
+
`[data-dx-stack-item="${closestOwnedItemStack.getAttribute('data-dx-stack')}"]`,
|
|
331
|
+
),
|
|
332
|
+
) as HTMLElement[])
|
|
333
|
+
: [];
|
|
334
|
+
if (closestOwnedItemStackItems.length > 0) {
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
scrollIntoViewAndFocus(
|
|
337
|
+
closestOwnedItemStackItems[
|
|
338
|
+
['ArrowUp', 'ArrowLeft'].includes(event.key) ? closestOwnedItemStackItems.length - 1 : 0
|
|
339
|
+
],
|
|
340
|
+
closestOwnedItemStack?.getAttribute('aria-orientation') as Orientation,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
onKeyDown?.(event);
|
|
348
|
+
},
|
|
349
|
+
[onKeyDown, stackId, circularFocus],
|
|
350
|
+
);
|
|
@@ -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;
|
|
@@ -5,14 +5,7 @@
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
7
|
export const MenuSignifierHorizontal = () => (
|
|
8
|
-
<svg
|
|
9
|
-
className='absolute block-end-[7px]'
|
|
10
|
-
width={20}
|
|
11
|
-
height={2}
|
|
12
|
-
viewBox='0 0 20 2'
|
|
13
|
-
stroke='currentColor'
|
|
14
|
-
opacity={0.5}
|
|
15
|
-
>
|
|
8
|
+
<svg className='absolute bottom-[7px]' width={20} height={2} viewBox='0 0 20 2' stroke='currentColor' opacity={0.5}>
|
|
16
9
|
<line
|
|
17
10
|
x1={0.5}
|
|
18
11
|
y1={0.75}
|
|
@@ -27,7 +20,7 @@ export const MenuSignifierHorizontal = () => (
|
|
|
27
20
|
);
|
|
28
21
|
|
|
29
22
|
export const MenuSignifierVertical = () => (
|
|
30
|
-
<svg className='absolute
|
|
23
|
+
<svg className='absolute left-1' width={2} height={18} viewBox='0 0 2 18' stroke='currentColor'>
|
|
31
24
|
<line x1={1} y1={3} x2={1} y2={18} strokeWidth={1.5} strokeLinecap='round' strokeDasharray='0 6' />
|
|
32
25
|
</svg>
|
|
33
26
|
);
|
|
@@ -13,19 +13,21 @@ import { StackItem, type StackItemRootProps } from './StackItem';
|
|
|
13
13
|
const DefaultStory = (props: StackItemRootProps) => {
|
|
14
14
|
return (
|
|
15
15
|
<StackItem.Root role='section' {...props} classNames='w-[20rem] border border-separator'>
|
|
16
|
-
<StackItem.Heading>
|
|
16
|
+
<StackItem.Heading classNames='w-full border-b border-separator'>
|
|
17
17
|
<span className='sr-only'>Title</span>
|
|
18
|
-
<div role='none' className='sticky -
|
|
18
|
+
<div role='none' className='sticky -top-px bg-(--sticky-bg) p-1 w-full'>
|
|
19
19
|
<DropdownMenu.Root>
|
|
20
20
|
<DropdownMenu.Trigger asChild>
|
|
21
21
|
<StackItem.SigilButton>
|
|
22
|
-
<Icon icon='ph--dots-three--regular'
|
|
22
|
+
<Icon icon='ph--dots-three--regular' />
|
|
23
23
|
</StackItem.SigilButton>
|
|
24
24
|
</DropdownMenu.Trigger>
|
|
25
25
|
</DropdownMenu.Root>
|
|
26
26
|
</div>
|
|
27
27
|
</StackItem.Heading>
|
|
28
|
-
<StackItem.Content>
|
|
28
|
+
<StackItem.Content>
|
|
29
|
+
<div className='p-4 text-center'>Content</div>
|
|
30
|
+
</StackItem.Content>
|
|
29
31
|
</StackItem.Root>
|
|
30
32
|
);
|
|
31
33
|
};
|
|
@@ -34,7 +36,7 @@ const meta = {
|
|
|
34
36
|
title: 'ui/react-ui-stack/StackItem',
|
|
35
37
|
component: StackItem.Root as any,
|
|
36
38
|
render: DefaultStory,
|
|
37
|
-
decorators: [withTheme],
|
|
39
|
+
decorators: [withTheme()],
|
|
38
40
|
parameters: {
|
|
39
41
|
layout: 'centered',
|
|
40
42
|
},
|