@r0hitsharma/dashboard-kit 0.12.0-rohit-fork-ci.1
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/README.md +150 -0
- package/dist/charting-interaction.d.ts +47 -0
- package/dist/charting-interaction.js +112 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +10 -0
- package/dist/interaction.d.ts +43 -0
- package/dist/interaction.js +41 -0
- package/dist/registry.d.ts +37 -0
- package/dist/registry.js +220 -0
- package/dist/renderer.d.ts +38 -0
- package/dist/renderer.js +140 -0
- package/dist/schema.d.ts +138 -0
- package/dist/schema.js +25 -0
- package/dist/validate.d.ts +104 -0
- package/dist/validate.js +174 -0
- package/package.json +61 -0
- package/src/charting-interaction.test.ts +95 -0
- package/src/charting-interaction.tsx +199 -0
- package/src/index.ts +57 -0
- package/src/interaction.ts +89 -0
- package/src/registry.tsx +410 -0
- package/src/renderer.tsx +275 -0
- package/src/schema.ts +148 -0
- package/src/validate.test.ts +257 -0
- package/src/validate.ts +233 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* Has to render: the bug this guards against is about COMMIT ORDERING
|
|
5
|
+
* (does a value published this commit reflect a `keyMap` change made in
|
|
6
|
+
* that same commit?), which a pure-logic test over `aliasesFor` alone
|
|
7
|
+
* can't observe — React's actual effect-flush order is the thing under
|
|
8
|
+
* test, matching how `ThemeProvider.test.ts` justifies its own jsdom use.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
DashboardInteractionProvider,
|
|
12
|
+
useDashboardInteraction,
|
|
13
|
+
type DashboardInteractionApi,
|
|
14
|
+
} from '@r0hitsharma/charting';
|
|
15
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
16
|
+
import { createElement, useEffect, type MutableRefObject } from 'react';
|
|
17
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
useChartingInteraction,
|
|
21
|
+
type InteractionKeyMap,
|
|
22
|
+
} from './charting-interaction.js';
|
|
23
|
+
import type { InteractionContextValue } from './interaction.js';
|
|
24
|
+
|
|
25
|
+
afterEach(cleanup);
|
|
26
|
+
|
|
27
|
+
const KEY_MAP_HIGHLIGHT: InteractionKeyMap = { myAlias: 'highlightedKey' };
|
|
28
|
+
const KEY_MAP_TIME_RANGE: InteractionKeyMap = { myAlias: 'timeRange' };
|
|
29
|
+
|
|
30
|
+
type Captured = {
|
|
31
|
+
chartApi: DashboardInteractionApi;
|
|
32
|
+
interaction: InteractionContextValue;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Exposed to the test via a ref written inside an effect, not a module-scope
|
|
36
|
+
// binding reassigned during render: the latter is exactly the pattern this
|
|
37
|
+
// PR eliminates from the library code, so the test holds itself to it too.
|
|
38
|
+
function Inner({
|
|
39
|
+
keyMap,
|
|
40
|
+
capturedRef,
|
|
41
|
+
}: {
|
|
42
|
+
keyMap: InteractionKeyMap;
|
|
43
|
+
capturedRef: MutableRefObject<Captured | null>;
|
|
44
|
+
}) {
|
|
45
|
+
const chartApi = useDashboardInteraction();
|
|
46
|
+
const { interaction, InteractionSync } = useChartingInteraction(keyMap);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
capturedRef.current = { chartApi, interaction };
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return createElement(InteractionSync);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function App({
|
|
56
|
+
keyMap,
|
|
57
|
+
capturedRef,
|
|
58
|
+
}: {
|
|
59
|
+
keyMap: InteractionKeyMap;
|
|
60
|
+
capturedRef: MutableRefObject<Captured | null>;
|
|
61
|
+
}) {
|
|
62
|
+
return createElement(
|
|
63
|
+
DashboardInteractionProvider,
|
|
64
|
+
null,
|
|
65
|
+
createElement(Inner, { keyMap, capturedRef }),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('ChartingInteractionSync', () => {
|
|
70
|
+
it('publishes under a keyMap change immediately, in the same commit as a chart value change', () => {
|
|
71
|
+
const capturedRef: MutableRefObject<Captured | null> = { current: null };
|
|
72
|
+
const { rerender } = render(
|
|
73
|
+
createElement(App, { keyMap: KEY_MAP_HIGHLIGHT, capturedRef }),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
act(() => {
|
|
77
|
+
capturedRef.current!.chartApi.setHighlightedKey('initial');
|
|
78
|
+
});
|
|
79
|
+
expect(capturedRef.current!.interaction.read('myAlias')).toBe('initial');
|
|
80
|
+
|
|
81
|
+
// Both happen in the SAME commit: `myAlias` is remapped from
|
|
82
|
+
// `highlightedKey` to `timeRange`, and `timeRange` itself changes. A
|
|
83
|
+
// stale (previous-commit) keyMap read would still resolve `myAlias` to
|
|
84
|
+
// `highlightedKey` here and miss the new value entirely.
|
|
85
|
+
act(() => {
|
|
86
|
+
capturedRef.current!.chartApi.setTimeRange({ start: 1, end: 2 });
|
|
87
|
+
rerender(createElement(App, { keyMap: KEY_MAP_TIME_RANGE, capturedRef }));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(capturedRef.current!.interaction.read('myAlias')).toEqual({
|
|
91
|
+
start: 1,
|
|
92
|
+
end: 2,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useDashboardInteraction,
|
|
3
|
+
type InteractionKey,
|
|
4
|
+
type TimeRange,
|
|
5
|
+
} from '@r0hitsharma/charting';
|
|
6
|
+
import {
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
type ReactElement,
|
|
12
|
+
type RefObject,
|
|
13
|
+
} from 'react';
|
|
14
|
+
|
|
15
|
+
import type { InteractionContextValue } from './interaction.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Adapts a manifest's free-form interaction vocabulary onto charting's REAL
|
|
19
|
+
* per-key store (`DashboardInteractionProvider` / `useDashboardInteraction` /
|
|
20
|
+
* `useInteractionValue`), so a declarative dashboard cross-filters the same
|
|
21
|
+
* synced chart group a hand-built one would.
|
|
22
|
+
*
|
|
23
|
+
* The reconciliation this file owns: a manifest names keys in an FDC3-shaped
|
|
24
|
+
* vocabulary (`highlightedAsset`, `selectedTimeRange`) while charting's store
|
|
25
|
+
* is typed to `highlightedKey` / `timeRange` / `hoveredTimestamp`. The key map
|
|
26
|
+
* below is the single, declared place that translation happens — not a rename
|
|
27
|
+
* of either side. Consumers can pass their own map to extend the vocabulary.
|
|
28
|
+
*
|
|
29
|
+
* ── Why a bridge rather than calling `useDashboardInteraction` in each widget
|
|
30
|
+
* Charting's `DashboardInteractionContext` value changes identity on every
|
|
31
|
+
* pointer move (`hoveredTimestamp` lives in it), so anything that consumes it
|
|
32
|
+
* re-renders at hover frequency. {@link InteractionSync} is the SOLE consumer:
|
|
33
|
+
* a null-rendering component that republishes the mapped keys into a tiny
|
|
34
|
+
* per-key store. A widget that declared `interaction.reads: ['highlightedAsset']`
|
|
35
|
+
* then re-renders when the highlight changes and at no other time — matching
|
|
36
|
+
* the discipline of charting's own `useInteractionValue`.
|
|
37
|
+
*/
|
|
38
|
+
export type InteractionKeyMap = Record<string, InteractionKey>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default manifest-key -> charting-key aliases. Native charting keys map to
|
|
42
|
+
* themselves so a manifest may also name them directly.
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_INTERACTION_KEY_MAP: InteractionKeyMap = {
|
|
45
|
+
highlightedAsset: 'highlightedKey',
|
|
46
|
+
highlightedKey: 'highlightedKey',
|
|
47
|
+
selectedTimeRange: 'timeRange',
|
|
48
|
+
timeRange: 'timeRange',
|
|
49
|
+
hoveredTimestamp: 'hoveredTimestamp',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ChartingInteraction = {
|
|
53
|
+
/** The generic surface to hand the renderer's `interaction` prop. */
|
|
54
|
+
interaction: InteractionContextValue;
|
|
55
|
+
/**
|
|
56
|
+
* Render ONCE inside a `SyncedChartGroup` / `DashboardInteractionProvider`.
|
|
57
|
+
* It renders nothing observable; it is the sole subscriber to the
|
|
58
|
+
* high-frequency charting context and republishes the mapped keys into the
|
|
59
|
+
* per-key store that {@link interaction} reads.
|
|
60
|
+
*/
|
|
61
|
+
InteractionSync: () => ReactElement;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type ChartingInteractionSyncProps = {
|
|
65
|
+
keyMap: InteractionKeyMap;
|
|
66
|
+
writerRef: RefObject<(key: string, value: unknown) => void>;
|
|
67
|
+
publish: (key: string, value: unknown) => void;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Module-scope, not a closure created per `useChartingInteraction` call:
|
|
72
|
+
* hooks nested inside a `useMemo`/`useCallback` are ambiguous to the rules-of
|
|
73
|
+
* -hooks check (it can't tell they belong to a separate component render, not
|
|
74
|
+
* the enclosing hook's), so this needs to be a real, statically top-level
|
|
75
|
+
* component. Everything it needs comes in as props instead of a closure.
|
|
76
|
+
*
|
|
77
|
+
* `keyMap` is a plain prop, not a ref synced from a parent effect: React
|
|
78
|
+
* flushes child effects before parent effects, so a ref updated by
|
|
79
|
+
* `useChartingInteraction`'s own effect would still hold the PREVIOUS
|
|
80
|
+
* `keyMap` when this component's effects below run in the commit where it
|
|
81
|
+
* actually changed. A prop is simply current already.
|
|
82
|
+
*/
|
|
83
|
+
function ChartingInteractionSync({
|
|
84
|
+
keyMap,
|
|
85
|
+
writerRef,
|
|
86
|
+
publish,
|
|
87
|
+
}: ChartingInteractionSyncProps): null {
|
|
88
|
+
const chart = useDashboardInteraction();
|
|
89
|
+
|
|
90
|
+
// Every manifest alias that resolves to `highlightedKey` gets the
|
|
91
|
+
// charting value republished under it; likewise for the other keys.
|
|
92
|
+
const aliasesFor = useCallback(
|
|
93
|
+
(chartingKey: InteractionKey): string[] =>
|
|
94
|
+
Object.entries(keyMap)
|
|
95
|
+
.filter(([, target]) => target === chartingKey)
|
|
96
|
+
.map(([alias]) => alias),
|
|
97
|
+
[keyMap],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
for (const alias of aliasesFor('highlightedKey')) {
|
|
102
|
+
publish(alias, chart.highlightedKey ?? undefined);
|
|
103
|
+
}
|
|
104
|
+
}, [chart.highlightedKey, publish, aliasesFor]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
for (const alias of aliasesFor('timeRange')) {
|
|
107
|
+
publish(alias, chart.timeRange ?? undefined);
|
|
108
|
+
}
|
|
109
|
+
}, [chart.timeRange, publish, aliasesFor]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
for (const alias of aliasesFor('hoveredTimestamp')) {
|
|
112
|
+
publish(alias, chart.hoveredTimestamp ?? undefined);
|
|
113
|
+
}
|
|
114
|
+
}, [chart.hoveredTimestamp, publish, aliasesFor]);
|
|
115
|
+
|
|
116
|
+
// The write side, held in a ref so `interaction.write` stays
|
|
117
|
+
// identity-stable for every consumer.
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
writerRef.current = (key: string, value: unknown) => {
|
|
120
|
+
switch (keyMap[key]) {
|
|
121
|
+
case 'highlightedKey':
|
|
122
|
+
chart.setHighlightedKey(value == null ? null : String(value));
|
|
123
|
+
return;
|
|
124
|
+
case 'timeRange':
|
|
125
|
+
chart.setTimeRange((value as TimeRange | null) ?? null);
|
|
126
|
+
return;
|
|
127
|
+
case 'hoveredTimestamp':
|
|
128
|
+
chart.setHoveredTimestamp(value == null ? null : Number(value));
|
|
129
|
+
return;
|
|
130
|
+
default:
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}, [chart, keyMap, writerRef]);
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Builds a charting-backed {@link InteractionContextValue}. Call it above a
|
|
141
|
+
* `SyncedChartGroup`, hand `interaction` to `DashboardRenderer`, and render
|
|
142
|
+
* `<InteractionSync />` inside the group.
|
|
143
|
+
*/
|
|
144
|
+
export function useChartingInteraction(
|
|
145
|
+
keyMap: InteractionKeyMap = DEFAULT_INTERACTION_KEY_MAP,
|
|
146
|
+
): ChartingInteraction {
|
|
147
|
+
const valuesRef = useRef(new Map<string, unknown>());
|
|
148
|
+
const listenersRef = useRef(new Map<string, Set<() => void>>());
|
|
149
|
+
const writerRef = useRef<(key: string, value: unknown) => void>(() => {});
|
|
150
|
+
|
|
151
|
+
const read = useCallback((key: string) => valuesRef.current.get(key), []);
|
|
152
|
+
|
|
153
|
+
const write = useCallback((key: string, value: unknown) => {
|
|
154
|
+
writerRef.current(key, value);
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
const subscribe = useCallback((key: string, onChange: () => void) => {
|
|
158
|
+
let listeners = listenersRef.current.get(key);
|
|
159
|
+
if (!listeners) {
|
|
160
|
+
listeners = new Set();
|
|
161
|
+
listenersRef.current.set(key, listeners);
|
|
162
|
+
}
|
|
163
|
+
listeners.add(onChange);
|
|
164
|
+
return () => {
|
|
165
|
+
listeners.delete(onChange);
|
|
166
|
+
};
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const publish = useCallback((key: string, value: unknown) => {
|
|
170
|
+
if (Object.is(valuesRef.current.get(key), value)) return;
|
|
171
|
+
valuesRef.current.set(key, value);
|
|
172
|
+
for (const listener of listenersRef.current.get(key) ?? []) listener();
|
|
173
|
+
}, []);
|
|
174
|
+
|
|
175
|
+
const interaction = useMemo<InteractionContextValue>(
|
|
176
|
+
() => ({ read, write, subscribe }),
|
|
177
|
+
[read, write, subscribe],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// A stable function identity, so React doesn't unmount/remount
|
|
181
|
+
// `ChartingInteractionSync` (and its charting subscription) across the
|
|
182
|
+
// dashboard's renders — as long as `keyMap` doesn't churn identity either.
|
|
183
|
+
// Pass a memoized `keyMap` (module-scoped, like the default, or your own
|
|
184
|
+
// `useMemo`); an inline object here remounts the subscription every render,
|
|
185
|
+
// the same discipline `usePlayback`'s `source` and `useDataTable`'s
|
|
186
|
+
// `columns` already require of their callers.
|
|
187
|
+
const InteractionSync = useCallback(
|
|
188
|
+
() => (
|
|
189
|
+
<ChartingInteractionSync
|
|
190
|
+
keyMap={keyMap}
|
|
191
|
+
writerRef={writerRef}
|
|
192
|
+
publish={publish}
|
|
193
|
+
/>
|
|
194
|
+
),
|
|
195
|
+
[publish, keyMap],
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return { interaction, InteractionSync };
|
|
199
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The declarative-dashboard schema — the data contract a manifest conforms to.
|
|
2
|
+
export type {
|
|
3
|
+
DashboardSpec,
|
|
4
|
+
DashboardDataSources,
|
|
5
|
+
DataBinding,
|
|
6
|
+
LayoutDirection,
|
|
7
|
+
LayoutNode,
|
|
8
|
+
SplitLayoutNode,
|
|
9
|
+
ThresholdRule,
|
|
10
|
+
ThresholdSeverity,
|
|
11
|
+
WidgetInteraction,
|
|
12
|
+
WidgetLayoutNode,
|
|
13
|
+
WidgetNode,
|
|
14
|
+
WidgetTableColumn,
|
|
15
|
+
} from './schema.js';
|
|
16
|
+
|
|
17
|
+
// The recursive renderer.
|
|
18
|
+
export {
|
|
19
|
+
DashboardRenderer,
|
|
20
|
+
renderLayoutNode,
|
|
21
|
+
type DashboardRendererProps,
|
|
22
|
+
} from './renderer.js';
|
|
23
|
+
|
|
24
|
+
// The string -> component registry mechanism + the default generic adapters.
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_REGISTRY,
|
|
27
|
+
UnknownWidget,
|
|
28
|
+
mergeRegistries,
|
|
29
|
+
type ComponentRegistry,
|
|
30
|
+
type RegistryComponent,
|
|
31
|
+
type RegistryComponentProps,
|
|
32
|
+
} from './registry.js';
|
|
33
|
+
|
|
34
|
+
// Structural + referential manifest validation, and the agent-exposure query.
|
|
35
|
+
export {
|
|
36
|
+
collectAgentWritableKeys,
|
|
37
|
+
dashboardSpecSchema,
|
|
38
|
+
validateDashboardSpec,
|
|
39
|
+
type ManifestIssue,
|
|
40
|
+
type ManifestValidation,
|
|
41
|
+
type ValidateOptions,
|
|
42
|
+
} from './validate.js';
|
|
43
|
+
|
|
44
|
+
// The generic interaction surface: the local store + the per-key read hook.
|
|
45
|
+
export {
|
|
46
|
+
useInteractionField,
|
|
47
|
+
useLocalInteraction,
|
|
48
|
+
type InteractionContextValue,
|
|
49
|
+
} from './interaction.js';
|
|
50
|
+
|
|
51
|
+
// The adapter onto charting's real per-key interaction store.
|
|
52
|
+
export {
|
|
53
|
+
DEFAULT_INTERACTION_KEY_MAP,
|
|
54
|
+
useChartingInteraction,
|
|
55
|
+
type ChartingInteraction,
|
|
56
|
+
type InteractionKeyMap,
|
|
57
|
+
} from './charting-interaction.js';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useMemo,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useSyncExternalStore,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The interaction surface a manifest's widgets read and write, addressed by
|
|
11
|
+
* the free-form string keys a `WidgetNode.interaction.reads`/`writes` names.
|
|
12
|
+
*
|
|
13
|
+
* This is deliberately a small, transport-agnostic `{read, write, subscribe}`
|
|
14
|
+
* contract rather than charting's concrete `DashboardInteractionApi` (which is
|
|
15
|
+
* typed to a fixed set of keys: `timeRange`, `hoveredTimestamp`, `filters`,
|
|
16
|
+
* `highlightedKey`). A manifest names its OWN vocabulary (`highlightedAsset`,
|
|
17
|
+
* `selectedTimeRange`, ...); {@link module:charting-interaction} is the adapter
|
|
18
|
+
* that maps that vocabulary onto charting's real per-key store. Keeping the
|
|
19
|
+
* engine's contract generic is what lets a consumer back interaction with
|
|
20
|
+
* charting, with their own store, or with the built-in local store below —
|
|
21
|
+
* without the engine caring which.
|
|
22
|
+
*/
|
|
23
|
+
export type InteractionContextValue = {
|
|
24
|
+
read: (key: string) => unknown;
|
|
25
|
+
write: (key: string, value: unknown) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Optional per-key change subscription. When present, widgets reach values
|
|
28
|
+
* through {@link useInteractionField} rather than calling `read` during
|
|
29
|
+
* render, so only the widgets bound to a changed key re-render — the same
|
|
30
|
+
* hover-perf discipline charting's own per-key store enforces.
|
|
31
|
+
*/
|
|
32
|
+
subscribe?: (key: string, onChange: () => void) => () => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type InteractionState = Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A self-contained interaction store backed by React state — the default the
|
|
39
|
+
* renderer falls back to when no `interaction` prop is supplied. Correct and
|
|
40
|
+
* cheap for a standalone manifest (a story, a preview) that isn't synced to a
|
|
41
|
+
* live chart group; for chart-synced interaction, pass the adapter from
|
|
42
|
+
* {@link module:charting-interaction} instead.
|
|
43
|
+
*/
|
|
44
|
+
export function useLocalInteraction(): InteractionContextValue {
|
|
45
|
+
const [, setState] = useState<InteractionState>({});
|
|
46
|
+
// Keep `read`/`write` identity-stable so the context value doesn't change
|
|
47
|
+
// for reasons other than a real state change.
|
|
48
|
+
const stateRef = useRef<InteractionState>({});
|
|
49
|
+
|
|
50
|
+
const read = useCallback((key: string) => stateRef.current[key], []);
|
|
51
|
+
const write = useCallback((key: string, value: unknown) => {
|
|
52
|
+
if (Object.is(stateRef.current[key], value)) return;
|
|
53
|
+
stateRef.current = { ...stateRef.current, [key]: value };
|
|
54
|
+
setState(stateRef.current);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
return useMemo(() => ({ read, write }), [read, write]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reads ONE interaction key, subscribing to just that key when the provider
|
|
62
|
+
* supports it. `key` may be `undefined` (a widget that declares no
|
|
63
|
+
* `interaction.reads`), in which case this is a no-op returning `undefined`.
|
|
64
|
+
*
|
|
65
|
+
* Named `useInteractionField` (not `useInteractionValue`) so a consumer file
|
|
66
|
+
* can import both this and charting's own `useInteractionValue` without a name
|
|
67
|
+
* collision.
|
|
68
|
+
*/
|
|
69
|
+
export function useInteractionField(
|
|
70
|
+
interaction: InteractionContextValue,
|
|
71
|
+
key: string | undefined,
|
|
72
|
+
): unknown {
|
|
73
|
+
const { read, subscribe } = interaction;
|
|
74
|
+
|
|
75
|
+
const subscribeToKey = useCallback(
|
|
76
|
+
(onChange: () => void) => {
|
|
77
|
+
if (!key || !subscribe) return () => {};
|
|
78
|
+
return subscribe(key, onChange);
|
|
79
|
+
},
|
|
80
|
+
[key, subscribe],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const getSnapshot = useCallback(
|
|
84
|
+
() => (key ? read(key) : undefined),
|
|
85
|
+
[key, read],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return useSyncExternalStore(subscribeToKey, getSnapshot, getSnapshot);
|
|
89
|
+
}
|