@dxos/react-ui-attention 0.8.4-main.ead640a → 0.8.4-main.f466a3d56e
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/LICENSE +102 -5
- package/dist/lib/browser/chunk-D2BLVJ6Z.mjs +421 -0
- package/dist/lib/browser/chunk-D2BLVJ6Z.mjs.map +7 -0
- package/dist/lib/browser/chunk-I4VO2RQM.mjs +255 -0
- package/dist/lib/browser/chunk-I4VO2RQM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +89 -23
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +22 -5
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +29 -0
- package/dist/lib/browser/types/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-6INLBC62.mjs +423 -0
- package/dist/lib/node-esm/chunk-6INLBC62.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-ZTUVSH4P.mjs +256 -0
- package/dist/lib/node-esm/chunk-ZTUVSH4P.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +89 -23
- 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 +22 -5
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +30 -0
- package/dist/lib/node-esm/types/index.mjs.map +7 -0
- package/dist/types/src/attention.d.ts +60 -20
- package/dist/types/src/attention.d.ts.map +1 -1
- package/dist/types/src/components/AttentionGlyph/AttentionGlyph.d.ts.map +1 -0
- package/dist/types/src/components/AttentionGlyph/AttentionGlyph.stories.d.ts.map +1 -0
- package/dist/types/src/components/AttentionGlyph/index.d.ts +2 -0
- package/dist/types/src/components/AttentionGlyph/index.d.ts.map +1 -0
- package/dist/types/src/components/AttentionProvider/AttentionProvider.d.ts +34 -0
- package/dist/types/src/components/AttentionProvider/AttentionProvider.d.ts.map +1 -0
- package/dist/types/src/components/AttentionProvider/AttentionProvider.stories.d.ts.map +1 -0
- package/dist/types/src/components/AttentionProvider/index.d.ts +2 -0
- package/dist/types/src/components/AttentionProvider/index.d.ts.map +1 -0
- package/dist/types/src/components/{SelectionProvider.d.ts → SelectionProvider/SelectionProvider.d.ts} +7 -6
- package/dist/types/src/components/SelectionProvider/SelectionProvider.d.ts.map +1 -0
- package/dist/types/src/components/SelectionProvider/index.d.ts +2 -0
- package/dist/types/src/components/SelectionProvider/index.d.ts.map +1 -0
- package/dist/types/src/selection.d.ts +23 -3
- 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/index.d.ts +3 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -32
- package/src/attention.test.ts +188 -27
- package/src/attention.ts +179 -75
- package/src/components/{AttentionGlyph.stories.tsx → AttentionGlyph/AttentionGlyph.stories.tsx} +2 -2
- package/src/components/{AttentionGlyph.tsx → AttentionGlyph/AttentionGlyph.tsx} +13 -6
- package/src/components/AttentionGlyph/index.ts +5 -0
- package/src/components/{AttentionProvider.stories.tsx → AttentionProvider/AttentionProvider.stories.tsx} +3 -4
- package/src/components/AttentionProvider/AttentionProvider.tsx +176 -0
- package/src/components/AttentionProvider/index.ts +5 -0
- package/src/components/SelectionProvider/SelectionProvider.tsx +174 -0
- package/src/components/SelectionProvider/index.ts +5 -0
- package/src/selection.ts +121 -79
- package/src/testing/decorators/withAttention.ts +22 -4
- package/src/types/index.ts +9 -0
- package/dist/lib/browser/chunk-HELLA3BH.mjs +0 -675
- package/dist/lib/browser/chunk-HELLA3BH.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FBZV7QG7.mjs +0 -677
- package/dist/lib/node-esm/chunk-FBZV7QG7.mjs.map +0 -7
- package/dist/types/src/components/AttentionGlyph.d.ts.map +0 -1
- package/dist/types/src/components/AttentionGlyph.stories.d.ts.map +0 -1
- package/dist/types/src/components/AttentionProvider.d.ts +0 -38
- package/dist/types/src/components/AttentionProvider.d.ts.map +0 -1
- package/dist/types/src/components/AttentionProvider.stories.d.ts.map +0 -1
- package/dist/types/src/components/SelectionProvider.d.ts.map +0 -1
- package/src/components/AttentionProvider.tsx +0 -201
- package/src/components/SelectionProvider.tsx +0 -120
- /package/dist/types/src/components/{AttentionGlyph.d.ts → AttentionGlyph/AttentionGlyph.d.ts} +0 -0
- /package/dist/types/src/components/{AttentionGlyph.stories.d.ts → AttentionGlyph/AttentionGlyph.stories.d.ts} +0 -0
- /package/dist/types/src/components/{AttentionProvider.stories.d.ts → AttentionProvider/AttentionProvider.stories.d.ts} +0 -0
|
@@ -6,13 +6,20 @@ import { type Primitive } from '@radix-ui/react-primitive';
|
|
|
6
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-foreground',
|
|
16
|
+
'[[aria-current][data-attention=true]_&]:bg-accent-surface',
|
|
17
|
+
'[[aria-current][data-attention=true]_&]:text-accent-foreground',
|
|
18
|
+
'[[aria-selected="true"][data-attention=true]_&]:bg-accent-surface',
|
|
19
|
+
'[[aria-selected="true"][data-attention=true]_&]:text-accent-foreground',
|
|
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
|
|
97
|
+
<div 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>
|
|
@@ -7,8 +7,7 @@ import React from 'react';
|
|
|
7
7
|
|
|
8
8
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
9
9
|
|
|
10
|
-
import { withAttention } from '
|
|
11
|
-
|
|
10
|
+
import { withAttention } from '../../testing';
|
|
12
11
|
import { useAttentionAttributes } from './AttentionProvider';
|
|
13
12
|
|
|
14
13
|
const Attendable = ({ id }: { id: string }) => {
|
|
@@ -16,7 +15,7 @@ const Attendable = ({ id }: { id: string }) => {
|
|
|
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
|
};
|
|
@@ -35,7 +34,7 @@ const Story = () => {
|
|
|
35
34
|
const meta = {
|
|
36
35
|
title: 'ui/react-ui-attention/AttentionProvider',
|
|
37
36
|
render: Story,
|
|
38
|
-
decorators: [withTheme, withAttention],
|
|
37
|
+
decorators: [withTheme(), withAttention()],
|
|
39
38
|
} satisfies Meta;
|
|
40
39
|
|
|
41
40
|
export default meta;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import { createContext } from '@radix-ui/react-context';
|
|
7
|
+
import { Primitive } from '@radix-ui/react-primitive';
|
|
8
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
9
|
+
import React, {
|
|
10
|
+
type ComponentPropsWithRef,
|
|
11
|
+
type FocusEvent,
|
|
12
|
+
type PropsWithChildren,
|
|
13
|
+
forwardRef,
|
|
14
|
+
useCallback,
|
|
15
|
+
useContext,
|
|
16
|
+
useEffect,
|
|
17
|
+
useMemo,
|
|
18
|
+
useState,
|
|
19
|
+
} from 'react';
|
|
20
|
+
|
|
21
|
+
import { log } from '@dxos/log';
|
|
22
|
+
import { useDefaultValue } from '@dxos/react-hooks';
|
|
23
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
24
|
+
import { mx } from '@dxos/ui-theme';
|
|
25
|
+
|
|
26
|
+
import { type Attention, AttentionManager, getAttendables } from '../../attention';
|
|
27
|
+
|
|
28
|
+
const ATTENTION_NAME = 'Attention';
|
|
29
|
+
const ATTENDABLE_ATTRIBUTE = 'data-attendable-id';
|
|
30
|
+
const ATTENTION_SOURCE_ATTRIBUTE = 'data-w-attention-source';
|
|
31
|
+
|
|
32
|
+
type AttentionContextValue = {
|
|
33
|
+
attention: AttentionManager;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const [AttentionContextProvider, useAttentionContext] = createContext<AttentionContextValue>(ATTENTION_NAME, {
|
|
37
|
+
attention: undefined as unknown as AttentionManager,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const UNKNOWN_ATTENDABLE = { hasAttention: false, isAncestor: false, isRelated: false } as Attention;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to the attention state for a qualified graph ID.
|
|
44
|
+
*/
|
|
45
|
+
// TODO(burdon): Unify with seleciton and change to contextId?
|
|
46
|
+
const useAttention = (attendableId?: string): Attention => {
|
|
47
|
+
const { attention } = useAttentionContext(ATTENTION_NAME);
|
|
48
|
+
const [state, setState] = useState<Attention>(UNKNOWN_ATTENDABLE);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!attendableId || !attention) {
|
|
51
|
+
setState(UNKNOWN_ATTENDABLE);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
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;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const useAttended = () => {
|
|
67
|
+
const { attention } = useAttentionContext(ATTENTION_NAME);
|
|
68
|
+
const [current, setCurrent] = useState<readonly string[]>([]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!attention) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setCurrent(attention.getCurrent());
|
|
75
|
+
|
|
76
|
+
return attention.subscribeCurrent((newCurrent) => {
|
|
77
|
+
setCurrent(newCurrent);
|
|
78
|
+
});
|
|
79
|
+
}, [attention]);
|
|
80
|
+
|
|
81
|
+
return current;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Computes HTML element attributes to apply so the attention system can detect changes.
|
|
86
|
+
*/
|
|
87
|
+
const useAttentionAttributes = (attendableId?: string) => {
|
|
88
|
+
const { hasAttention } = useAttention(attendableId);
|
|
89
|
+
return useMemo(() => {
|
|
90
|
+
const attributes: Record<string, string | undefined> = { [ATTENDABLE_ATTRIBUTE]: attendableId };
|
|
91
|
+
if (hasAttention) {
|
|
92
|
+
attributes[ATTENTION_SOURCE_ATTRIBUTE] = 'true';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return attributes;
|
|
96
|
+
}, [attendableId, hasAttention]);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type RootAttentionProviderProps = PropsWithChildren<{
|
|
100
|
+
attention?: AttentionManager;
|
|
101
|
+
onChange?: (nextAttended: string[]) => void;
|
|
102
|
+
}>;
|
|
103
|
+
|
|
104
|
+
const RootAttentionProvider = ({ children, attention: propsAttention, onChange }: RootAttentionProviderProps) => {
|
|
105
|
+
const registry = useContext(RegistryContext);
|
|
106
|
+
const attention = useDefaultValue(propsAttention, () => new AttentionManager(registry));
|
|
107
|
+
|
|
108
|
+
const handleFocus = useCallback(
|
|
109
|
+
(event: FocusEvent) => {
|
|
110
|
+
// NOTE(thure): Use the following to debug focus movement across the app:
|
|
111
|
+
log('focus', { related: event.relatedTarget, target: event.target });
|
|
112
|
+
|
|
113
|
+
const selector = [
|
|
114
|
+
'[data-attendable-id]',
|
|
115
|
+
...Array.from(document.querySelectorAll('[aria-controls]')).map(
|
|
116
|
+
(el) => `[id="${el.getAttribute('aria-controls')}"]`,
|
|
117
|
+
),
|
|
118
|
+
].join(',');
|
|
119
|
+
const prev = attention.getCurrent();
|
|
120
|
+
const next = getAttendables(selector, event.target);
|
|
121
|
+
// TODO(wittjosiah): Not allowing empty state means that the attended item is not strictly guaranteed to be in the DOM.
|
|
122
|
+
// Currently this depends on the deck in order to ensure that when the attended item is removed something else is attended.
|
|
123
|
+
// Only update state if the result is different and not empty.
|
|
124
|
+
if (next.length > 0 && (prev.length !== next.length || !!prev.find((id, index) => next[index] !== id))) {
|
|
125
|
+
attention.update(next);
|
|
126
|
+
onChange?.(next);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[attention, onChange],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<AttentionContextProvider attention={attention}>
|
|
134
|
+
<div className='contents' onFocusCapture={handleFocus}>
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
</AttentionContextProvider>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type AttendableContainerProps = ThemedClassName<
|
|
142
|
+
ComponentPropsWithRef<'div'> & { id: string; asChild?: boolean }
|
|
143
|
+
>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Note that DeckPlugin and StackPlugin both handle attention on their own,
|
|
147
|
+
* and when rendering content in those cases it is not necessary to also render an `AttendableContainer`.
|
|
148
|
+
* This component is primarily for Storybook stories and other testing scenarios,
|
|
149
|
+
* or the rare cases where an attendable entity is rendered outside of either of those plugins.
|
|
150
|
+
*/
|
|
151
|
+
const AttendableContainer = forwardRef<HTMLDivElement, AttendableContainerProps>(
|
|
152
|
+
({ id, classNames, children, asChild, ...props }, forwardedRef) => {
|
|
153
|
+
const attentionAttrs = useAttentionAttributes(id);
|
|
154
|
+
const Comp = asChild ? Slot : Primitive.div;
|
|
155
|
+
return (
|
|
156
|
+
<Comp
|
|
157
|
+
role='none'
|
|
158
|
+
{...props}
|
|
159
|
+
{...attentionAttrs}
|
|
160
|
+
className={mx('dx-attention-surface', props.tabIndex === 0 && 'dx-focus-ring-inset-over-all', classNames)}
|
|
161
|
+
ref={forwardedRef}
|
|
162
|
+
>
|
|
163
|
+
{children}
|
|
164
|
+
</Comp>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
RootAttentionProvider,
|
|
171
|
+
AttendableContainer,
|
|
172
|
+
useAttentionContext,
|
|
173
|
+
useAttention,
|
|
174
|
+
useAttended,
|
|
175
|
+
useAttentionAttributes,
|
|
176
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import { createContext } from '@radix-ui/react-context';
|
|
7
|
+
import * as Match from 'effect/Match';
|
|
8
|
+
import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { useDefaultValue } from '@dxos/react-hooks';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type Selection,
|
|
14
|
+
SelectionManager,
|
|
15
|
+
type SelectionMode,
|
|
16
|
+
type SelectionResult,
|
|
17
|
+
defaultSelection,
|
|
18
|
+
} from '../../selection';
|
|
19
|
+
|
|
20
|
+
const SELECTION_NAME = 'Selection';
|
|
21
|
+
|
|
22
|
+
type SelectionContextValue = {
|
|
23
|
+
selection: SelectionManager;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Default value lets consumers like `useSelected` render outside a
|
|
27
|
+
// `SelectionProvider` (e.g. isolated stories) without throwing —
|
|
28
|
+
// `selection` reads as `undefined` and the hook falls back to the
|
|
29
|
+
// per-mode default result. Without this default, Radix `createContext`
|
|
30
|
+
// throws "Selection must be used within Selection" on every consumer.
|
|
31
|
+
const [SelectionContextProvider, useSelectionContext] = createContext<SelectionContextValue>(SELECTION_NAME, {
|
|
32
|
+
selection: undefined as unknown as SelectionManager,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Manages selection state across the app for multiple contexts.
|
|
37
|
+
*/
|
|
38
|
+
export const SelectionProvider = ({
|
|
39
|
+
children,
|
|
40
|
+
selection: selectionProp,
|
|
41
|
+
}: PropsWithChildren<{ selection?: SelectionManager }>) => {
|
|
42
|
+
const registry = useContext(RegistryContext);
|
|
43
|
+
const selection = useDefaultValue(selectionProp, () => new SelectionManager(registry));
|
|
44
|
+
|
|
45
|
+
return <SelectionContextProvider selection={selection}>{children}</SelectionContextProvider>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the selection contexts.
|
|
50
|
+
*/
|
|
51
|
+
export const useSelectionManager = (): SelectionManager => {
|
|
52
|
+
const { selection } = useSelectionContext(SELECTION_NAME);
|
|
53
|
+
return selection;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getDefaultResult = <T extends SelectionMode>(mode: T): SelectionResult<T> => {
|
|
57
|
+
return Match.type<Selection>().pipe(
|
|
58
|
+
Match.when({ mode: 'single' }, (selection) => selection.id),
|
|
59
|
+
Match.when({ mode: 'multi' }, (selection) => selection.ids),
|
|
60
|
+
Match.when({ mode: 'range' }, (selection) =>
|
|
61
|
+
selection.from && selection.to ? { from: selection.from, to: selection.to } : undefined,
|
|
62
|
+
),
|
|
63
|
+
Match.when({ mode: 'multi-range' }, (selection) => selection.ranges),
|
|
64
|
+
Match.exhaustive,
|
|
65
|
+
)(defaultSelection(mode)) as SelectionResult<T>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the selected objects for a given context.
|
|
70
|
+
*/
|
|
71
|
+
export const useSelected = <T extends SelectionMode>(
|
|
72
|
+
contextId?: string,
|
|
73
|
+
mode: T = 'multi' as T,
|
|
74
|
+
): SelectionResult<T> => {
|
|
75
|
+
const { selection } = useSelectionContext(SELECTION_NAME);
|
|
76
|
+
const [state, setState] = useState<SelectionResult<T>>(() =>
|
|
77
|
+
contextId && selection ? selection.getSelected(contextId, mode) : getDefaultResult(mode),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!contextId || !selection) {
|
|
82
|
+
setState(getDefaultResult(mode));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Set initial state.
|
|
87
|
+
setState(selection.getSelected(contextId, mode));
|
|
88
|
+
|
|
89
|
+
// Subscribe to changes.
|
|
90
|
+
return selection.subscribe(() => {
|
|
91
|
+
setState(selection.getSelected(contextId, mode));
|
|
92
|
+
});
|
|
93
|
+
}, [selection, contextId, mode]);
|
|
94
|
+
|
|
95
|
+
return state;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type UseSelectionActions = {
|
|
99
|
+
singleSelect: (id: string) => void;
|
|
100
|
+
multiSelect: (ids: string[]) => void;
|
|
101
|
+
rangeSelect: (from: string, to: string) => void;
|
|
102
|
+
toggle: (id: string) => void;
|
|
103
|
+
clear: () => void;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Provides functions to manage the selection state for multiple contexts.
|
|
108
|
+
*/
|
|
109
|
+
export const useSelectionActions = (contextIds: string[]): UseSelectionActions => {
|
|
110
|
+
const stableContextIds = useMemo(() => contextIds, [JSON.stringify(contextIds)]);
|
|
111
|
+
const { selection } = useSelectionContext(SELECTION_NAME);
|
|
112
|
+
|
|
113
|
+
// No-op when rendered outside a `SelectionProvider` (e.g. an isolated
|
|
114
|
+
// story or a test harness) — `selection` is `undefined` from the
|
|
115
|
+
// context default value. Matches the pre-co-locate behaviour.
|
|
116
|
+
const singleSelect = useCallback(
|
|
117
|
+
(id: string) => {
|
|
118
|
+
if (!selection) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
for (const contextId of stableContextIds) {
|
|
122
|
+
selection.updateSingle(contextId, id);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[selection, stableContextIds],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const multiSelect = useCallback(
|
|
129
|
+
(ids: string[]) => {
|
|
130
|
+
if (!selection) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
for (const contextId of stableContextIds) {
|
|
134
|
+
selection.updateMulti(contextId, ids);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
[selection, stableContextIds],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const rangeSelect = useCallback(
|
|
141
|
+
(from: string, to: string) => {
|
|
142
|
+
if (!selection) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const contextId of stableContextIds) {
|
|
146
|
+
selection.updateRange(contextId, from, to);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
[selection, stableContextIds],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const toggle = useCallback(
|
|
153
|
+
(id: string) => {
|
|
154
|
+
if (!selection) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
for (const contextId of stableContextIds) {
|
|
158
|
+
selection.toggleSelection(contextId, id);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
[selection, stableContextIds],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const clear = useCallback(() => {
|
|
165
|
+
if (!selection) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const contextId of stableContextIds) {
|
|
169
|
+
selection.clearSelection(contextId);
|
|
170
|
+
}
|
|
171
|
+
}, [selection, stableContextIds]);
|
|
172
|
+
|
|
173
|
+
return { singleSelect, multiSelect, rangeSelect, toggle, clear };
|
|
174
|
+
};
|