@dxos/react-ui-attention 0.8.4-main.b97322e → 0.8.4-main.bcb3aa67d6

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.
Files changed (58) hide show
  1. package/dist/lib/browser/chunk-4SEURHOL.mjs +721 -0
  2. package/dist/lib/browser/chunk-4SEURHOL.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-GMKHOVJ5.mjs +1 -0
  4. package/dist/lib/browser/chunk-GMKHOVJ5.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +85 -13
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +20 -5
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types.mjs +2 -0
  11. package/dist/lib/browser/types.mjs.map +7 -0
  12. package/dist/lib/node-esm/chunk-6PQ4P7AL.mjs +723 -0
  13. package/dist/lib/node-esm/chunk-6PQ4P7AL.mjs.map +7 -0
  14. package/dist/lib/node-esm/chunk-UHVMYYIB.mjs +2 -0
  15. package/dist/lib/node-esm/chunk-UHVMYYIB.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +85 -13
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/testing/index.mjs +20 -5
  20. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  21. package/dist/lib/node-esm/types.mjs +3 -0
  22. package/dist/lib/node-esm/types.mjs.map +7 -0
  23. package/dist/types/src/attention.d.ts +56 -20
  24. package/dist/types/src/attention.d.ts.map +1 -1
  25. package/dist/types/src/components/AttentionGlyph.d.ts.map +1 -1
  26. package/dist/types/src/components/AttentionGlyph.stories.d.ts +9 -20
  27. package/dist/types/src/components/AttentionGlyph.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/AttentionProvider.d.ts +15 -16
  29. package/dist/types/src/components/AttentionProvider.d.ts.map +1 -1
  30. package/dist/types/src/components/AttentionProvider.stories.d.ts +6 -4
  31. package/dist/types/src/components/AttentionProvider.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/SelectionProvider.d.ts +6 -5
  33. package/dist/types/src/components/SelectionProvider.d.ts.map +1 -1
  34. package/dist/types/src/index.d.ts +1 -0
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/selection.d.ts +24 -4
  37. package/dist/types/src/selection.d.ts.map +1 -1
  38. package/dist/types/src/testing/decorators/withAttention.d.ts +5 -1
  39. package/dist/types/src/testing/decorators/withAttention.d.ts.map +1 -1
  40. package/dist/types/src/types.d.ts +9 -0
  41. package/dist/types/src/types.d.ts.map +1 -0
  42. package/dist/types/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +38 -24
  44. package/src/attention.test.ts +188 -27
  45. package/src/attention.ts +175 -74
  46. package/src/components/AttentionGlyph.stories.tsx +17 -14
  47. package/src/components/AttentionGlyph.tsx +14 -7
  48. package/src/components/AttentionProvider.stories.tsx +13 -9
  49. package/src/components/AttentionProvider.tsx +80 -97
  50. package/src/components/SelectionProvider.tsx +66 -19
  51. package/src/index.ts +1 -0
  52. package/src/selection.ts +113 -72
  53. package/src/testing/decorators/withAttention.ts +21 -4
  54. package/src/types.ts +13 -0
  55. package/dist/lib/browser/chunk-3K54PTHR.mjs +0 -650
  56. package/dist/lib/browser/chunk-3K54PTHR.mjs.map +0 -7
  57. package/dist/lib/node-esm/chunk-LVRGGYEI.mjs +0 -652
  58. package/dist/lib/node-esm/chunk-LVRGGYEI.mjs.map +0 -7
