@dxos/react-ui 0.8.4-main.a4bbb77 → 0.8.4-main.ae835ea
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-LBCJC75U.mjs → chunk-HUZZ56DW.mjs} +165 -115
- package/dist/lib/browser/chunk-HUZZ56DW.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +3 -1
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +44 -16
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/{chunk-QTUGGUCB.mjs → chunk-OJLL6E2Z.mjs} +165 -115
- package/dist/lib/node-esm/chunk-OJLL6E2Z.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +3 -1
- package/dist/lib/node-esm/index.mjs.map +1 -1
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +44 -16
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/components/Icon/Icon.stories.d.ts +17 -0
- package/dist/types/src/components/Icon/Icon.stories.d.ts.map +1 -0
- package/dist/types/src/components/Lists/Treegrid.d.ts.map +1 -1
- package/dist/types/src/components/Main/Main.d.ts +1 -10
- package/dist/types/src/components/Main/Main.d.ts.map +1 -1
- package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -5
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/hooks/useVisualViewport.d.ts +2 -2
- package/dist/types/src/hooks/useVisualViewport.d.ts.map +1 -1
- package/dist/types/src/testing/decorators/index.d.ts +2 -1
- package/dist/types/src/testing/decorators/index.d.ts.map +1 -1
- package/dist/types/src/testing/decorators/withLayout.d.ts +15 -0
- package/dist/types/src/testing/decorators/withLayout.d.ts.map +1 -0
- package/dist/types/src/util/domino.d.ts +1 -1
- package/dist/types/src/util/domino.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/usePx.d.ts +8 -0
- package/dist/types/src/util/usePx.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -15
- package/src/components/Icon/Icon.stories.tsx +113 -0
- package/src/components/Icon/Icon.tsx +1 -1
- package/src/components/Lists/Treegrid.tsx +57 -16
- package/src/components/Main/Main.tsx +16 -7
- package/src/components/Popover/Popover.tsx +3 -3
- package/src/components/Toolbar/Toolbar.tsx +16 -5
- package/src/hooks/useSafeArea.ts +2 -2
- package/src/hooks/useVisualViewport.ts +3 -3
- package/src/testing/decorators/index.ts +2 -1
- package/src/testing/decorators/withLayout.tsx +56 -0
- package/src/util/domino.ts +6 -4
- package/src/util/index.ts +1 -0
- package/src/util/usePx.ts +61 -0
- package/dist/lib/browser/chunk-LBCJC75U.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-QTUGGUCB.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.ae835ea",
|
|
4
4
|
"description": "Low-level React components for DXOS, applying a theme to a core group of primitives",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -74,32 +74,33 @@
|
|
|
74
74
|
"keyborg": "^2.5.0",
|
|
75
75
|
"react-i18next": "^11.18.6",
|
|
76
76
|
"react-remove-scroll": "^2.6.0",
|
|
77
|
-
"@dxos/debug": "0.8.4-main.
|
|
78
|
-
"@dxos/lit-ui": "0.8.4-main.
|
|
79
|
-
"@dxos/log": "0.8.4-main.
|
|
80
|
-
"@dxos/react-hooks": "0.8.4-main.
|
|
81
|
-
"@dxos/react-input": "0.8.4-main.
|
|
82
|
-
"@dxos/react-list": "0.8.4-main.
|
|
83
|
-
"@dxos/react-ui-types": "0.8.4-main.
|
|
84
|
-
"@dxos/util": "0.8.4-main.
|
|
77
|
+
"@dxos/debug": "0.8.4-main.ae835ea",
|
|
78
|
+
"@dxos/lit-ui": "0.8.4-main.ae835ea",
|
|
79
|
+
"@dxos/log": "0.8.4-main.ae835ea",
|
|
80
|
+
"@dxos/react-hooks": "0.8.4-main.ae835ea",
|
|
81
|
+
"@dxos/react-input": "0.8.4-main.ae835ea",
|
|
82
|
+
"@dxos/react-list": "0.8.4-main.ae835ea",
|
|
83
|
+
"@dxos/react-ui-types": "0.8.4-main.ae835ea",
|
|
84
|
+
"@dxos/util": "0.8.4-main.ae835ea"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@dnd-kit/core": "^6.0.5",
|
|
88
88
|
"@dnd-kit/sortable": "^7.0.1",
|
|
89
89
|
"@dnd-kit/utilities": "^3.2.0",
|
|
90
|
-
"@
|
|
91
|
-
"@types/react
|
|
90
|
+
"@phosphor-icons/react": "^2.1.10",
|
|
91
|
+
"@types/react": "~19.2.2",
|
|
92
|
+
"@types/react-dom": "~19.2.2",
|
|
92
93
|
"react": "~19.2.0",
|
|
93
94
|
"react-dom": "~19.2.0",
|
|
94
95
|
"vite": "7.1.9",
|
|
95
|
-
"@dxos/random": "0.8.4-main.
|
|
96
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
97
|
-
"@dxos/util": "0.8.4-main.
|
|
96
|
+
"@dxos/random": "0.8.4-main.ae835ea",
|
|
97
|
+
"@dxos/react-ui-theme": "0.8.4-main.ae835ea",
|
|
98
|
+
"@dxos/util": "0.8.4-main.ae835ea"
|
|
98
99
|
},
|
|
99
100
|
"peerDependencies": {
|
|
100
101
|
"react": "^19.0.0",
|
|
101
102
|
"react-dom": "^19.0.0",
|
|
102
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
103
|
+
"@dxos/react-ui-theme": "0.8.4-main.ae835ea"
|
|
103
104
|
},
|
|
104
105
|
"publishConfig": {
|
|
105
106
|
"access": "public"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { IconBase, type IconProps, type IconWeight } from '@phosphor-icons/react';
|
|
6
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
|
+
import React, { type FC, type ReactElement, type SVGProps, forwardRef } from 'react';
|
|
8
|
+
|
|
9
|
+
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
10
|
+
|
|
11
|
+
import { withTheme } from '../../testing';
|
|
12
|
+
|
|
13
|
+
import { Icon } from './Icon';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create icon from serializable data.
|
|
17
|
+
* https://github.com/phosphor-icons/react#custom-icons
|
|
18
|
+
* https://github.com/phosphor-icons/core/tree/main/assets
|
|
19
|
+
*/
|
|
20
|
+
const createIcon = ({
|
|
21
|
+
name,
|
|
22
|
+
weights,
|
|
23
|
+
}: {
|
|
24
|
+
name: string;
|
|
25
|
+
weights: Record<string, SVGProps<SVGPathElement>[]>;
|
|
26
|
+
}): FC<IconProps> => {
|
|
27
|
+
const CustomIcon = forwardRef<SVGSVGElement, IconProps>((props, ref) => (
|
|
28
|
+
<IconBase
|
|
29
|
+
ref={ref}
|
|
30
|
+
{...props}
|
|
31
|
+
weights={
|
|
32
|
+
new Map<IconWeight, ReactElement>(
|
|
33
|
+
Object.entries(weights).map(
|
|
34
|
+
([key, paths]) =>
|
|
35
|
+
[
|
|
36
|
+
key,
|
|
37
|
+
<>
|
|
38
|
+
{paths.map((props, i) => (
|
|
39
|
+
<path key={`${key}-${i}`} {...props} />
|
|
40
|
+
))}
|
|
41
|
+
</>,
|
|
42
|
+
] as [IconWeight, ReactElement],
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
/>
|
|
47
|
+
));
|
|
48
|
+
|
|
49
|
+
CustomIcon.displayName = name;
|
|
50
|
+
return CustomIcon;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const DefaultStory = ({ CustomIcon }: { CustomIcon: FC<IconProps> }) => {
|
|
54
|
+
return (
|
|
55
|
+
<div className='grid grid-cols-2 gap-8'>
|
|
56
|
+
<CustomIcon weight={'regular'} className={mx(getSize(16))} />
|
|
57
|
+
<Icon icon='ph--github-logo--regular' classNames={mx(getSize(16))} />
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const meta = {
|
|
63
|
+
title: 'ui/react-ui-core/Icon',
|
|
64
|
+
render: DefaultStory,
|
|
65
|
+
decorators: [withTheme],
|
|
66
|
+
parameters: {
|
|
67
|
+
layout: 'centered',
|
|
68
|
+
},
|
|
69
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
70
|
+
|
|
71
|
+
export default meta;
|
|
72
|
+
|
|
73
|
+
type Story = StoryObj<typeof meta>;
|
|
74
|
+
|
|
75
|
+
export const Default: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
CustomIcon: createIcon({
|
|
78
|
+
name: 'GithubLogo',
|
|
79
|
+
weights: {
|
|
80
|
+
// https://github.com/phosphor-icons/core/tree/main/assets
|
|
81
|
+
// <path d="M119.83,56A52,52,0,0,0,76,32a51.92,51.92,0,0,0-3.49,44.7A49.28,49.28,0,0,0,64,104v8a48,48,0,0,0,48,48h48a48,48,0,0,0,48-48v-8a49.28,49.28,0,0,0-8.51-27.3A51.92,51.92,0,0,0,196,32a52,52,0,0,0-43.83,24Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
|
82
|
+
// <path d="M104,232V192a32,32,0,0,1,32-32h0a32,32,0,0,1,32,32v40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
|
83
|
+
// <path d="M104,208H72a32,32,0,0,1-32-32A32,32,0,0,0,8,144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
|
84
|
+
regular: [
|
|
85
|
+
{
|
|
86
|
+
d: 'M119.83,56A52,52,0,0,0,76,32a51.92,51.92,0,0,0-3.49,44.7A49.28,49.28,0,0,0,64,104v8a48,48,0,0,0,48,48h48a48,48,0,0,0,48-48v-8a49.28,49.28,0,0,0-8.51-27.3A51.92,51.92,0,0,0,196,32a52,52,0,0,0-43.83,24Z',
|
|
87
|
+
fill: 'none',
|
|
88
|
+
stroke: 'currentColor',
|
|
89
|
+
strokeLinecap: 'round',
|
|
90
|
+
strokeLinejoin: 'round',
|
|
91
|
+
strokeWidth: '16',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
d: 'M104,232V192a32,32,0,0,1,32-32h0a32,32,0,0,1,32,32v40',
|
|
95
|
+
fill: 'none',
|
|
96
|
+
stroke: 'currentColor',
|
|
97
|
+
strokeLinecap: 'round',
|
|
98
|
+
strokeLinejoin: 'round',
|
|
99
|
+
strokeWidth: '16',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
d: 'M104,208H72a32,32,0,0,1-32-32A32,32,0,0,0,8,144',
|
|
103
|
+
fill: 'none',
|
|
104
|
+
stroke: 'currentColor',
|
|
105
|
+
strokeLinecap: 'round',
|
|
106
|
+
strokeLinejoin: 'round',
|
|
107
|
+
strokeWidth: '16',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -16,7 +16,7 @@ export type IconProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.s
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export const Icon = memo(
|
|
19
|
-
forwardRef<SVGSVGElement, IconProps>(({ icon, classNames, size, ...props }, forwardedRef) => {
|
|
19
|
+
forwardRef<SVGSVGElement, IconProps>(({ icon, classNames, size = 4, ...props }, forwardedRef) => {
|
|
20
20
|
const { tx } = useThemeContext();
|
|
21
21
|
const href = useIconHref(icon);
|
|
22
22
|
return (
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useFocusFinders } from '@fluentui/react-tabster';
|
|
6
6
|
import { type Scope, createContextScope } from '@radix-ui/react-context';
|
|
7
7
|
import { Primitive } from '@radix-ui/react-primitive';
|
|
8
8
|
import { Slot } from '@radix-ui/react-slot';
|
|
9
9
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
10
|
-
import React, {
|
|
10
|
+
import React, {
|
|
11
|
+
type CSSProperties,
|
|
12
|
+
type ComponentPropsWithRef,
|
|
13
|
+
type KeyboardEvent,
|
|
14
|
+
forwardRef,
|
|
15
|
+
useCallback,
|
|
16
|
+
} from 'react';
|
|
11
17
|
|
|
12
18
|
import { useThemeContext } from '../../hooks';
|
|
13
19
|
import { type ThemedClassName } from '../../util';
|
|
@@ -40,12 +46,58 @@ const TreegridRoot = forwardRef<HTMLDivElement, TreegridRootProps>(
|
|
|
40
46
|
({ asChild, classNames, children, style, gridTemplateColumns, ...props }, forwardedRef) => {
|
|
41
47
|
const { tx } = useThemeContext();
|
|
42
48
|
const Root = asChild ? Slot : Primitive.div;
|
|
43
|
-
const
|
|
49
|
+
const { findFirstFocusable } = useFocusFinders();
|
|
50
|
+
|
|
51
|
+
const handleKeyDown = useCallback(
|
|
52
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
53
|
+
switch (event.key) {
|
|
54
|
+
case 'ArrowDown':
|
|
55
|
+
case 'ArrowUp': {
|
|
56
|
+
const direction = event.key === 'ArrowDown' ? 'down' : 'up';
|
|
57
|
+
const target = event.target as HTMLElement;
|
|
58
|
+
|
|
59
|
+
// Find ancestor with data-arrow-keys containing the relevant direction.
|
|
60
|
+
const ancestorWithArrowKeys = target.closest(`[data-arrow-keys*="${direction}"], [data-arrow-keys="all"]`);
|
|
61
|
+
|
|
62
|
+
// If no ancestor with data-arrow-keys found, proceed with row navigation.
|
|
63
|
+
if (!ancestorWithArrowKeys) {
|
|
64
|
+
// Find the closest row
|
|
65
|
+
const currentRow = target.closest('[role="row"]');
|
|
66
|
+
if (currentRow) {
|
|
67
|
+
// Find the treegrid container.
|
|
68
|
+
const treegrid = currentRow.closest('[role="treegrid"]');
|
|
69
|
+
if (treegrid) {
|
|
70
|
+
// Get all rows in the treegrid.
|
|
71
|
+
const rows = Array.from(treegrid.querySelectorAll('[role="row"]'));
|
|
72
|
+
const currentIndex = rows.indexOf(currentRow as Element);
|
|
73
|
+
|
|
74
|
+
// Find next or previous row.
|
|
75
|
+
const nextIndex = direction === 'down' ? currentIndex + 1 : currentIndex - 1;
|
|
76
|
+
const targetRow = rows[nextIndex];
|
|
77
|
+
|
|
78
|
+
if (targetRow) {
|
|
79
|
+
// Focus the first focusable element in the target row.
|
|
80
|
+
const firstFocusable = findFirstFocusable(targetRow as HTMLElement);
|
|
81
|
+
if (firstFocusable) {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
firstFocusable.focus();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
props.onKeyDown?.(event);
|
|
93
|
+
},
|
|
94
|
+
[findFirstFocusable],
|
|
95
|
+
);
|
|
44
96
|
|
|
45
97
|
return (
|
|
46
98
|
<Root
|
|
47
99
|
role='treegrid'
|
|
48
|
-
{
|
|
100
|
+
onKeyDown={handleKeyDown}
|
|
49
101
|
{...props}
|
|
50
102
|
className={tx('treegrid.root', 'treegrid', {}, classNames)}
|
|
51
103
|
style={{ ...style, gridTemplateColumns }}
|
|
@@ -91,13 +143,6 @@ const TreegridRow = forwardRef<HTMLDivElement, TreegridRowScopedProps<TreegridRo
|
|
|
91
143
|
onChange: propsOnOpenChange,
|
|
92
144
|
defaultProp: defaultOpen,
|
|
93
145
|
});
|
|
94
|
-
const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
|
|
95
|
-
const arrowGroupAttrs = useArrowNavigationGroup({
|
|
96
|
-
axis: 'horizontal',
|
|
97
|
-
tabbable: false,
|
|
98
|
-
circular: false,
|
|
99
|
-
memorizeCurrent: false,
|
|
100
|
-
});
|
|
101
146
|
|
|
102
147
|
return (
|
|
103
148
|
<TreegridRowProvider open={open} onOpenChange={onOpenChange} scope={__treegridRowScope}>
|
|
@@ -106,15 +151,11 @@ const TreegridRow = forwardRef<HTMLDivElement, TreegridRowScopedProps<TreegridRo
|
|
|
106
151
|
aria-level={level}
|
|
107
152
|
className={tx('treegrid.row', 'treegrid__row', { level }, classNames)}
|
|
108
153
|
{...(parentOf && { 'aria-expanded': open, 'aria-owns': parentOf })}
|
|
109
|
-
tabIndex={0}
|
|
110
|
-
{...focusableGroupAttrs}
|
|
111
154
|
{...props}
|
|
112
155
|
id={id}
|
|
113
156
|
ref={forwardedRef}
|
|
114
157
|
>
|
|
115
|
-
|
|
116
|
-
{children}
|
|
117
|
-
</div>
|
|
158
|
+
{children}
|
|
118
159
|
</Root>
|
|
119
160
|
</TreegridRowProvider>
|
|
120
161
|
);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
5
6
|
import { createContext } from '@radix-ui/react-context';
|
|
6
7
|
import { DialogContent, Root as DialogRoot, DialogTitle } from '@radix-ui/react-dialog';
|
|
7
8
|
import { Primitive } from '@radix-ui/react-primitive';
|
|
@@ -70,7 +71,10 @@ const useLandmarkMover = (propsOnKeyDown: ComponentPropsWithoutRef<'div'>['onKey
|
|
|
70
71
|
},
|
|
71
72
|
[propsOnKeyDown],
|
|
72
73
|
);
|
|
73
|
-
|
|
74
|
+
|
|
75
|
+
// TODO(thure): This was disconnected once before in #8818, if this should change again to support the browser
|
|
76
|
+
// extension, please ensure the change doesn’t break web, desktop and mobile.
|
|
77
|
+
const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited', ignoreDefaultKeydown: { Tab: true } });
|
|
74
78
|
|
|
75
79
|
return {
|
|
76
80
|
onKeyDown: handleKeyDown,
|
|
@@ -141,7 +145,7 @@ const MainRoot = ({
|
|
|
141
145
|
children,
|
|
142
146
|
...props
|
|
143
147
|
}: MainRootProps) => {
|
|
144
|
-
const [isLg] = useMediaQuery('lg'
|
|
148
|
+
const [isLg] = useMediaQuery('lg');
|
|
145
149
|
const [navigationSidebarState = isLg ? 'expanded' : 'collapsed', setNavigationSidebarState] =
|
|
146
150
|
useControllableState<SidebarState>({
|
|
147
151
|
prop: propsNavigationSidebarState,
|
|
@@ -210,7 +214,7 @@ const MainSidebar = forwardRef<HTMLDivElement, MainSidebarProps>(
|
|
|
210
214
|
{ classNames, children, swipeToDismiss, onOpenAutoFocus, state, resizing, onStateChange, side, label, ...props },
|
|
211
215
|
forwardedRef,
|
|
212
216
|
) => {
|
|
213
|
-
const [isLg] = useMediaQuery('lg'
|
|
217
|
+
const [isLg] = useMediaQuery('lg');
|
|
214
218
|
const { tx } = useThemeContext();
|
|
215
219
|
const { t } = useTranslation();
|
|
216
220
|
const ref = useForwardedRef(forwardedRef);
|
|
@@ -218,10 +222,15 @@ const MainSidebar = forwardRef<HTMLDivElement, MainSidebarProps>(
|
|
|
218
222
|
useSwipeToDismiss(swipeToDismiss ? ref : noopRef, {
|
|
219
223
|
onDismiss: () => onStateChange?.('closed'),
|
|
220
224
|
});
|
|
225
|
+
// NOTE(thure): This is a workaround for something further down the tree grabbing focus on Escape. Adding this
|
|
226
|
+
// intervention to `Tabs.Root` or `Tabs.Tabpenel` instances is somehow ineffectual.
|
|
221
227
|
const handleKeyDown = useCallback(
|
|
222
228
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
const focusGroupParent = (event.target as HTMLElement).closest('[data-tabster]');
|
|
230
|
+
if (event.key === 'Escape' && focusGroupParent) {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
event.stopPropagation();
|
|
233
|
+
(focusGroupParent as HTMLElement).focus();
|
|
225
234
|
}
|
|
226
235
|
props.onKeyDown?.(event);
|
|
227
236
|
},
|
|
@@ -239,7 +248,7 @@ const MainSidebar = forwardRef<HTMLDivElement, MainSidebarProps>(
|
|
|
239
248
|
data-state={state}
|
|
240
249
|
data-resizing={resizing ? 'true' : 'false'}
|
|
241
250
|
className={tx('main.sidebar', 'main__sidebar', {}, classNames)}
|
|
242
|
-
|
|
251
|
+
onKeyDownCapture={handleKeyDown}
|
|
243
252
|
{...(state === 'closed' && { inert: true })}
|
|
244
253
|
ref={ref}
|
|
245
254
|
>
|
|
@@ -329,7 +338,7 @@ MainContent.displayName = MAIN_NAME;
|
|
|
329
338
|
type MainOverlayProps = ThemedClassName<Omit<ComponentPropsWithRef<typeof Primitive.div>, 'children'>>;
|
|
330
339
|
|
|
331
340
|
const MainOverlay = forwardRef<HTMLDivElement, MainOverlayProps>(({ classNames, ...props }, forwardedRef) => {
|
|
332
|
-
const [isLg] = useMediaQuery('lg'
|
|
341
|
+
const [isLg] = useMediaQuery('lg');
|
|
333
342
|
const { navigationSidebarState, setNavigationSidebarState, complementarySidebarState, setComplementarySidebarState } =
|
|
334
343
|
useMainContext(MAIN_NAME);
|
|
335
344
|
const { tx } = useThemeContext();
|
|
@@ -396,6 +396,7 @@ type PopoverContentImplElement = ElementRef<typeof PopperPrimitive.Content>;
|
|
|
396
396
|
type FocusScopeProps = ComponentPropsWithoutRef<typeof FocusScope>;
|
|
397
397
|
type DismissableLayerProps = ComponentPropsWithoutRef<typeof DismissableLayer>;
|
|
398
398
|
type PopperContentProps = ThemedClassName<ComponentPropsWithoutRef<typeof PopperPrimitive.Content>>;
|
|
399
|
+
|
|
399
400
|
interface PopoverContentImplProps
|
|
400
401
|
extends Omit<PopperContentProps, 'onPlaced'>,
|
|
401
402
|
Omit<DismissableLayerProps, 'onDismiss'> {
|
|
@@ -440,8 +441,7 @@ const PopoverContentImpl = forwardRef<PopoverContentImplElement, PopoverContentI
|
|
|
440
441
|
const elevation = useElevationContext();
|
|
441
442
|
const safeCollisionPadding = useSafeCollisionPadding(collisionPadding);
|
|
442
443
|
|
|
443
|
-
// Make sure the whole tree has focus guards as our `Popover` may be
|
|
444
|
-
// the last element in the DOM (because of the `Portal`)
|
|
444
|
+
// Make sure the whole tree has focus guards as our `Popover` may be the last element in the DOM (because of the `Portal`)
|
|
445
445
|
useFocusGuards();
|
|
446
446
|
|
|
447
447
|
return (
|
|
@@ -472,7 +472,7 @@ const PopoverContentImpl = forwardRef<PopoverContentImplElement, PopoverContentI
|
|
|
472
472
|
ref={forwardedRef}
|
|
473
473
|
style={{
|
|
474
474
|
...contentProps.style,
|
|
475
|
-
//
|
|
475
|
+
// Re-namespace exposed content custom properties.
|
|
476
476
|
...{
|
|
477
477
|
'--radix-popover-content-transform-origin': 'var(--radix-popper-transform-origin)',
|
|
478
478
|
'--radix-popover-content-available-width': 'var(--radix-popper-available-width)',
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ToggleGroupItemProps as ToggleGroupItemPrimitiveProps } from '@radix-ui/react-toggle-group';
|
|
6
6
|
import * as ToolbarPrimitive from '@radix-ui/react-toolbar';
|
|
7
|
-
import React, { forwardRef } from 'react';
|
|
7
|
+
import React, { Fragment, forwardRef } from 'react';
|
|
8
8
|
|
|
9
9
|
import { useThemeContext } from '../../hooks';
|
|
10
10
|
import { type ThemedClassName } from '../../util';
|
|
@@ -22,19 +22,30 @@ import {
|
|
|
22
22
|
import { Link, type LinkProps } from '../Link';
|
|
23
23
|
import { Separator, type SeparatorProps } from '../Separator';
|
|
24
24
|
|
|
25
|
-
type ToolbarRootProps = ThemedClassName<
|
|
25
|
+
type ToolbarRootProps = ThemedClassName<
|
|
26
|
+
ToolbarPrimitive.ToolbarProps & {
|
|
27
|
+
textBlockWidth?: boolean;
|
|
28
|
+
layoutManaged?: boolean;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
>;
|
|
26
32
|
|
|
27
33
|
const ToolbarRoot = forwardRef<HTMLDivElement, ToolbarRootProps>(
|
|
28
|
-
({ classNames, children, layoutManaged, ...props }, forwardedRef) => {
|
|
34
|
+
({ classNames, children, layoutManaged, textBlockWidth: textBlockWidthParam, disabled, ...props }, forwardedRef) => {
|
|
29
35
|
const { tx } = useThemeContext();
|
|
36
|
+
const InnerRoot = textBlockWidthParam ? 'div' : Fragment;
|
|
37
|
+
const innerRootProps = textBlockWidthParam
|
|
38
|
+
? { role: 'none', className: tx('toolbar.inner', 'toolbar', { layoutManaged }, classNames) }
|
|
39
|
+
: {};
|
|
40
|
+
|
|
30
41
|
return (
|
|
31
42
|
<ToolbarPrimitive.Root
|
|
32
43
|
{...props}
|
|
33
44
|
data-arrow-keys={props.orientation === 'vertical' ? 'up down' : 'left right'}
|
|
34
|
-
className={tx('toolbar.root', 'toolbar', { layoutManaged }, classNames)}
|
|
45
|
+
className={tx('toolbar.root', 'toolbar', { layoutManaged, disabled }, classNames)}
|
|
35
46
|
ref={forwardedRef}
|
|
36
47
|
>
|
|
37
|
-
{children}
|
|
48
|
+
<InnerRoot {...innerRootProps}>{children}</InnerRoot>
|
|
38
49
|
</ToolbarPrimitive.Root>
|
|
39
50
|
);
|
|
40
51
|
},
|
package/src/hooks/useSafeArea.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { useCallback, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { useViewportResize } from '@dxos/react-hooks';
|
|
8
8
|
|
|
9
9
|
export type SafeAreaPadding = Record<'top' | 'right' | 'bottom' | 'left', number>;
|
|
10
10
|
|
|
@@ -21,6 +21,6 @@ export const useSafeArea = (): SafeAreaPadding => {
|
|
|
21
21
|
});
|
|
22
22
|
}, []);
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
useViewportResize(handleResize);
|
|
25
25
|
return padding;
|
|
26
26
|
};
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { useCallback, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { useViewportResize } from '@dxos/react-hooks';
|
|
8
8
|
|
|
9
|
-
export const useVisualViewport = (deps?: Parameters<typeof
|
|
9
|
+
export const useVisualViewport = (deps?: Parameters<typeof useViewportResize>[1]) => {
|
|
10
10
|
const [width, setWidth] = useState<number | null>(null);
|
|
11
11
|
const [height, setHeight] = useState<number | null>(null);
|
|
12
12
|
|
|
@@ -17,7 +17,7 @@ export const useVisualViewport = (deps?: Parameters<typeof useResize>[1]) => {
|
|
|
17
17
|
}
|
|
18
18
|
}, []);
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
useViewportResize(handleResize, deps);
|
|
21
21
|
|
|
22
22
|
return { width, height };
|
|
23
23
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Decorator } from '@storybook/react';
|
|
6
|
+
import React, { type FC, type PropsWithChildren } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type ClassNameValue, type ThemedClassName } from '@dxos/react-ui';
|
|
9
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
+
|
|
11
|
+
export type ContainerProps = ThemedClassName<PropsWithChildren>;
|
|
12
|
+
|
|
13
|
+
export type ContainerType = 'default' | 'column';
|
|
14
|
+
|
|
15
|
+
export type WithLayoutProps =
|
|
16
|
+
| FC<ContainerProps>
|
|
17
|
+
| { classNames?: ClassNameValue; container?: ContainerType; scroll?: boolean };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Adds layout container.
|
|
21
|
+
*/
|
|
22
|
+
export const withLayout =
|
|
23
|
+
(props: WithLayoutProps): Decorator =>
|
|
24
|
+
(Story) => {
|
|
25
|
+
if (typeof props === 'function') {
|
|
26
|
+
const Container = props;
|
|
27
|
+
return (
|
|
28
|
+
<Container>
|
|
29
|
+
<Story />
|
|
30
|
+
</Container>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Container = layouts[(props as any).container as ContainerType] ?? layouts.default;
|
|
35
|
+
return (
|
|
36
|
+
<Container classNames={mx(props.classNames, props.scroll ? 'overflow-y-auto' : 'overflow-hidden')}>
|
|
37
|
+
<Story />
|
|
38
|
+
</Container>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const layouts: Record<ContainerType, FC<ContainerProps>> = {
|
|
43
|
+
default: ({ children, classNames }: ContainerProps) => (
|
|
44
|
+
<div role='none' className={mx(classNames)}>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
|
|
49
|
+
column: ({ children, classNames }: ContainerProps) => (
|
|
50
|
+
<div role='none' className='fixed inset-0 flex justify-center overflow-hidden bg-deckSurface'>
|
|
51
|
+
<div role='none' className={mx('flex flex-col is-[40rem] bg-baseSurface', classNames)}>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
};
|
package/src/util/domino.ts
CHANGED
|
@@ -29,12 +29,14 @@ export class Domino<T extends HTMLElement> {
|
|
|
29
29
|
this._el.dataset[key] = value;
|
|
30
30
|
return this;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
Object.
|
|
32
|
+
attributes(attr: Record<string, string | undefined>): this {
|
|
33
|
+
Object.entries(attr)
|
|
34
|
+
.filter(([_, value]) => value !== undefined)
|
|
35
|
+
.map(([key, value]) => this._el.setAttribute(key, value!));
|
|
34
36
|
return this;
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
(this._el
|
|
38
|
+
style(styles: Partial<CSSStyleDeclaration>): this {
|
|
39
|
+
Object.assign(this._el.style, styles);
|
|
38
40
|
return this;
|
|
39
41
|
}
|
|
40
42
|
children<C extends HTMLElement>(...children: Domino<C>[]): this {
|
package/src/util/index.ts
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
const getDocumentElementFontSize = () => parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* React hook that converts rem values to pixels and updates when the root font size changes.
|
|
11
|
+
*
|
|
12
|
+
* @param rem The rem value to convert to pixels
|
|
13
|
+
* @returns The current pixel value equivalent of the rem input
|
|
14
|
+
*/
|
|
15
|
+
export const usePx = (rem: number): number => {
|
|
16
|
+
const [fontSize, setFontSize] = useState(() => {
|
|
17
|
+
if (typeof document !== 'undefined') {
|
|
18
|
+
return getDocumentElementFontSize();
|
|
19
|
+
}
|
|
20
|
+
return 16; // Default fallback for SSR
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const updateFontSize = useCallback(() => {
|
|
24
|
+
setFontSize(getDocumentElementFontSize());
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (typeof document === 'undefined') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create a ResizeObserver to watch for font size changes on the document element
|
|
33
|
+
const resizeObserver = new ResizeObserver(updateFontSize);
|
|
34
|
+
resizeObserver.observe(document.documentElement);
|
|
35
|
+
|
|
36
|
+
// Also listen for viewport changes that might affect font size
|
|
37
|
+
const mediaQueryList = window.matchMedia('all');
|
|
38
|
+
const handleMediaChange = () => {
|
|
39
|
+
updateFontSize();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (mediaQueryList.addEventListener) {
|
|
43
|
+
mediaQueryList.addEventListener('change', handleMediaChange);
|
|
44
|
+
} else {
|
|
45
|
+
// Fallback for older browsers
|
|
46
|
+
mediaQueryList.addListener(handleMediaChange);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
resizeObserver.disconnect();
|
|
51
|
+
if (mediaQueryList.removeEventListener) {
|
|
52
|
+
mediaQueryList.removeEventListener('change', handleMediaChange);
|
|
53
|
+
} else {
|
|
54
|
+
// Fallback for older browsers
|
|
55
|
+
mediaQueryList.removeListener(handleMediaChange);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
return useMemo(() => rem * fontSize, [fontSize]);
|
|
61
|
+
};
|