@dxos/react-ui-attention 0.8.4-main.f9ba587 → 0.8.4-main.fcc0d83b33
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-DAIZNFKL.mjs +668 -0
- package/dist/lib/browser/chunk-DAIZNFKL.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +86 -13
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +20 -7
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types.mjs +1 -0
- package/dist/lib/browser/types.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HZTQDMTS.mjs +670 -0
- package/dist/lib/node-esm/chunk-HZTQDMTS.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +86 -13
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +20 -7
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types.mjs +2 -0
- package/dist/lib/node-esm/types.mjs.map +7 -0
- package/dist/types/src/attention.d.ts +56 -20
- package/dist/types/src/attention.d.ts.map +1 -1
- package/dist/types/src/components/AttentionGlyph.d.ts.map +1 -1
- package/dist/types/src/components/AttentionGlyph.stories.d.ts +9 -20
- package/dist/types/src/components/AttentionGlyph.stories.d.ts.map +1 -1
- package/dist/types/src/components/AttentionProvider.d.ts +15 -16
- package/dist/types/src/components/AttentionProvider.d.ts.map +1 -1
- package/dist/types/src/components/AttentionProvider.stories.d.ts +6 -4
- package/dist/types/src/components/AttentionProvider.stories.d.ts.map +1 -1
- package/dist/types/src/components/SelectionProvider.d.ts +6 -5
- package/dist/types/src/components/SelectionProvider.d.ts.map +1 -1
- package/dist/types/src/selection.d.ts +49 -17
- package/dist/types/src/selection.d.ts.map +1 -1
- package/dist/types/src/testing/decorators/withAttention.d.ts +5 -1
- package/dist/types/src/testing/decorators/withAttention.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +9 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -31
- package/src/attention.test.ts +188 -27
- package/src/attention.ts +175 -74
- package/src/components/AttentionGlyph.stories.tsx +17 -14
- package/src/components/AttentionGlyph.tsx +14 -7
- package/src/components/AttentionProvider.stories.tsx +12 -9
- package/src/components/AttentionProvider.tsx +80 -97
- package/src/components/SelectionProvider.tsx +66 -19
- package/src/selection.ts +135 -65
- package/src/testing/decorators/withAttention.ts +21 -4
- package/src/types.ts +13 -0
- package/dist/lib/browser/chunk-QPEFFJ4E.mjs +0 -616
- package/dist/lib/browser/chunk-QPEFFJ4E.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-F2JRKDYV.mjs +0 -618
- package/dist/lib/node-esm/chunk-F2JRKDYV.mjs.map +0 -7
|
@@ -2,19 +2,18 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
6
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
8
7
|
|
|
9
8
|
import { Button } from '@dxos/react-ui';
|
|
10
|
-
import { withTheme } from '@dxos/
|
|
9
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
10
|
import { range } from '@dxos/util';
|
|
12
11
|
|
|
13
12
|
import { AttentionGlyph, type AttentionGlyphProps } from './AttentionGlyph';
|
|
14
13
|
|
|
15
|
-
const
|
|
14
|
+
const DefaultStory = (props: AttentionGlyphProps) => {
|
|
16
15
|
return (
|
|
17
|
-
<ul className='flex gap-2
|
|
16
|
+
<ul className='flex gap-2 mb-2'>
|
|
18
17
|
<li>
|
|
19
18
|
<AttentionGlyph presence='none' {...props} />
|
|
20
19
|
</li>
|
|
@@ -28,26 +27,30 @@ const Story = (props: AttentionGlyphProps) => {
|
|
|
28
27
|
);
|
|
29
28
|
};
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
const meta = {
|
|
32
31
|
title: 'ui/react-ui-attention/AttentionGlyph',
|
|
33
|
-
component: AttentionGlyph,
|
|
34
|
-
render:
|
|
35
|
-
decorators: [withTheme],
|
|
36
|
-
}
|
|
32
|
+
component: AttentionGlyph as any,
|
|
33
|
+
render: DefaultStory,
|
|
34
|
+
decorators: [withTheme()],
|
|
35
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
|
|
39
|
+
type Story = StoryObj<typeof meta>;
|
|
37
40
|
|
|
38
|
-
export const Default = {
|
|
41
|
+
export const Default: Story = {
|
|
39
42
|
args: {},
|
|
40
43
|
};
|
|
41
44
|
|
|
42
|
-
export const Attention = {
|
|
45
|
+
export const Attention: Story = {
|
|
43
46
|
args: { attended: true },
|
|
44
47
|
};
|
|
45
48
|
|
|
46
|
-
export const Contains = {
|
|
49
|
+
export const Contains: Story = {
|
|
47
50
|
args: { containsAttended: true },
|
|
48
51
|
};
|
|
49
52
|
|
|
50
|
-
export const Syncing = {
|
|
53
|
+
export const Syncing: Story = {
|
|
51
54
|
render: () => {
|
|
52
55
|
const [spaces, setSpaces] = useState(
|
|
53
56
|
new Map<string, boolean>(range(8).map((i) => [`space-${i + 1}`, Math.random() > 0.5])),
|
|
@@ -3,16 +3,23 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Primitive } from '@radix-ui/react-primitive';
|
|
6
|
-
import React, { type
|
|
6
|
+
import React, { type CSSProperties, type ComponentPropsWithRef, forwardRef, useMemo } from 'react';
|
|
7
7
|
|
|
8
8
|
import { Icon, type ThemedClassName } from '@dxos/react-ui';
|
|
9
|
-
import { mx } from '@dxos/
|
|
9
|
+
import { mx } from '@dxos/ui-theme';
|
|
10
10
|
|
|
11
|
-
const attentionGlyphStyles =
|
|
12
|
-
'inline-block rounded-
|
|
11
|
+
const attentionGlyphStyles = mx(
|
|
12
|
+
'inline-block rounded-xs w-3 h-3 bg-transparent text-accent-text transition-colors',
|
|
13
|
+
'[[data-contains-attended=true]_&]:bg-attention-contains',
|
|
14
|
+
'[[data-attention=true]_&]:bg-accent-surface',
|
|
15
|
+
'[[data-attention=true]_&]:text-accent-surface-text',
|
|
16
|
+
'[[aria-current][data-attention=true]_&]:bg-accent-surface',
|
|
17
|
+
'[[aria-current][data-attention=true]_&]:text-accent-surface-text',
|
|
18
|
+
'[[aria-selected="true"][data-attention=true]_&]:bg-accent-surface',
|
|
19
|
+
'[[aria-selected="true"][data-attention=true]_&]:text-accent-surface-text',
|
|
20
|
+
);
|
|
13
21
|
|
|
14
|
-
const presenceIconStyles =
|
|
15
|
-
'is-3 bs-3 group-[[aria-current]_&:hover]/attentionGlyphButton:hidden group-[[aria-current]_&:focus]/attentionGlyphButton:hidden group-[[data-attention=true]_&:hover]/attentionGlyphButton:hidden group-[[data-attention=true]_&:focus]/attentionGlyphButton:hidden group-[[aria-current][data-attention=true]_&:hover]/attentionGlyphButton:hidden group-[[aria-current][data-attention=true]_&:focus]/attentionGlyphButton:hidden';
|
|
22
|
+
const presenceIconStyles = mx('w-3 h-3');
|
|
16
23
|
|
|
17
24
|
const PresenceOne = () => {
|
|
18
25
|
return (
|
|
@@ -87,7 +94,7 @@ export const AttentionGlyph = forwardRef<HTMLSpanElement, AttentionGlyphProps>(
|
|
|
87
94
|
) : null;
|
|
88
95
|
|
|
89
96
|
return (
|
|
90
|
-
<div role='none' className='flex' data-attention={attended} data-contains-attended={containsAttended}>
|
|
97
|
+
<div role='none' className='flex group' data-attention={attended} data-contains-attended={containsAttended}>
|
|
91
98
|
<span role='none' {...props} className={mx(attentionGlyphStyles, classNames)} ref={forwardedRef}>
|
|
92
99
|
{icon}
|
|
93
100
|
</span>
|
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
6
|
import React from 'react';
|
|
8
7
|
|
|
9
|
-
import { withTheme } from '@dxos/
|
|
8
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
9
|
|
|
11
|
-
import { useAttentionAttributes } from './AttentionProvider';
|
|
12
10
|
import { withAttention } from '../testing';
|
|
11
|
+
import { useAttentionAttributes } from './AttentionProvider';
|
|
13
12
|
|
|
14
13
|
const Attendable = ({ id }: { id: string }) => {
|
|
15
14
|
const attentionAttrs = useAttentionAttributes(id);
|
|
16
15
|
|
|
17
16
|
return (
|
|
18
17
|
<div {...attentionAttrs}>
|
|
19
|
-
<textarea className='attention-surface resize-none' placeholder={id} />
|
|
18
|
+
<textarea className='dx-attention-surface resize-none' placeholder={id} />
|
|
20
19
|
</div>
|
|
21
20
|
);
|
|
22
21
|
};
|
|
@@ -32,10 +31,14 @@ const Story = () => {
|
|
|
32
31
|
);
|
|
33
32
|
};
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
const meta = {
|
|
36
35
|
title: 'ui/react-ui-attention/AttentionProvider',
|
|
37
36
|
render: Story,
|
|
38
|
-
decorators: [withTheme, withAttention],
|
|
39
|
-
};
|
|
37
|
+
decorators: [withTheme(), withAttention()],
|
|
38
|
+
} satisfies Meta;
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
|
|
42
|
+
type Story = StoryObj<typeof meta>;
|
|
40
43
|
|
|
41
|
-
export const Default = {};
|
|
44
|
+
export const Default: Story = {};
|
|
@@ -2,58 +2,93 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// eslint-disable-next-line unused-imports/no-unused-imports
|
|
8
|
-
import { createContext, type Scope, type CreateScope } from '@radix-ui/react-context';
|
|
5
|
+
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import { createContext } from '@radix-ui/react-context';
|
|
9
7
|
import { Primitive } from '@radix-ui/react-primitive';
|
|
10
8
|
import { Slot } from '@radix-ui/react-slot';
|
|
11
|
-
import React, {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
import React, {
|
|
10
|
+
type ComponentPropsWithRef,
|
|
11
|
+
type FocusEvent,
|
|
12
|
+
type PropsWithChildren,
|
|
13
|
+
forwardRef,
|
|
14
|
+
useContext,
|
|
15
|
+
useEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useState,
|
|
18
|
+
} from 'react';
|
|
19
|
+
|
|
20
|
+
import { log } from '@dxos/log';
|
|
21
|
+
import { useDefaultValue } from '@dxos/react-hooks';
|
|
22
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
23
|
+
import { mx } from '@dxos/ui-theme';
|
|
24
|
+
|
|
25
|
+
import { AttentionManager, getAttendables } from '../attention';
|
|
26
|
+
import { type Attention } from '../types';
|
|
17
27
|
|
|
18
28
|
const ATTENTION_NAME = 'Attention';
|
|
19
|
-
const
|
|
20
|
-
const ATTENTION_SOURCE_ATTRIBUTE = 'data-
|
|
29
|
+
const ATTENDABLE_ATTRIBUTE = 'data-attendable-id';
|
|
30
|
+
const ATTENTION_SOURCE_ATTRIBUTE = 'data-w-attention-source';
|
|
21
31
|
|
|
22
32
|
type AttentionContextValue = {
|
|
23
33
|
attention: AttentionManager;
|
|
24
|
-
path: string[];
|
|
25
34
|
};
|
|
26
35
|
|
|
27
36
|
const [AttentionContextProvider, useAttentionContext] = createContext<AttentionContextValue>(ATTENTION_NAME, {
|
|
28
|
-
attention:
|
|
29
|
-
path: [],
|
|
37
|
+
attention: undefined as unknown as AttentionManager,
|
|
30
38
|
});
|
|
31
39
|
|
|
32
|
-
const UNKNOWN_ATTENDABLE = { hasAttention: false, isAncestor: false, isRelated: false };
|
|
40
|
+
const UNKNOWN_ATTENDABLE = { hasAttention: false, isAncestor: false, isRelated: false } as Attention;
|
|
33
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to the attention state for a qualified graph ID.
|
|
44
|
+
*/
|
|
34
45
|
const useAttention = (attendableId?: string): Attention => {
|
|
35
|
-
const { attention
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
const { attention } = useAttentionContext(ATTENTION_NAME);
|
|
47
|
+
const [state, setState] = useState<Attention>(UNKNOWN_ATTENDABLE);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!attendableId || !attention) {
|
|
51
|
+
setState(UNKNOWN_ATTENDABLE);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
const currentState = attention.get(attendableId);
|
|
56
|
+
setState(currentState);
|
|
57
|
+
|
|
58
|
+
return attention.subscribe(attendableId, (newState) => {
|
|
59
|
+
setState(newState);
|
|
60
|
+
});
|
|
61
|
+
}, [attention, attendableId]);
|
|
62
|
+
|
|
63
|
+
return state;
|
|
42
64
|
};
|
|
43
65
|
|
|
44
66
|
const useAttended = () => {
|
|
45
67
|
const { attention } = useAttentionContext(ATTENTION_NAME);
|
|
46
|
-
|
|
68
|
+
const [current, setCurrent] = useState<readonly string[]>([]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!attention) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setCurrent(attention.getCurrent());
|
|
76
|
+
|
|
77
|
+
return attention.subscribeCurrent((newCurrent) => {
|
|
78
|
+
setCurrent(newCurrent);
|
|
79
|
+
});
|
|
80
|
+
}, [attention]);
|
|
81
|
+
|
|
82
|
+
return current;
|
|
47
83
|
};
|
|
48
84
|
|
|
49
85
|
/**
|
|
50
|
-
* Computes HTML element attributes to apply so the attention system can detect changes
|
|
51
|
-
* @param attendableId
|
|
86
|
+
* Computes HTML element attributes to apply so the attention system can detect changes.
|
|
52
87
|
*/
|
|
53
88
|
const useAttentionAttributes = (attendableId?: string) => {
|
|
54
89
|
const { hasAttention } = useAttention(attendableId);
|
|
55
90
|
return useMemo(() => {
|
|
56
|
-
const attributes: Record<string, string | undefined> = { [
|
|
91
|
+
const attributes: Record<string, string | undefined> = { [ATTENDABLE_ATTRIBUTE]: attendableId };
|
|
57
92
|
if (hasAttention) {
|
|
58
93
|
attributes[ATTENTION_SOURCE_ATTRIBUTE] = 'true';
|
|
59
94
|
}
|
|
@@ -62,11 +97,6 @@ const useAttentionAttributes = (attendableId?: string) => {
|
|
|
62
97
|
}, [attendableId, hasAttention]);
|
|
63
98
|
};
|
|
64
99
|
|
|
65
|
-
const useAttentionPath = () => {
|
|
66
|
-
const { path } = useAttentionContext(ATTENTION_NAME);
|
|
67
|
-
return path;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
100
|
const RootAttentionProvider = ({
|
|
71
101
|
children,
|
|
72
102
|
attention: propsAttention,
|
|
@@ -77,15 +107,20 @@ const RootAttentionProvider = ({
|
|
|
77
107
|
onChange: (nextAttended: string[]) => void;
|
|
78
108
|
}>
|
|
79
109
|
>) => {
|
|
80
|
-
const
|
|
110
|
+
const registry = useContext(RegistryContext);
|
|
111
|
+
const attention = useDefaultValue(propsAttention, () => new AttentionManager(registry));
|
|
112
|
+
|
|
81
113
|
const handleFocus = (event: FocusEvent) => {
|
|
114
|
+
// NOTE(thure): Use the following to debug focus movement across the app:
|
|
115
|
+
log('focus', { related: event.relatedTarget, target: event.target });
|
|
116
|
+
|
|
82
117
|
const selector = [
|
|
83
118
|
'[data-attendable-id]',
|
|
84
119
|
...Array.from(document.querySelectorAll('[aria-controls]')).map(
|
|
85
120
|
(el) => `[id="${el.getAttribute('aria-controls')}"]`,
|
|
86
121
|
),
|
|
87
122
|
].join(',');
|
|
88
|
-
const prev = attention.
|
|
123
|
+
const prev = attention.getCurrent();
|
|
89
124
|
const next = getAttendables(selector, event.target);
|
|
90
125
|
// TODO(wittjosiah): Not allowing empty state means that the attended item is not strictly guaranteed to be in the DOM.
|
|
91
126
|
// Currently this depends on the deck in order to ensure that when the attended item is removed something else is attended.
|
|
@@ -96,103 +131,51 @@ const RootAttentionProvider = ({
|
|
|
96
131
|
}
|
|
97
132
|
};
|
|
98
133
|
|
|
99
|
-
// NOTE(thure): Use the following to debug the macOS package issue #8540
|
|
100
|
-
|
|
101
|
-
// const [startEl, setStartEl] = useState<HTMLElement | null>(null);
|
|
102
|
-
// const [endEl, setEndEl] = useState<HTMLElement | null>(null);
|
|
103
|
-
//
|
|
104
|
-
// const handleEventDebug = useCallback((event: any) => {
|
|
105
|
-
// console.log(`[${event.type}]`, event.target, event.currentTarget, [event.clientX, event.clientY]);
|
|
106
|
-
// }, []);
|
|
107
|
-
//
|
|
108
|
-
// const handleStartDebug = useCallback(
|
|
109
|
-
// (event: any) => {
|
|
110
|
-
// setStartEl(event.target);
|
|
111
|
-
// handleEventDebug(event);
|
|
112
|
-
// },
|
|
113
|
-
// [handleEventDebug],
|
|
114
|
-
// );
|
|
115
|
-
//
|
|
116
|
-
// const handleEndDebug = useCallback(
|
|
117
|
-
// (event: any) => {
|
|
118
|
-
// setEndEl(event.target);
|
|
119
|
-
// handleEventDebug(event);
|
|
120
|
-
// },
|
|
121
|
-
// [handleEventDebug],
|
|
122
|
-
// );
|
|
123
|
-
//
|
|
124
|
-
// const handleClickDebug = useCallback(
|
|
125
|
-
// (event: any) => {
|
|
126
|
-
// console.log('[click compare]', startEl, endEl, startEl === endEl);
|
|
127
|
-
// handleEventDebug(event);
|
|
128
|
-
// },
|
|
129
|
-
// [startEl, endEl, handleEventDebug],
|
|
130
|
-
// );
|
|
131
|
-
|
|
132
134
|
return (
|
|
133
|
-
<AttentionContextProvider attention={attention}
|
|
134
|
-
<div
|
|
135
|
-
role='none'
|
|
136
|
-
className='contents'
|
|
137
|
-
onFocusCapture={handleFocus}
|
|
138
|
-
// onClick={handleClickDebug}
|
|
139
|
-
// onMouseDown={handleStartDebug}
|
|
140
|
-
// onMouseUp={handleEndDebug}
|
|
141
|
-
>
|
|
135
|
+
<AttentionContextProvider attention={attention}>
|
|
136
|
+
<div role='none' className='contents' onFocusCapture={handleFocus}>
|
|
142
137
|
{children}
|
|
143
138
|
</div>
|
|
144
139
|
</AttentionContextProvider>
|
|
145
140
|
);
|
|
146
141
|
};
|
|
147
142
|
|
|
148
|
-
const AttentionProvider = ({ id, children }: PropsWithChildren<{ id: string }>) => {
|
|
149
|
-
const { attention, path } = useAttentionContext(ATTENTION_NAME);
|
|
150
|
-
const nextPath = useMemo(() => [id, ...path], [id, path]);
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<AttentionContextProvider attention={attention} path={nextPath}>
|
|
154
|
-
{children}
|
|
155
|
-
</AttentionContextProvider>
|
|
156
|
-
);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
143
|
export type AttendableContainerProps = ThemedClassName<
|
|
160
144
|
ComponentPropsWithRef<'div'> & { id: string; asChild?: boolean }
|
|
161
145
|
>;
|
|
162
146
|
|
|
163
147
|
/**
|
|
164
|
-
* Note that DeckPlugin and StackPlugin both handle attention on their own,
|
|
165
|
-
* is not necessary to also render an `AttendableContainer`.
|
|
166
|
-
*
|
|
148
|
+
* Note that DeckPlugin and StackPlugin both handle attention on their own,
|
|
149
|
+
* and when rendering content in those cases it is not necessary to also render an `AttendableContainer`.
|
|
150
|
+
* This component is primarily for Storybook stories and other testing scenarios,
|
|
151
|
+
* or the rare cases where an attendable entity is rendered outside of either of those plugins.
|
|
167
152
|
*/
|
|
168
153
|
const AttendableContainer = forwardRef<HTMLDivElement, AttendableContainerProps>(
|
|
169
154
|
({ id, classNames, children, asChild, ...props }, forwardedRef) => {
|
|
170
155
|
const attentionAttrs = useAttentionAttributes(id);
|
|
171
|
-
const
|
|
156
|
+
const Comp = asChild ? Slot : Primitive.div;
|
|
172
157
|
return (
|
|
173
|
-
<
|
|
158
|
+
<Comp
|
|
174
159
|
role='none'
|
|
175
|
-
{...attentionAttrs}
|
|
176
160
|
{...props}
|
|
177
|
-
|
|
161
|
+
{...attentionAttrs}
|
|
162
|
+
className={mx('dx-attention-surface', props.tabIndex === 0 && 'dx-focus-ring-inset-over-all', classNames)}
|
|
178
163
|
ref={forwardedRef}
|
|
179
164
|
>
|
|
180
165
|
{children}
|
|
181
|
-
</
|
|
166
|
+
</Comp>
|
|
182
167
|
);
|
|
183
168
|
},
|
|
184
169
|
);
|
|
185
170
|
|
|
186
171
|
export {
|
|
187
172
|
RootAttentionProvider,
|
|
188
|
-
AttentionProvider,
|
|
189
173
|
AttendableContainer,
|
|
190
174
|
useAttentionContext,
|
|
191
175
|
useAttention,
|
|
192
176
|
useAttended,
|
|
193
177
|
useAttentionAttributes,
|
|
194
|
-
useAttentionPath,
|
|
195
178
|
ATTENTION_NAME,
|
|
196
|
-
|
|
179
|
+
ATTENDABLE_ATTRIBUTE,
|
|
197
180
|
ATTENTION_SOURCE_ATTRIBUTE,
|
|
198
181
|
};
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { RegistryContext } from '@effect-atom/atom-react';
|
|
5
6
|
import { createContext } from '@radix-ui/react-context';
|
|
6
|
-
import
|
|
7
|
-
import React, { useCallback, useMemo,
|
|
7
|
+
import * as Match from 'effect/Match';
|
|
8
|
+
import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
8
9
|
|
|
9
|
-
import { useDefaultValue } from '@dxos/react-
|
|
10
|
+
import { useDefaultValue } from '@dxos/react-hooks';
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
|
-
defaultSelection,
|
|
13
|
-
SelectionManager,
|
|
14
|
-
type SelectionResult,
|
|
15
13
|
type Selection,
|
|
14
|
+
SelectionManager,
|
|
16
15
|
type SelectionMode,
|
|
16
|
+
type SelectionResult,
|
|
17
|
+
defaultSelection,
|
|
17
18
|
} from '../selection';
|
|
18
19
|
|
|
19
20
|
const SELECTION_NAME = 'Selection';
|
|
@@ -23,7 +24,7 @@ type SelectionContextValue = {
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const [SelectionContextProvider, useSelectionContext] = createContext<SelectionContextValue>(SELECTION_NAME, {
|
|
26
|
-
selection:
|
|
27
|
+
selection: undefined as unknown as SelectionManager,
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -34,7 +35,8 @@ export const SelectionProvider = ({
|
|
|
34
35
|
children,
|
|
35
36
|
selection: propsSelection,
|
|
36
37
|
}: PropsWithChildren<{ selection?: SelectionManager }>) => {
|
|
37
|
-
const
|
|
38
|
+
const registry = useContext(RegistryContext);
|
|
39
|
+
const selection = useDefaultValue(propsSelection, () => new SelectionManager(registry));
|
|
38
40
|
return <SelectionContextProvider selection={selection}>{children}</SelectionContextProvider>;
|
|
39
41
|
};
|
|
40
42
|
|
|
@@ -46,6 +48,18 @@ export const useSelectionManager = () => {
|
|
|
46
48
|
return selection;
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
const getDefaultResult = <T extends SelectionMode>(mode: T): SelectionResult<T> => {
|
|
52
|
+
return Match.type<Selection>().pipe(
|
|
53
|
+
Match.when({ mode: 'single' }, (selection) => selection.id),
|
|
54
|
+
Match.when({ mode: 'multi' }, (selection) => selection.ids),
|
|
55
|
+
Match.when({ mode: 'range' }, (selection) =>
|
|
56
|
+
selection.from && selection.to ? { from: selection.from, to: selection.to } : undefined,
|
|
57
|
+
),
|
|
58
|
+
Match.when({ mode: 'multi-range' }, (selection) => selection.ranges),
|
|
59
|
+
Match.exhaustive,
|
|
60
|
+
)(defaultSelection(mode)) as SelectionResult<T>;
|
|
61
|
+
};
|
|
62
|
+
|
|
49
63
|
/**
|
|
50
64
|
* Get the selected objects for a given context.
|
|
51
65
|
*/
|
|
@@ -54,28 +68,49 @@ export const useSelected = <T extends SelectionMode>(
|
|
|
54
68
|
mode: T = 'multi' as T,
|
|
55
69
|
): SelectionResult<T> => {
|
|
56
70
|
const { selection } = useSelectionContext(SELECTION_NAME);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
const [state, setState] = useState<SelectionResult<T>>(() =>
|
|
72
|
+
contextId && selection ? selection.getSelected(contextId, mode) : getDefaultResult(mode),
|
|
73
|
+
);
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!contextId || !selection) {
|
|
77
|
+
setState(getDefaultResult(mode));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Set initial state.
|
|
82
|
+
setState(selection.getSelected(contextId, mode));
|
|
83
|
+
|
|
84
|
+
// Subscribe to changes.
|
|
85
|
+
return selection.subscribe(() => {
|
|
86
|
+
setState(selection.getSelected(contextId, mode));
|
|
87
|
+
});
|
|
88
|
+
}, [selection, contextId, mode]);
|
|
89
|
+
|
|
90
|
+
return state;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type UseSelectionActions = {
|
|
94
|
+
singleSelect: (id: string) => void;
|
|
95
|
+
multiSelect: (ids: string[]) => void;
|
|
96
|
+
rangeSelect: (from: string, to: string) => void;
|
|
97
|
+
toggle: (id: string) => void;
|
|
98
|
+
clear: () => void;
|
|
68
99
|
};
|
|
69
100
|
|
|
70
101
|
/**
|
|
71
102
|
* Provides functions to manage the selection state for multiple contexts.
|
|
72
103
|
*/
|
|
73
|
-
|
|
104
|
+
// TODO(burdon): Mode not used.
|
|
105
|
+
export const useSelectionActions = (contextIds: string[], mode: SelectionMode = 'multi'): UseSelectionActions => {
|
|
74
106
|
const stableContextIds = useMemo(() => contextIds, [JSON.stringify(contextIds)]); // TODO(burdon): Avoid stringify.
|
|
75
107
|
const { selection } = useSelectionContext(SELECTION_NAME);
|
|
76
108
|
|
|
77
109
|
const singleSelect = useCallback(
|
|
78
110
|
(id: string) => {
|
|
111
|
+
if (!selection) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
79
114
|
for (const contextId of stableContextIds) {
|
|
80
115
|
selection.updateSingle(contextId, id);
|
|
81
116
|
}
|
|
@@ -85,6 +120,9 @@ export const useSelectionActions = (contextIds: string[], mode: SelectionMode =
|
|
|
85
120
|
|
|
86
121
|
const multiSelect = useCallback(
|
|
87
122
|
(ids: string[]) => {
|
|
123
|
+
if (!selection) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
88
126
|
for (const contextId of stableContextIds) {
|
|
89
127
|
selection.updateMulti(contextId, ids);
|
|
90
128
|
}
|
|
@@ -94,6 +132,9 @@ export const useSelectionActions = (contextIds: string[], mode: SelectionMode =
|
|
|
94
132
|
|
|
95
133
|
const rangeSelect = useCallback(
|
|
96
134
|
(from: string, to: string) => {
|
|
135
|
+
if (!selection) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
97
138
|
for (const contextId of stableContextIds) {
|
|
98
139
|
selection.updateRange(contextId, from, to);
|
|
99
140
|
}
|
|
@@ -103,6 +144,9 @@ export const useSelectionActions = (contextIds: string[], mode: SelectionMode =
|
|
|
103
144
|
|
|
104
145
|
const toggle = useCallback(
|
|
105
146
|
(id: string) => {
|
|
147
|
+
if (!selection) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
106
150
|
for (const contextId of stableContextIds) {
|
|
107
151
|
selection.toggleSelection(contextId, id);
|
|
108
152
|
}
|
|
@@ -111,6 +155,9 @@ export const useSelectionActions = (contextIds: string[], mode: SelectionMode =
|
|
|
111
155
|
);
|
|
112
156
|
|
|
113
157
|
const clear = useCallback(() => {
|
|
158
|
+
if (!selection) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
114
161
|
for (const contextId of stableContextIds) {
|
|
115
162
|
selection.clearSelection(contextId);
|
|
116
163
|
}
|