@dxos/react-ui-stack 0.8.4-main.c1de068 → 0.8.4-main.e098934
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{chunk-P3TQV4BA.mjs → chunk-3V2YUQK5.mjs} +346 -169
- package/dist/lib/browser/chunk-3V2YUQK5.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +5 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/playwright/index.mjs +23 -6
- package/dist/lib/browser/playwright/index.mjs.map +2 -2
- package/dist/lib/browser/testing/index.mjs +3 -3
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/{chunk-3WVEPAJ4.mjs → chunk-HE3BRF7A.mjs} +346 -169
- package/dist/lib/node-esm/chunk-HE3BRF7A.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +5 -1
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/playwright/index.mjs +23 -6
- package/dist/lib/node-esm/playwright/index.mjs.map +2 -2
- package/dist/lib/node-esm/testing/index.mjs +3 -3
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/types/src/components/Image/Image.d.ts +11 -0
- package/dist/types/src/components/Image/Image.d.ts.map +1 -0
- package/dist/types/src/components/Image/Image.stories.d.ts +31 -0
- package/dist/types/src/components/Image/Image.stories.d.ts.map +1 -0
- package/dist/types/src/components/Image/index.d.ts +2 -0
- package/dist/types/src/components/Image/index.d.ts.map +1 -0
- package/dist/types/src/components/Stack/Stack.d.ts +9 -2
- package/dist/types/src/components/Stack/Stack.d.ts.map +1 -1
- package/dist/types/src/components/Stack/Stack.stories.d.ts +12 -2
- 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 +3 -3
- package/dist/types/src/components/StackItem/StackItem.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItem.stories.d.ts +13 -4
- 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/StackItemHeading.d.ts +1 -1
- package/dist/types/src/components/StackItem/StackItemHeading.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemResizeHandle.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItemSigil.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/exemplars/Card/Card.d.ts +2 -2
- package/dist/types/src/exemplars/Card/Card.d.ts.map +1 -1
- package/dist/types/src/exemplars/Card/Card.stories.d.ts +34 -3
- package/dist/types/src/exemplars/Card/Card.stories.d.ts.map +1 -1
- package/dist/types/src/exemplars/Card/fragments.d.ts +2 -1
- package/dist/types/src/exemplars/Card/fragments.d.ts.map +1 -1
- package/dist/types/src/exemplars/CardStack/CardStack.d.ts.map +1 -1
- package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts +6 -2
- package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useStackDropForElements.d.ts +1 -1
- package/dist/types/src/hooks/useStackDropForElements.d.ts.map +1 -1
- package/dist/types/src/testing/CardContainer.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -21
- package/src/components/Image/Image.stories.tsx +58 -0
- package/src/components/Image/Image.tsx +137 -0
- package/src/components/Image/index.ts +5 -0
- package/src/components/Stack/Stack.stories.tsx +6 -5
- package/src/components/Stack/Stack.tsx +160 -18
- package/src/components/StackContext.tsx +2 -1
- package/src/components/StackItem/StackItem.stories.tsx +14 -10
- package/src/components/StackItem/StackItem.tsx +15 -14
- package/src/components/StackItem/StackItemContent.tsx +1 -0
- package/src/components/StackItem/StackItemHeading.tsx +3 -3
- package/src/components/StackItem/StackItemResizeHandle.tsx +2 -1
- package/src/components/StackItem/StackItemSigil.tsx +2 -1
- package/src/components/index.ts +1 -0
- package/src/exemplars/Card/Card.stories.tsx +33 -23
- package/src/exemplars/Card/Card.tsx +11 -11
- package/src/exemplars/Card/fragments.ts +2 -1
- package/src/exemplars/CardStack/CardStack.stories.tsx +6 -6
- package/src/exemplars/CardStack/CardStack.tsx +1 -1
- package/src/hooks/useStackDropForElements.ts +1 -1
- package/src/testing/CardContainer.tsx +9 -6
- package/dist/lib/browser/chunk-P3TQV4BA.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-3WVEPAJ4.mjs.map +0 -7
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.e098934",
|
|
4
4
|
"description": "A stack component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -54,36 +54,36 @@
|
|
|
54
54
|
"@radix-ui/react-slot": "1.1.2",
|
|
55
55
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
56
56
|
"react-resize-detector": "^11.0.1",
|
|
57
|
-
"@dxos/echo-schema": "0.8.4-main.
|
|
58
|
-
"@dxos/keyboard": "0.8.4-main.
|
|
59
|
-
"@dxos/live-object": "0.8.4-main.
|
|
60
|
-
"@dxos/
|
|
61
|
-
"@dxos/react-ui-
|
|
62
|
-
"@dxos/
|
|
63
|
-
"@dxos/
|
|
57
|
+
"@dxos/echo-schema": "0.8.4-main.e098934",
|
|
58
|
+
"@dxos/keyboard": "0.8.4-main.e098934",
|
|
59
|
+
"@dxos/live-object": "0.8.4-main.e098934",
|
|
60
|
+
"@dxos/storybook-utils": "0.8.4-main.e098934",
|
|
61
|
+
"@dxos/react-ui-attention": "0.8.4-main.e098934",
|
|
62
|
+
"@dxos/util": "0.8.4-main.e098934",
|
|
63
|
+
"@dxos/react-ui-dnd": "0.8.4-main.e098934"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/react": "~18.2.0",
|
|
67
67
|
"@types/react-dom": "~18.2.0",
|
|
68
68
|
"react": "~18.2.0",
|
|
69
69
|
"react-dom": "~18.2.0",
|
|
70
|
-
"vite": "
|
|
71
|
-
"@dxos/app-graph": "0.8.4-main.
|
|
72
|
-
"@dxos/
|
|
73
|
-
"@dxos/
|
|
74
|
-
"@dxos/random": "0.8.4-main.
|
|
75
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
76
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
77
|
-
"@dxos/
|
|
78
|
-
"@dxos/
|
|
70
|
+
"vite": "7.1.1",
|
|
71
|
+
"@dxos/app-graph": "0.8.4-main.e098934",
|
|
72
|
+
"@dxos/client": "0.8.4-main.e098934",
|
|
73
|
+
"@dxos/echo-schema": "0.8.4-main.e098934",
|
|
74
|
+
"@dxos/random": "0.8.4-main.e098934",
|
|
75
|
+
"@dxos/react-ui": "0.8.4-main.e098934",
|
|
76
|
+
"@dxos/react-ui-theme": "0.8.4-main.e098934",
|
|
77
|
+
"@dxos/storybook-utils": "0.8.4-main.e098934",
|
|
78
|
+
"@dxos/test-utils": "0.8.4-main.e098934"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"react": "~18.2.0",
|
|
82
82
|
"react-dom": "~18.2.0",
|
|
83
|
-
"@dxos/client": "0.8.4-main.
|
|
84
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
85
|
-
"@dxos/random": "0.8.4-main.
|
|
86
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
83
|
+
"@dxos/client": "0.8.4-main.e098934",
|
|
84
|
+
"@dxos/react-ui": "0.8.4-main.e098934",
|
|
85
|
+
"@dxos/random": "0.8.4-main.e098934",
|
|
86
|
+
"@dxos/react-ui-theme": "0.8.4-main.e098934"
|
|
87
87
|
},
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos-theme';
|
|
6
|
+
|
|
7
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
import { faker } from '@dxos/random';
|
|
11
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
12
|
+
|
|
13
|
+
import { Image } from './Image';
|
|
14
|
+
|
|
15
|
+
faker.seed(1);
|
|
16
|
+
|
|
17
|
+
const meta = {
|
|
18
|
+
title: 'ui/react-ui-stack/Image',
|
|
19
|
+
component: Image,
|
|
20
|
+
render: (args) => (
|
|
21
|
+
<div className='absolute inset-0 flex place-items-center'>
|
|
22
|
+
<Image {...args} />
|
|
23
|
+
</div>
|
|
24
|
+
),
|
|
25
|
+
decorators: [withTheme],
|
|
26
|
+
parameters: {
|
|
27
|
+
layout: 'fullscreen',
|
|
28
|
+
},
|
|
29
|
+
} satisfies Meta<typeof Image>;
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
|
|
33
|
+
type Story = StoryObj<typeof meta>;
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
src: faker.image.url(),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Access to image at 'https://dxos.network/dxos-logotype-blue.png'
|
|
43
|
+
* from origin 'http://localhost:9009' has been blocked by CORS policy:
|
|
44
|
+
* No 'Access-Control-Allow-Origin' header is present on the requested resource.
|
|
45
|
+
*/
|
|
46
|
+
export const Cors: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
src: 'https://dxos.network/dxos-logotype-blue.png',
|
|
49
|
+
classNames: 'w-[20rem]',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const SVG: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
src: 'https://dxos.network/bg-kube.svg',
|
|
56
|
+
classNames: 'w-[20rem]',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type SyntheticEvent, useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
9
|
+
|
|
10
|
+
export type ImageProps = ThemedClassName<{
|
|
11
|
+
src: string;
|
|
12
|
+
alt?: string;
|
|
13
|
+
crossOrigin?: 'anonymous' | 'use-credentials' | '';
|
|
14
|
+
sampleSize?: number;
|
|
15
|
+
contrast?: number;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export const Image = ({
|
|
19
|
+
classNames,
|
|
20
|
+
src,
|
|
21
|
+
alt = '',
|
|
22
|
+
crossOrigin = 'anonymous',
|
|
23
|
+
sampleSize = 64,
|
|
24
|
+
contrast = 0.95,
|
|
25
|
+
}: ImageProps) => {
|
|
26
|
+
const [crossOriginState, setCrossOriginState] = useState<ImageProps['crossOrigin']>(crossOrigin);
|
|
27
|
+
const [dominantColor, setDominantColor] = useState<string | undefined>(undefined);
|
|
28
|
+
const [imageLoaded, setImageLoaded] = useState<boolean>(false);
|
|
29
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
30
|
+
|
|
31
|
+
// TODO(burdon): Cache?
|
|
32
|
+
const extractDominantColor = (img: HTMLImageElement): void => {
|
|
33
|
+
const canvas = canvasRef.current;
|
|
34
|
+
const ctx = canvas?.getContext('2d');
|
|
35
|
+
if (!canvas || !ctx) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Draw the image scaled down.
|
|
40
|
+
canvas.width = sampleSize;
|
|
41
|
+
canvas.height = sampleSize;
|
|
42
|
+
ctx.drawImage(img, 0, 0, sampleSize, sampleSize);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Get image data.
|
|
46
|
+
const imageData = ctx.getImageData(0, 0, sampleSize, sampleSize);
|
|
47
|
+
const pixels = imageData.data;
|
|
48
|
+
|
|
49
|
+
// Calculate average color with more weight to vibrant colors.
|
|
50
|
+
let r = 0;
|
|
51
|
+
let g = 0;
|
|
52
|
+
let b = 0;
|
|
53
|
+
let totalWeight = 0;
|
|
54
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
55
|
+
const red = pixels[i];
|
|
56
|
+
const green = pixels[i + 1];
|
|
57
|
+
const blue = pixels[i + 2];
|
|
58
|
+
const alpha = pixels[i + 3];
|
|
59
|
+
|
|
60
|
+
// Skip transparent pixels.
|
|
61
|
+
if (alpha === 0) continue;
|
|
62
|
+
|
|
63
|
+
// Calculate saturation to weight vibrant colors more.
|
|
64
|
+
const max = Math.max(red, green, blue);
|
|
65
|
+
const min = Math.min(red, green, blue);
|
|
66
|
+
const saturation = max === 0 ? 0 : (max - min) / max;
|
|
67
|
+
const weight = 1 + saturation * 2; // Give more weight to saturated colors.
|
|
68
|
+
|
|
69
|
+
r += red * weight;
|
|
70
|
+
g += green * weight;
|
|
71
|
+
b += blue * weight;
|
|
72
|
+
totalWeight += weight;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (totalWeight > 0) {
|
|
76
|
+
r = Math.round(r / totalWeight);
|
|
77
|
+
g = Math.round(g / totalWeight);
|
|
78
|
+
b = Math.round(b / totalWeight);
|
|
79
|
+
|
|
80
|
+
// Slightly darken the color for better contrast.
|
|
81
|
+
r = Math.round(r * contrast);
|
|
82
|
+
g = Math.round(g * contrast);
|
|
83
|
+
b = Math.round(b * contrast);
|
|
84
|
+
setDominantColor(`rgb(${r}, ${g}, ${b})`);
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
setCrossOriginState(undefined);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// CORS not supported by server.
|
|
92
|
+
const handleImageError = (): void => {
|
|
93
|
+
setCrossOriginState(undefined);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleImageLoad = (ev: SyntheticEvent<HTMLImageElement>): void => {
|
|
97
|
+
const img = ev.target as HTMLImageElement;
|
|
98
|
+
extractDominantColor(img);
|
|
99
|
+
setImageLoaded(true);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
className={mx(`relative flex is-full justify-center overflow-hidden transition-all duration-700`, classNames)}
|
|
105
|
+
style={{
|
|
106
|
+
backgroundColor: dominantColor,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{/* Hidden canvas for color extraction. */}
|
|
110
|
+
<canvas ref={canvasRef} style={{ display: 'none' }} aria-hidden='true' />
|
|
111
|
+
|
|
112
|
+
{/* Background gradient overlay for smooth transition. */}
|
|
113
|
+
<div
|
|
114
|
+
className='absolute inset-0 pointer-events-none'
|
|
115
|
+
style={{
|
|
116
|
+
background: dominantColor
|
|
117
|
+
? `radial-gradient(circle at center, transparent 30%, ${dominantColor} 100%)`
|
|
118
|
+
: undefined,
|
|
119
|
+
transition: 'opacity 0.7s ease-in-out',
|
|
120
|
+
opacity: 0.5,
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<img
|
|
125
|
+
src={src}
|
|
126
|
+
alt={alt}
|
|
127
|
+
crossOrigin={crossOriginState}
|
|
128
|
+
onError={handleImageError}
|
|
129
|
+
onLoad={handleImageLoad}
|
|
130
|
+
className={mx('z-10 object-contain transition-opacity duration-500', classNames)}
|
|
131
|
+
style={{
|
|
132
|
+
opacity: imageLoaded ? 1 : 0,
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
@@ -6,14 +6,15 @@ import '@dxos-theme';
|
|
|
6
6
|
|
|
7
7
|
import { type Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
8
8
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
9
|
-
import React, {
|
|
9
|
+
import React, { useCallback, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
import { faker } from '@dxos/random';
|
|
12
12
|
import { withTheme } from '@dxos/storybook-utils';
|
|
13
13
|
|
|
14
|
-
import { Stack } from './Stack';
|
|
15
|
-
import { StackItem } from '../StackItem';
|
|
16
14
|
import { type StackItemData } from '../defs';
|
|
15
|
+
import { StackItem } from '../StackItem';
|
|
16
|
+
|
|
17
|
+
import { Stack } from './Stack';
|
|
17
18
|
|
|
18
19
|
type StoryStackItem = {
|
|
19
20
|
id: string;
|
|
@@ -129,12 +130,12 @@ const DefaultStory = () => {
|
|
|
129
130
|
);
|
|
130
131
|
};
|
|
131
132
|
|
|
132
|
-
const meta
|
|
133
|
+
const meta = {
|
|
133
134
|
title: 'ui/react-ui-stack/Stack',
|
|
134
135
|
component: DefaultStory,
|
|
135
136
|
decorators: [withTheme],
|
|
136
137
|
argTypes: { orientation: { control: 'radio', options: ['horizontal', 'vertical'] } },
|
|
137
|
-
}
|
|
138
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
138
139
|
|
|
139
140
|
export default meta;
|
|
140
141
|
|
|
@@ -2,28 +2,35 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
|
|
6
5
|
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
7
6
|
import React, {
|
|
8
|
-
Children,
|
|
9
7
|
type CSSProperties,
|
|
8
|
+
Children,
|
|
10
9
|
type ComponentPropsWithRef,
|
|
10
|
+
type KeyboardEvent,
|
|
11
11
|
forwardRef,
|
|
12
|
-
useState,
|
|
13
|
-
useMemo,
|
|
14
12
|
useCallback,
|
|
15
13
|
useEffect,
|
|
14
|
+
useMemo,
|
|
15
|
+
useState,
|
|
16
16
|
} from 'react';
|
|
17
17
|
|
|
18
|
-
import { type ThemedClassName,
|
|
18
|
+
import { ListItem, type ThemedClassName, useId } from '@dxos/react-ui';
|
|
19
19
|
import { mx } from '@dxos/react-ui-theme';
|
|
20
20
|
|
|
21
21
|
import { useStackDropForElements } from '../../hooks';
|
|
22
|
-
import { StackContext } from '../StackContext';
|
|
23
22
|
import { type StackContextValue } from '../defs';
|
|
23
|
+
import { StackContext } from '../StackContext';
|
|
24
24
|
|
|
25
25
|
export type Orientation = 'horizontal' | 'vertical';
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Size is how Stack and its StackItems coordinate the dimensions of the items with the available space.
|
|
28
|
+
* - `intrinsic` signals to Stack and its StackItems to occupy their intrinsic size
|
|
29
|
+
* - Any other size will extrinsically fill the available space along the axis of its orientation and handle overflow:
|
|
30
|
+
* - `contain` causes StackItems to occupy their intrinsic size
|
|
31
|
+
* - `split` divides the Stack’s available space among the StackItems
|
|
32
|
+
*/
|
|
33
|
+
export type Size = 'intrinsic' | 'contain' | 'split';
|
|
27
34
|
|
|
28
35
|
export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
|
|
29
36
|
Partial<StackContextValue> & {
|
|
@@ -35,7 +42,7 @@ export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'ar
|
|
|
35
42
|
export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
36
43
|
export const railGridVertical = 'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
37
44
|
|
|
38
|
-
// TODO(ZaymonFC): Magic 2px to stop overflow
|
|
45
|
+
// TODO(ZaymonFC): Magic 2px to stop overflow.
|
|
39
46
|
export const railGridHorizontalContainFitContent =
|
|
40
47
|
'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_fit-content(calc(100%-var(--rail-size)*2+2px))_[content-end]]';
|
|
41
48
|
export const railGridVerticalContainFitContent =
|
|
@@ -43,6 +50,16 @@ export const railGridVerticalContainFitContent =
|
|
|
43
50
|
|
|
44
51
|
export const autoScrollRootAttributes = { 'data-drag-autoscroll': 'idle' };
|
|
45
52
|
|
|
53
|
+
const PERPENDICULAR_FOCUS_THRESHHOLD = 128;
|
|
54
|
+
|
|
55
|
+
const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orientation']) => {
|
|
56
|
+
el.scrollIntoView({
|
|
57
|
+
behavior: 'instant',
|
|
58
|
+
[orientation === 'vertical' ? 'block' : 'inline']: 'center',
|
|
59
|
+
});
|
|
60
|
+
return el.focus();
|
|
61
|
+
};
|
|
62
|
+
|
|
46
63
|
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
47
64
|
(
|
|
48
65
|
{
|
|
@@ -60,13 +77,13 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
60
77
|
},
|
|
61
78
|
forwardedRef,
|
|
62
79
|
) => {
|
|
80
|
+
const stackId = useId('stack', props.id);
|
|
63
81
|
const [stackElement, stackRef] = useState<HTMLDivElement | null>(null);
|
|
64
82
|
const composedItemRef = composeRefs<HTMLDivElement>(stackRef, forwardedRef);
|
|
65
|
-
const arrowNavigationAttrs = useArrowNavigationGroup({ axis: orientation });
|
|
66
83
|
|
|
67
84
|
const styles: CSSProperties = {
|
|
68
85
|
[orientation === 'horizontal' ? 'gridTemplateColumns' : 'gridTemplateRows']:
|
|
69
|
-
`repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
86
|
+
size === 'split' ? `repeat(${itemsCount}, 1fr)` : `repeat(${itemsCount}, min-content) [tabster-dummies] 0`,
|
|
70
87
|
...style,
|
|
71
88
|
};
|
|
72
89
|
|
|
@@ -97,14 +114,138 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
97
114
|
}
|
|
98
115
|
}, [stackElement, separatorOnScroll, orientation]);
|
|
99
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Handles moving focus using the arrow keys. Focus is only handled by the nearest stack; if the arrow key matches the
|
|
119
|
+
* orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item; or, if there is no
|
|
120
|
+
* such stack item, focus is passed to the adjacent empty stack if one can be found.
|
|
121
|
+
*/
|
|
122
|
+
const handleKeyDown = useCallback(
|
|
123
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
124
|
+
const target = event.target as HTMLElement;
|
|
125
|
+
if (
|
|
126
|
+
event.key.startsWith('Arrow') &&
|
|
127
|
+
!target.closest(
|
|
128
|
+
`input, textarea, [role="textbox"], [data-tabster*="mover"], [data-arrow-keys="all"], [data-arrow-keys~="${event.key.toLowerCase().slice(5)}"]`,
|
|
129
|
+
)
|
|
130
|
+
) {
|
|
131
|
+
const closestOwnedItem = target.closest(`[data-dx-stack-item="${stackId}"]`);
|
|
132
|
+
const closestStack = target.closest('[data-dx-stack]') as HTMLElement | null;
|
|
133
|
+
const closestStackItems = Array.from(
|
|
134
|
+
closestStack?.querySelectorAll(`[data-dx-stack-item="${stackId}"]`) ?? [],
|
|
135
|
+
);
|
|
136
|
+
const closestStackOrientation = closestStack?.getAttribute('aria-orientation') as Orientation;
|
|
137
|
+
const ancestorStack = closestStack?.parentElement?.closest('[data-dx-stack]') as HTMLElement | null;
|
|
138
|
+
if (closestOwnedItem && closestStack) {
|
|
139
|
+
const ancestorOrientation = ancestorStack?.getAttribute('aria-orientation') as Orientation | undefined;
|
|
140
|
+
const parallelDelta = (
|
|
141
|
+
closestStackOrientation === 'vertical' ? event.key === 'ArrowUp' : event.key === 'ArrowLeft'
|
|
142
|
+
)
|
|
143
|
+
? -1
|
|
144
|
+
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowDown' : event.key === 'ArrowRight')
|
|
145
|
+
? 1
|
|
146
|
+
: 0;
|
|
147
|
+
const perpendicularDelta = (
|
|
148
|
+
closestStackOrientation === 'vertical' ? event.key === 'ArrowLeft' : event.key === 'ArrowUp'
|
|
149
|
+
)
|
|
150
|
+
? -1
|
|
151
|
+
: (closestStackOrientation === 'vertical' ? event.key === 'ArrowRight' : event.key === 'ArrowDown')
|
|
152
|
+
? 1
|
|
153
|
+
: 0;
|
|
154
|
+
if (parallelDelta !== 0) {
|
|
155
|
+
const adjacentItem = closestStackItems[
|
|
156
|
+
(closestStackItems.indexOf(closestOwnedItem) + parallelDelta + closestStackItems.length) %
|
|
157
|
+
closestStackItems.length
|
|
158
|
+
] as HTMLElement | undefined;
|
|
159
|
+
if (adjacentItem) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
scrollIntoViewAndFocus(adjacentItem, closestStackOrientation);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (perpendicularDelta !== 0) {
|
|
165
|
+
if (ancestorStack && ancestorOrientation !== closestStackOrientation) {
|
|
166
|
+
const siblingStacks = Array.from(
|
|
167
|
+
ancestorStack.querySelectorAll(
|
|
168
|
+
`[data-dx-stack-item="${ancestorStack.getAttribute('data-dx-stack')}"] [data-dx-stack]`,
|
|
169
|
+
),
|
|
170
|
+
) as HTMLElement[];
|
|
171
|
+
const adjacentStack = siblingStacks[
|
|
172
|
+
(siblingStacks.indexOf(closestStack) + perpendicularDelta + siblingStacks.length) %
|
|
173
|
+
siblingStacks.length
|
|
174
|
+
] as HTMLElement | undefined;
|
|
175
|
+
const adjacentStackSelfItem = adjacentStack?.closest(
|
|
176
|
+
`[data-dx-stack-item=${ancestorStack.getAttribute('data-dx-stack')}]`,
|
|
177
|
+
) as HTMLElement | undefined;
|
|
178
|
+
const adjacentStackItems = adjacentStack
|
|
179
|
+
? (Array.from(
|
|
180
|
+
adjacentStack.querySelectorAll(
|
|
181
|
+
`[data-dx-stack-item="${adjacentStack.getAttribute('data-dx-stack')}"]`,
|
|
182
|
+
),
|
|
183
|
+
) as HTMLElement[])
|
|
184
|
+
: [];
|
|
185
|
+
if (adjacentStackItems.length > 0) {
|
|
186
|
+
// Find the closest item by position
|
|
187
|
+
const ownedItemRect = closestOwnedItem.getBoundingClientRect();
|
|
188
|
+
const targetPosition =
|
|
189
|
+
closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
|
|
190
|
+
|
|
191
|
+
let closestItem = adjacentStackItems[0];
|
|
192
|
+
let closestDistance = Infinity;
|
|
193
|
+
|
|
194
|
+
for (const item of adjacentStackItems) {
|
|
195
|
+
const itemRect = item.getBoundingClientRect();
|
|
196
|
+
const itemPosition = closestStackOrientation === 'vertical' ? itemRect.top : itemRect.left;
|
|
197
|
+
const distance = Math.abs(itemPosition - targetPosition);
|
|
198
|
+
|
|
199
|
+
if (distance < closestDistance) {
|
|
200
|
+
closestDistance = distance;
|
|
201
|
+
closestItem = item;
|
|
202
|
+
}
|
|
203
|
+
if (closestDistance <= PERPENDICULAR_FOCUS_THRESHHOLD) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
scrollIntoViewAndFocus(closestItem, closestStackOrientation);
|
|
210
|
+
} else if (adjacentStackSelfItem) {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
scrollIntoViewAndFocus(adjacentStackSelfItem, ancestorOrientation);
|
|
213
|
+
}
|
|
214
|
+
} else if (closestOwnedItem) {
|
|
215
|
+
const closestOwnedItemStack = closestOwnedItem.querySelector('[data-dx-stack]');
|
|
216
|
+
const closestOwnedItemStackItems = closestOwnedItemStack
|
|
217
|
+
? (Array.from(
|
|
218
|
+
closestOwnedItemStack.querySelectorAll(
|
|
219
|
+
`[data-dx-stack-item="${closestOwnedItemStack.getAttribute('data-dx-stack')}"]`,
|
|
220
|
+
),
|
|
221
|
+
) as HTMLElement[])
|
|
222
|
+
: [];
|
|
223
|
+
if (closestOwnedItemStackItems.length > 0) {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
scrollIntoViewAndFocus(
|
|
226
|
+
closestOwnedItemStackItems[
|
|
227
|
+
['ArrowUp', 'ArrowLeft'].includes(event.key) ? closestOwnedItemStackItems.length - 1 : 0
|
|
228
|
+
],
|
|
229
|
+
closestOwnedItemStack?.getAttribute('aria-orientation') as Orientation,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
props.onKeyDown?.(event);
|
|
237
|
+
},
|
|
238
|
+
[props.onKeyDown, stackId],
|
|
239
|
+
);
|
|
240
|
+
|
|
100
241
|
const gridClasses = useMemo(() => {
|
|
101
242
|
if (!rail) {
|
|
102
|
-
return orientation === 'horizontal' ? 'grid-rows-1 pli-
|
|
243
|
+
return orientation === 'horizontal' ? 'grid-rows-1 pli-[--stack-gap]' : 'grid-cols-1 plb-[--stack-gap]';
|
|
103
244
|
}
|
|
104
245
|
if (orientation === 'horizontal') {
|
|
105
|
-
return
|
|
246
|
+
return railGridHorizontal;
|
|
106
247
|
} else {
|
|
107
|
-
return
|
|
248
|
+
return railGridVertical;
|
|
108
249
|
}
|
|
109
250
|
}, [rail, orientation, size]);
|
|
110
251
|
|
|
@@ -125,19 +266,20 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
125
266
|
}, [stackElement, handleScroll]);
|
|
126
267
|
|
|
127
268
|
return (
|
|
128
|
-
<StackContext.Provider value={{ orientation, rail, size, onRearrange }}>
|
|
269
|
+
<StackContext.Provider value={{ orientation, rail, size, onRearrange, stackId }}>
|
|
129
270
|
<div
|
|
130
271
|
{...props}
|
|
131
|
-
{...arrowNavigationAttrs}
|
|
132
272
|
className={mx(
|
|
133
|
-
'grid relative',
|
|
273
|
+
'grid relative [--stack-gap:var(--dx-trimXs)]',
|
|
134
274
|
gridClasses,
|
|
135
|
-
|
|
275
|
+
size === 'contain' &&
|
|
136
276
|
(orientation === 'horizontal'
|
|
137
|
-
? 'overflow-x-auto min-bs-0 max-bs-full bs-full'
|
|
277
|
+
? 'overflow-x-auto overscroll-x-contain min-bs-0 max-bs-full bs-full'
|
|
138
278
|
: 'overflow-y-auto min-is-0 max-is-full is-full'),
|
|
139
279
|
classNames,
|
|
140
280
|
)}
|
|
281
|
+
onKeyDown={handleKeyDown}
|
|
282
|
+
data-dx-stack={stackId}
|
|
141
283
|
data-rail={rail}
|
|
142
284
|
aria-orientation={orientation}
|
|
143
285
|
style={styles}
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { createContext, useContext } from 'react';
|
|
6
6
|
|
|
7
|
+
import { type StackItemRearrangeHandler, type StackItemSize } from './defs';
|
|
7
8
|
import { type Orientation, type Size } from './Stack';
|
|
8
|
-
import { type StackItemSize, type StackItemRearrangeHandler } from './defs';
|
|
9
9
|
|
|
10
10
|
export type StackContextValue = {
|
|
11
11
|
orientation: Orientation;
|
|
12
12
|
rail: boolean;
|
|
13
13
|
size: Size;
|
|
14
14
|
onRearrange?: StackItemRearrangeHandler;
|
|
15
|
+
stackId?: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export const StackContext = createContext<StackContextValue>({
|
|
@@ -7,16 +7,14 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
8
|
import React from 'react';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { DropdownMenu, Icon } from '@dxos/react-ui';
|
|
11
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
13
|
-
import { StackItem } from './StackItem';
|
|
13
|
+
import { StackItem, type StackItemRootProps } from './StackItem';
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
render: (args) => (
|
|
19
|
-
<StackItem.Root role='section' {...args} classNames='w-[20rem] border border-separator'>
|
|
15
|
+
const DefaultStory = (props: StackItemRootProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<StackItem.Root role='section' {...props} classNames='w-[20rem] border border-separator'>
|
|
20
18
|
<StackItem.Heading>
|
|
21
19
|
<span className='sr-only'>Title</span>
|
|
22
20
|
<div role='none' className='sticky -block-start-px bg-[--sticky-bg] p-1 is-full'>
|
|
@@ -31,16 +29,22 @@ const meta: Meta<typeof StackItem.Root> = {
|
|
|
31
29
|
</StackItem.Heading>
|
|
32
30
|
<StackItem.Content classNames='p-2'>Content</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: {
|