@@ -2,19 +2,18 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
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/storybook-utils';
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 Story = (props: AttentionGlyphProps) => {
14
+ const DefaultStory = (props: AttentionGlyphProps) => {
16
15
  return (
17
- <ul className='flex gap-2 mbe-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
- export default {
30
+ const meta = {
32
31
  title: 'ui/react-ui-attention/AttentionGlyph',
33
- component: AttentionGlyph,
34
- render: Story,
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 ComponentPropsWithRef, type CSSProperties, forwardRef, useMemo } from 'react';
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/react-ui-theme';
9
+ import { mx } from '@dxos/ui-theme';
10
10
 
11
- const attentionGlyphStyles =
12
- 'inline-block rounded-sm is-3 bs-3 bg-transparent text-accentText transition-colors [[data-contains-attended=true]_&]:bg-attentionRelated [[data-attention=true]_&]:bg-accentSurface [[data-attention=true]_&]:text-accentSurfaceText [[aria-current][data-attention=true]_&]:bg-accentSurface [[aria-current][data-attention=true]_&]:text-accentSurfaceText';
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,21 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
6
  import React from 'react';
8
7
 
9
- import { withTheme } from '@dxos/storybook-utils';
8
+ import { withTheme } from '@dxos/react-ui/testing';
10
9
 
11
- import { useAttentionAttributes } from './AttentionProvider';
12
10
  import { withAttention } from '../testing';
13
11
 
12
+ import { useAttentionAttributes } from './AttentionProvider';
13
+
14
14
  const Attendable = ({ id }: { id: string }) => {
15
15
  const attentionAttrs = useAttentionAttributes(id);
16
16
 
17
17
  return (
18
18
  <div {...attentionAttrs}>
19
- <textarea className='attention-surface resize-none' placeholder={id} />
19
+ <textarea className='dx-attention-surface resize-none' placeholder={id} />
20
20
  </div>
21
21
  );
22
22
  };
@@ -32,10 +32,14 @@ const Story = () => {
32
32
  );
33
33
  };
34
34
 
35
- export default {
35
+ const meta = {
36
36
  title: 'ui/react-ui-attention/AttentionProvider',
37
37
  render: Story,
38
- decorators: [withTheme, withAttention],
39
- };
38
+ decorators: [withTheme(), withAttention()],
39
+ } satisfies Meta;
40
+
41
+ export default meta;
42
+
43
+ type Story = StoryObj<typeof meta>;
40
44
 
41
- export const Default = {};
45
+ export const Default: Story = {};
@@ -2,58 +2,93 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- // NOTE(thure): The following unused imports quell TS2742 (“likely not portable”).
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, { useMemo, type FocusEvent, type PropsWithChildren, type ComponentPropsWithRef, forwardRef } from 'react';
12
-
13
- import { useDefaultValue, type ThemedClassName } from '@dxos/react-ui';
14
- import { mx } from '@dxos/react-ui-theme';
15
-
16
- import { ATTENDABLE_PATH_SEPARATOR, type Attention, AttentionManager, getAttendables } from '../attention';
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 ATTENABLE_ATTRIBUTE = 'data-attendable-id';
20
- const ATTENTION_SOURCE_ATTRIBUTE = 'data-is-attention-source';
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: new AttentionManager(),
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, path } = useAttentionContext(ATTENTION_NAME);
36
- if (!attendableId) {
37
- return UNKNOWN_ATTENDABLE;
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
- const current = [...attendableId.split(ATTENDABLE_PATH_SEPARATOR), ...path];
41
- return attention.get(current);
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
- return attention.current;
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> = { [ATTENABLE_ATTRIBUTE]: attendableId };
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 attention = useDefaultValue(propsAttention, () => new AttentionManager());
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.current;
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} path={[]}>
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, and when rendering content in those cases it
165
- * is not necessary to also render an `AttendableContainer`. This component is primarily for Storybook stories and other
166
- * testing scenarios, or the rare cases where an attendable entity is rendered outside of either of those plugins.
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 Root = asChild ? Slot : Primitive.div;
156
+ const Comp = asChild ? Slot : Primitive.div;
172
157
  return (
173
- <Root
158
+ <Comp
174
159
  role='none'
175
- {...attentionAttrs}
176
160
  {...props}
177
- className={mx('attention-surface', props.tabIndex === 0 && 'dx-focus-ring-inset-over-all', classNames)}
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
- </Root>
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
- ATTENABLE_ATTRIBUTE,
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 { Match } from 'effect';
7
- import React, { useCallback, useMemo, type PropsWithChildren } from 'react';
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-ui';
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: new SelectionManager(),
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 selection = useDefaultValue(propsSelection, () => new SelectionManager());
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
- if (contextId) {
58
- return selection.getSelected(contextId, mode);
59
- }
71
+ const [state, setState] = useState<SelectionResult<T>>(() =>
72
+ contextId && selection ? selection.getSelected(contextId, mode) : getDefaultResult(mode),
73
+ );
60
74
 
61
- return Match.type<Selection>().pipe(
62
- Match.when({ mode: 'single' }, (s) => s.id),
63
- Match.when({ mode: 'multi' }, (s) => s.ids),
64
- Match.when({ mode: 'range' }, (s) => (s.from && s.to ? { from: s.from, to: s.to } : undefined)),
65
- Match.when({ mode: 'multi-range' }, (s) => s.ranges),
66
- Match.exhaustive,
67
- )(defaultSelection(mode)) as any;
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
- export const useSelectionActions = (contextIds: string[], mode: SelectionMode = 'multi') => {
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
  }
package/src/index.ts CHANGED
@@ -5,3 +5,4 @@
5
5
  export * from './attention';
6
6
  export * from './components';
7
7
  export * from './selection';
8
+ export * from './types';