@dxos/react-ui-stack 0.8.4-main.d05673bc65 → 0.8.4-main.dfabb4ec29
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 +129 -174
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +23 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +129 -174
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- 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 +2 -7
- 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/StackItem.d.ts +4 -4
- 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.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.map +1 -1
- 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/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 +8 -8
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -35
- package/src/components/Stack/Stack.stories.tsx +7 -9
- package/src/components/Stack/Stack.tsx +193 -206
- package/src/components/StackContext.tsx +1 -1
- package/src/components/StackItem/StackItem.stories.tsx +2 -2
- package/src/components/StackItem/StackItem.tsx +13 -15
- package/src/components/StackItem/StackItemContent.tsx +0 -1
- package/src/components/StackItem/StackItemHeading.tsx +3 -7
- package/src/components/StackItem/StackItemResizeHandle.tsx +0 -1
- package/src/components/StackItem/StackItemSigil.tsx +2 -2
- package/src/hooks/useStackDropForElements.ts +2 -2
- package/src/index.ts +0 -1
- package/src/translations.ts +8 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-stack",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.dfabb4ec29",
|
|
4
4
|
"description": "A stack component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"author": "DXOS.org",
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"type": "module",
|
|
15
|
+
"imports": {
|
|
16
|
+
"#translations": "./src/translations.ts"
|
|
17
|
+
},
|
|
15
18
|
"exports": {
|
|
16
19
|
".": {
|
|
17
20
|
"types": "./dist/types/src/index.d.ts",
|
|
@@ -22,19 +25,15 @@
|
|
|
22
25
|
"types": "./dist/types/src/playwright/index.d.ts",
|
|
23
26
|
"browser": "./dist/lib/browser/playwright/index.mjs",
|
|
24
27
|
"node": "./dist/lib/node-esm/playwright/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./translations": {
|
|
30
|
+
"source": "./src/translations.ts",
|
|
31
|
+
"types": "./dist/types/src/translations.d.ts",
|
|
32
|
+
"browser": "./dist/lib/browser/translations.mjs",
|
|
33
|
+
"node": "./dist/lib/node-esm/translations.mjs"
|
|
25
34
|
}
|
|
26
35
|
},
|
|
27
36
|
"types": "dist/types/src/index.d.ts",
|
|
28
|
-
"typesVersions": {
|
|
29
|
-
"*": {
|
|
30
|
-
"playwright": [
|
|
31
|
-
"dist/types/src/playwright/index.d.ts"
|
|
32
|
-
],
|
|
33
|
-
"testing": [
|
|
34
|
-
"dist/types/src/testing/index.d.ts"
|
|
35
|
-
]
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
37
|
"files": [
|
|
39
38
|
"dist",
|
|
40
39
|
"src"
|
|
@@ -53,40 +52,40 @@
|
|
|
53
52
|
"@radix-ui/react-slot": "1.1.2",
|
|
54
53
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
55
54
|
"react-resize-detector": "^11.0.1",
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/react-ui-
|
|
59
|
-
"@dxos/react-ui-
|
|
60
|
-
"@dxos/util": "0.8.4-main.
|
|
61
|
-
"@dxos/
|
|
55
|
+
"@dxos/echo": "0.8.4-main.dfabb4ec29",
|
|
56
|
+
"@dxos/keyboard": "0.8.4-main.dfabb4ec29",
|
|
57
|
+
"@dxos/react-ui-attention": "0.8.4-main.dfabb4ec29",
|
|
58
|
+
"@dxos/react-ui-mosaic": "0.8.4-main.dfabb4ec29",
|
|
59
|
+
"@dxos/util": "0.8.4-main.dfabb4ec29",
|
|
60
|
+
"@dxos/react-ui-dnd": "0.8.4-main.dfabb4ec29"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
63
|
"@types/react": "~19.2.7",
|
|
65
64
|
"@types/react-dom": "~19.2.3",
|
|
66
65
|
"react": "~19.2.3",
|
|
67
66
|
"react-dom": "~19.2.3",
|
|
68
|
-
"vite": "^
|
|
69
|
-
"@dxos/app-framework": "0.8.4-main.
|
|
70
|
-
"@dxos/
|
|
71
|
-
"@dxos/
|
|
72
|
-
"@dxos/
|
|
73
|
-
"@dxos/echo-db": "0.8.4-main.
|
|
74
|
-
"@dxos/
|
|
75
|
-
"@dxos/react-client": "0.8.4-main.
|
|
76
|
-
"@dxos/schema": "0.8.4-main.
|
|
77
|
-
"@dxos/
|
|
78
|
-
"@dxos/
|
|
79
|
-
"@dxos/
|
|
80
|
-
"@dxos/
|
|
81
|
-
"@dxos/
|
|
67
|
+
"vite": "^8.0.10",
|
|
68
|
+
"@dxos/app-framework": "0.8.4-main.dfabb4ec29",
|
|
69
|
+
"@dxos/app-graph": "0.8.4-main.dfabb4ec29",
|
|
70
|
+
"@dxos/client": "0.8.4-main.dfabb4ec29",
|
|
71
|
+
"@dxos/echo": "0.8.4-main.dfabb4ec29",
|
|
72
|
+
"@dxos/echo-db": "0.8.4-main.dfabb4ec29",
|
|
73
|
+
"@dxos/random": "0.8.4-main.dfabb4ec29",
|
|
74
|
+
"@dxos/react-client": "0.8.4-main.dfabb4ec29",
|
|
75
|
+
"@dxos/schema": "0.8.4-main.dfabb4ec29",
|
|
76
|
+
"@dxos/storybook-utils": "0.8.4-main.dfabb4ec29",
|
|
77
|
+
"@dxos/react-ui": "0.8.4-main.dfabb4ec29",
|
|
78
|
+
"@dxos/types": "0.8.4-main.dfabb4ec29",
|
|
79
|
+
"@dxos/test-utils": "0.8.4-main.dfabb4ec29",
|
|
80
|
+
"@dxos/ui-theme": "0.8.4-main.dfabb4ec29"
|
|
82
81
|
},
|
|
83
82
|
"peerDependencies": {
|
|
84
83
|
"react": "~19.2.3",
|
|
85
84
|
"react-dom": "~19.2.3",
|
|
86
|
-
"@dxos/client": "0.8.4-main.
|
|
87
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
88
|
-
"@dxos/
|
|
89
|
-
"@dxos/
|
|
85
|
+
"@dxos/client": "0.8.4-main.dfabb4ec29",
|
|
86
|
+
"@dxos/react-ui": "0.8.4-main.dfabb4ec29",
|
|
87
|
+
"@dxos/ui-theme": "0.8.4-main.dfabb4ec29",
|
|
88
|
+
"@dxos/random": "0.8.4-main.dfabb4ec29"
|
|
90
89
|
},
|
|
91
90
|
"publishConfig": {
|
|
92
91
|
"access": "public"
|
|
@@ -6,12 +6,11 @@ import { type Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge
|
|
|
6
6
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
7
|
import React, { useCallback, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { random } from '@dxos/random';
|
|
10
10
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
11
|
|
|
12
12
|
import { StackItem } from '../StackItem';
|
|
13
13
|
import { type StackItemData } from '../types';
|
|
14
|
-
|
|
15
14
|
import { Stack } from './Stack';
|
|
16
15
|
|
|
17
16
|
type StoryStackItem = {
|
|
@@ -30,16 +29,16 @@ const KanbanBlock = ({ item }: { item: StoryStackItem }) => {
|
|
|
30
29
|
|
|
31
30
|
const DefaultStory = () => {
|
|
32
31
|
const [columns, setColumns] = useState<StoryStackItem[]>(
|
|
33
|
-
|
|
32
|
+
random.helpers.multiple(
|
|
34
33
|
() =>
|
|
35
34
|
({
|
|
36
|
-
id:
|
|
37
|
-
title:
|
|
38
|
-
items:
|
|
35
|
+
id: random.string.uuid(),
|
|
36
|
+
title: random.lorem.paragraph(),
|
|
37
|
+
items: random.helpers.multiple(
|
|
39
38
|
() =>
|
|
40
39
|
({
|
|
41
|
-
id:
|
|
42
|
-
title:
|
|
40
|
+
id: random.string.uuid(),
|
|
41
|
+
title: random.lorem.paragraph(),
|
|
43
42
|
}) satisfies StoryStackItem,
|
|
44
43
|
{ count: { min: 32, max: 64 } },
|
|
45
44
|
),
|
|
@@ -77,7 +76,6 @@ const DefaultStory = () => {
|
|
|
77
76
|
targetColumn.items
|
|
78
77
|
) {
|
|
79
78
|
const [movedCard] = sourceColumn.items.splice(sourceCardIndex, 1);
|
|
80
|
-
|
|
81
79
|
let insertIndex;
|
|
82
80
|
if (sourceColumn === targetColumn && sourceCardIndex < targetCardIndex) {
|
|
83
81
|
insertIndex = closestEdge === 'bottom' ? targetCardIndex : targetCardIndex - 1;
|
|
@@ -4,7 +4,6 @@
|
|
|
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,
|
|
10
9
|
type FocusEvent,
|
|
@@ -12,7 +11,6 @@ import React, {
|
|
|
12
11
|
forwardRef,
|
|
13
12
|
useCallback,
|
|
14
13
|
useEffect,
|
|
15
|
-
useMemo,
|
|
16
14
|
useState,
|
|
17
15
|
} from 'react';
|
|
18
16
|
|
|
@@ -37,14 +35,6 @@ export type Size = 'intrinsic' | 'contain' | 'split';
|
|
|
37
35
|
export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--dx-rail-size)_[content-start]_1fr_[content-end]]';
|
|
38
36
|
export const railGridVertical = 'grid-cols-[[rail-start]_var(--dx-rail-size)_[content-start]_1fr_[content-end]]';
|
|
39
37
|
|
|
40
|
-
// TODO(ZaymonFC): Magic 2px to stop overflow.
|
|
41
|
-
export const railGridHorizontalContainFitContent =
|
|
42
|
-
'grid-rows-[[rail-start]_var(--dx-rail-size)_[content-start]_fit-content(calc(100%-var(--dx-rail-size)*2+2px))_[content-end]]';
|
|
43
|
-
export const railGridVerticalContainFitContent =
|
|
44
|
-
'grid-cols-[[rail-start]_var(--dx-rail-size)_[content-start]_fit-content(calc(100%-var(--dx-rail-size)*2+2px))_[content-end]]';
|
|
45
|
-
|
|
46
|
-
export const autoScrollRootAttributes = { 'data-drag-autoscroll': 'idle' };
|
|
47
|
-
|
|
48
38
|
const PERPENDICULAR_FOCUS_THRESHHOLD = 128;
|
|
49
39
|
|
|
50
40
|
const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orientation']) => {
|
|
@@ -58,9 +48,9 @@ const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orient
|
|
|
58
48
|
export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
|
|
59
49
|
Partial<StackContextValue> & {
|
|
60
50
|
itemsCount?: number;
|
|
61
|
-
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
62
|
-
separatorOnScroll?: number;
|
|
63
51
|
circularFocus?: boolean;
|
|
52
|
+
separatorOnScroll?: number;
|
|
53
|
+
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
64
54
|
};
|
|
65
55
|
|
|
66
56
|
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
@@ -68,34 +58,30 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
68
58
|
{
|
|
69
59
|
children,
|
|
70
60
|
classNames,
|
|
61
|
+
id,
|
|
71
62
|
style,
|
|
72
63
|
orientation = 'vertical',
|
|
73
|
-
rail = true,
|
|
64
|
+
rail = true,
|
|
74
65
|
size = 'intrinsic',
|
|
75
|
-
onRearrange,
|
|
76
66
|
itemsCount = Children.count(children),
|
|
77
|
-
getDropElement,
|
|
78
|
-
separatorOnScroll,
|
|
79
67
|
circularFocus,
|
|
68
|
+
separatorOnScroll,
|
|
69
|
+
getDropElement,
|
|
70
|
+
onBlur,
|
|
71
|
+
onKeyDown,
|
|
72
|
+
onRearrange,
|
|
80
73
|
...props
|
|
81
74
|
},
|
|
82
75
|
forwardedRef,
|
|
83
76
|
) => {
|
|
84
|
-
const stackId = useId('stack',
|
|
77
|
+
const stackId = useId('stack', id);
|
|
85
78
|
const [stackElement, stackRef] = useState<HTMLDivElement | null>(null);
|
|
86
79
|
const [lastFocusedItem, setLastFocusedItem] = useState<string>();
|
|
87
80
|
const composedItemRef = composeRefs<HTMLDivElement>(stackRef, forwardedRef);
|
|
88
81
|
|
|
89
|
-
const
|
|
90
|
-
[orientation === 'horizontal' ? 'gridTemplateColumns' : 'gridTemplateRows']:
|
|
91
|
-
size === 'split' ? `repeat(${itemsCount}, 1fr)` : `repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
92
|
-
...style,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const selfDroppable = !!(itemsCount < 1 && onRearrange && props.id);
|
|
96
|
-
|
|
82
|
+
const selfDroppable = !!(itemsCount < 1 && onRearrange && id);
|
|
97
83
|
const { dropping } = useStackDropForElements({
|
|
98
|
-
id
|
|
84
|
+
id,
|
|
99
85
|
element: getDropElement && stackElement ? getDropElement(stackElement) : stackElement,
|
|
100
86
|
scrollElement: stackElement,
|
|
101
87
|
selfDroppable,
|
|
@@ -103,6 +89,7 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
103
89
|
onRearrange,
|
|
104
90
|
});
|
|
105
91
|
|
|
92
|
+
/** Updates scroll separator data attributes based on current scroll position. */
|
|
106
93
|
const handleScroll = useCallback(() => {
|
|
107
94
|
if (stackElement && Number.isFinite(separatorOnScroll)) {
|
|
108
95
|
const scrollPosition = orientation === 'horizontal' ? stackElement.scrollLeft : stackElement.scrollTop;
|
|
@@ -119,9 +106,7 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
119
106
|
}
|
|
120
107
|
}, [stackElement, separatorOnScroll, orientation]);
|
|
121
108
|
|
|
122
|
-
/**
|
|
123
|
-
* Handles blur events to track the last focused item within this stack.
|
|
124
|
-
*/
|
|
109
|
+
/** Handles blur events to track the last focused item within this stack. */
|
|
125
110
|
const handleBlur = useCallback(
|
|
126
111
|
(event: FocusEvent<HTMLDivElement>) => {
|
|
127
112
|
if (event.target) {
|
|
@@ -131,178 +116,15 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
131
116
|
setLastFocusedItem(closestStackItem?.getAttribute('data-dx-item-id') ?? undefined);
|
|
132
117
|
}
|
|
133
118
|
}
|
|
134
|
-
|
|
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);
|
|
119
|
+
onBlur?.(event);
|
|
290
120
|
},
|
|
291
|
-
[
|
|
121
|
+
[stackId, onBlur],
|
|
292
122
|
);
|
|
293
123
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return orientation === 'horizontal' ? 'grid-rows-1 px-(--stack-gap)' : 'grid-cols-1 py-(--stack-gap)';
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (orientation === 'horizontal') {
|
|
300
|
-
return railGridHorizontal;
|
|
301
|
-
} else {
|
|
302
|
-
return railGridVertical;
|
|
303
|
-
}
|
|
304
|
-
}, [rail, orientation, size]);
|
|
124
|
+
/** Handles keyboard navigation within the stack. */
|
|
125
|
+
const handleKeyDown = useKeyDown(stackId, circularFocus, onKeyDown);
|
|
305
126
|
|
|
127
|
+
/** Observes DOM mutations to keep scroll separator state in sync. */
|
|
306
128
|
useEffect(() => {
|
|
307
129
|
if (!(stackElement && Number.isFinite(separatorOnScroll))) {
|
|
308
130
|
return;
|
|
@@ -313,35 +135,46 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
313
135
|
});
|
|
314
136
|
|
|
315
137
|
observer.observe(stackElement, { childList: true, subtree: true });
|
|
316
|
-
|
|
317
138
|
return () => {
|
|
318
139
|
observer.disconnect();
|
|
319
140
|
};
|
|
320
141
|
}, [stackElement, handleScroll]);
|
|
321
142
|
|
|
322
143
|
return (
|
|
323
|
-
<StackContext.Provider value={{ orientation, rail, size, onRearrange
|
|
144
|
+
<StackContext.Provider value={{ stackId, orientation, rail, size, onRearrange }}>
|
|
324
145
|
<div
|
|
325
146
|
{...props}
|
|
147
|
+
{...(Number.isFinite(separatorOnScroll) && { onScroll: handleScroll })}
|
|
326
148
|
className={mx(
|
|
327
|
-
'grid
|
|
328
|
-
gridClasses,
|
|
149
|
+
'relative grid [--stack-gap:var(--spacing-trim-xs)]',
|
|
329
150
|
size === 'contain' &&
|
|
330
151
|
(orientation === 'horizontal'
|
|
331
152
|
? 'overflow-x-auto overscroll-x-contain min-h-0 max-h-full h-full'
|
|
332
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)',
|
|
333
161
|
classNames,
|
|
334
162
|
)}
|
|
335
|
-
|
|
336
|
-
|
|
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}
|
|
337
171
|
data-dx-stack={stackId}
|
|
338
172
|
data-dx-stack-circular-focus={circularFocus}
|
|
339
173
|
data-dx-last-focused-item={lastFocusedItem}
|
|
340
174
|
data-rail={rail}
|
|
341
|
-
|
|
342
|
-
|
|
175
|
+
onBlur={handleBlur}
|
|
176
|
+
onKeyDown={handleKeyDown}
|
|
343
177
|
ref={composedItemRef}
|
|
344
|
-
{...(Number.isFinite(separatorOnScroll) && { onScroll: handleScroll })}
|
|
345
178
|
>
|
|
346
179
|
{children}
|
|
347
180
|
{selfDroppable && dropping && (
|
|
@@ -361,3 +194,157 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
361
194
|
export { StackContext };
|
|
362
195
|
|
|
363
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
|
+
);
|