Skip to content

Commit 10428e8

Browse files
committed
simpler tooltip api
1 parent 8bb3b1f commit 10428e8

File tree

6 files changed

+70
-82
lines changed

6 files changed

+70
-82
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"homepage": "https://cloudscape.design",
99
"scripts": {
10-
"prebuild": "rm -rf lib dist .cache",
10+
"prebuild": "rm -rf lib dist .cache && rm -rf node_modules/.vite && rm -rf node_modules/.vite-temp",
1111
"build": "npm-run-all build:pkg --parallel build:src:* --parallel build:pages:* build:themeable",
1212
"lint": "npm-run-all --parallel lint:*",
1313
"lint:eslint": "eslint --ignore-path .gitignore --ext ts,tsx,js .",

src/cartesian-chart/chart-tooltip-cartesian.tsx

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { useState } from "react";
55

6-
import { TooltipContentGetter } from "../core/interfaces-core";
6+
import { TooltipContent } from "../core/interfaces-core";
77
import ChartSeriesDetails, { ChartSeriesDetailItem } from "../internal/components/series-details";
88
import { ChartSeriesMarkerStatus, ChartSeriesMarkerType } from "../internal/components/series-marker";
99
import { getDefaultFormatter } from "./default-formatters";
@@ -24,14 +24,11 @@ export function useChartTooltipCartesian(
2424
const { xAxis, series } = props.options;
2525
const [expandedSeries, setExpandedSeries] = useState<Record<string, Set<string>>>({});
2626

27-
const getContent: TooltipContentGetter<{ expandedSeries: Record<string, Set<string>> }> = (point: {
28-
x: number;
29-
y: number;
30-
}) => {
27+
const getContent = (point: { x: number; y: number }): null | TooltipContent => {
3128
const chart = getChart();
3229
if (!chart) {
3330
console.warn("Chart instance is not available.");
34-
return () => ({ title: null, body: null });
31+
return null;
3532
}
3633

3734
const matchedItems: CartesianChartProps.TooltipSeriesDetailItem[] = [];
@@ -120,41 +117,39 @@ export function useChartTooltipCartesian(
120117

121118
const tooltipDetails = { x: point.x, items: matchedItems };
122119

123-
return ({ expandedSeries }) => {
124-
const content = props.tooltip?.content?.(tooltipDetails) ?? (
125-
<ChartSeriesDetails
126-
details={details}
127-
expandedSeries={expandedSeries[point.x]}
128-
setExpandedState={(id, isExpanded) => {
129-
setExpandedSeries((oldState) => {
130-
const expandedSeriesInCurrentCoordinate = new Set(oldState[point.x]);
131-
if (isExpanded) {
132-
expandedSeriesInCurrentCoordinate.add(id);
133-
} else {
134-
expandedSeriesInCurrentCoordinate.delete(id);
135-
}
136-
return {
137-
...oldState,
138-
[point.x]: expandedSeriesInCurrentCoordinate,
139-
};
140-
});
141-
}}
142-
/>
143-
);
144-
145-
const footer = props.tooltip?.footer?.(tooltipDetails);
146-
147-
const body = content;
148-
149-
return {
150-
title: props.tooltip?.title?.(tooltipDetails) ?? titleFormatter(point.x),
151-
body,
152-
footer,
153-
};
120+
const content = props.tooltip?.content?.(tooltipDetails) ?? (
121+
<ChartSeriesDetails
122+
details={details}
123+
expandedSeries={expandedSeries[point.x]}
124+
setExpandedState={(id, isExpanded) => {
125+
setExpandedSeries((oldState) => {
126+
const expandedSeriesInCurrentCoordinate = new Set(oldState[point.x]);
127+
if (isExpanded) {
128+
expandedSeriesInCurrentCoordinate.add(id);
129+
} else {
130+
expandedSeriesInCurrentCoordinate.delete(id);
131+
}
132+
return {
133+
...oldState,
134+
[point.x]: expandedSeriesInCurrentCoordinate,
135+
};
136+
});
137+
}}
138+
/>
139+
);
140+
141+
const footer = props.tooltip?.footer?.(tooltipDetails);
142+
143+
const body = content;
144+
145+
return {
146+
title: props.tooltip?.title?.(tooltipDetails) ?? titleFormatter(point.x),
147+
body,
148+
footer,
154149
};
155150
};
156151

157-
return { getContent, state: { expandedSeries } };
152+
return { getContent };
158153
}
159154

160155
function getSeriesMarkerType(series: CartesianChartProps.Series): ChartSeriesMarkerType {

src/core/chart-core.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import { ChartNoDataProps, LegendMarkersProps, TooltipProps } from "./interfaces
1919
import * as Styles from "./styles";
2020
import { getSeriesToIdMap } from "./utils";
2121

22-
interface CloudscapeHighchartsCoreProps<TooltipState> {
22+
interface CloudscapeHighchartsCoreProps {
2323
highcharts: null | typeof Highcharts;
2424
options: Highcharts.Options;
25-
tooltip?: TooltipProps<TooltipState>;
25+
tooltip?: TooltipProps;
2626
noData?: ChartNoDataProps;
2727
legendMarkers?: LegendMarkersProps;
2828
fallback?: React.ReactNode;
@@ -38,7 +38,7 @@ export interface CloudscapeHighchartsRef {
3838
}
3939

4040
interface CloudscapeHighchartsForwardRefType {
41-
<T>(props: CloudscapeHighchartsCoreProps<T> & { ref?: React.Ref<CloudscapeHighchartsRef> }): JSX.Element;
41+
(props: CloudscapeHighchartsCoreProps & { ref?: React.Ref<CloudscapeHighchartsRef> }): JSX.Element;
4242
}
4343

4444
/**
@@ -298,7 +298,7 @@ export const CloudscapeHighcharts = forwardRef(
298298
}}
299299
/>
300300

301-
{tooltipProps && <ChartTooltip {...tooltip.props} tooltipState={tooltipProps.state} />}
301+
{tooltipProps && <ChartTooltip {...tooltip.props} />}
302302

303303
{noDataProps && <ChartNoData {...noData.props} />}
304304

src/core/chart-tooltip.tsx

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { colorChartsLineTick } from "@cloudscape-design/design-tokens";
99
import Popover from "../internal/components/popover";
1010
import AsyncStore, { useSelector } from "../internal/utils/async-store";
1111
import { DebouncedCall } from "../internal/utils/utils";
12-
import { Point, TooltipContent, TooltipContentGetter } from "./interfaces-core";
12+
import { Point, TooltipContent } from "./interfaces-core";
1313

1414
const MOUSE_LEAVE_DELAY = 300;
1515
const LAST_DISMISS_DELAY = 250;
@@ -19,15 +19,14 @@ const LAST_DISMISS_DELAY = 250;
1919
// The tooltip is can be hidden then we receive mouse-leave. It can also be pinned/unpinned on mouse click.
2020
// Despite event names, they events also fire on keyboard interactions.
2121

22-
export function useChartTooltip<State>(
22+
export function useChartTooltip(
2323
highcharts: null | typeof Highcharts,
2424
getChart: () => Highcharts.Chart,
2525
tooltipProps?: {
26-
state: State;
27-
getContent: TooltipContentGetter<State>;
26+
getContent: (point: Point) => null | TooltipContent;
2827
},
2928
) {
30-
const tooltipStore = useRef(new TooltipStore(getChart, tooltipProps?.getContent)).current;
29+
const tooltipStore = useRef(new TooltipStore(getChart)).current;
3130

3231
const options: Highcharts.Options = {
3332
chart: {
@@ -63,21 +62,24 @@ export function useChartTooltip<State>(
6362
},
6463
};
6564

66-
return { options, props: { tooltipStore } };
65+
return { options, props: { tooltipStore, getContent: tooltipProps?.getContent ?? (() => null) } };
6766
}
6867

69-
export function ChartTooltip<TooltipState>({
68+
export function ChartTooltip({
7069
tooltipStore,
71-
tooltipState,
70+
getContent,
7271
}: {
73-
tooltipStore: TooltipStore<TooltipState>;
74-
tooltipState: TooltipState;
72+
tooltipStore: TooltipStore;
73+
getContent: (point: Point) => null | TooltipContent;
7574
}) {
7675
const tooltip = useSelector(tooltipStore, (s) => s);
77-
if (!tooltip.content) {
76+
if (!tooltip.visible) {
77+
return null;
78+
}
79+
const content = getContent(tooltip.point);
80+
if (!content) {
7881
return null;
7982
}
80-
const renderedContent = tooltip.content(tooltipState);
8183
return (
8284
<Popover
8385
getTrack={tooltipStore.getTrack}
@@ -87,36 +89,33 @@ export function ChartTooltip<TooltipState>({
8789
onDismiss={tooltipStore.onDismiss}
8890
onMouseEnter={tooltipStore.onMouseEnterTooltip}
8991
onMouseLeave={tooltipStore.onMouseLeaveTooltip}
90-
title={renderedContent.title}
91-
footer={renderedContent.footer}
92+
title={content.title}
93+
footer={content.footer}
9294
>
93-
{renderedContent.body}
95+
{content.body}
9496
</Popover>
9597
);
9698
}
9799

98-
interface ReactiveTooltipState<TooltipState> {
100+
interface ReactiveTooltipState {
99101
visible: boolean;
100102
pinned: boolean;
101103
point: Point;
102-
content: null | ((state: TooltipState) => TooltipContent);
103104
}
104105

105-
class TooltipStore<TooltipState> extends AsyncStore<ReactiveTooltipState<TooltipState>> {
106+
class TooltipStore extends AsyncStore<ReactiveTooltipState> {
106107
public getTrack: () => null | HTMLElement | SVGElement = () => null;
107108

108109
private getChart: () => Highcharts.Chart;
109-
private getContent?: TooltipContentGetter<TooltipState>;
110110
private targetElement: null | Highcharts.SVGElement = null;
111111
private markerElement: null | Highcharts.SVGElement = null;
112112
private mouseLeaveCall = new DebouncedCall();
113113
private lastDismissTime = 0;
114114
private tooltipHovered = false;
115115

116-
constructor(getChart: () => Highcharts.Chart, getContent?: TooltipContentGetter<TooltipState>) {
117-
super({ visible: false, pinned: false, point: { x: 0, y: 0 }, content: null });
116+
constructor(getChart: () => Highcharts.Chart) {
117+
super({ visible: false, pinned: false, point: { x: 0, y: 0 } });
118118
this.getChart = getChart;
119-
this.getContent = getContent;
120119
}
121120

122121
// When hovering (or focusing) over the target (point, bar, segment, etc.) we show the tooltip in the target coordinate.
@@ -131,8 +130,7 @@ class TooltipStore<TooltipState> extends AsyncStore<ReactiveTooltipState<Tooltip
131130
this.moveMarkers(target);
132131
this.set(() => {
133132
const point = { x: target.x, y: target.y ?? 0 };
134-
const content = this.getContent?.(point) ?? null;
135-
return { visible: !!content, pinned: false, point, content };
133+
return { visible: true, pinned: false, point };
136134
});
137135
};
138136

@@ -147,16 +145,14 @@ class TooltipStore<TooltipState> extends AsyncStore<ReactiveTooltipState<Tooltip
147145
this.moveMarkers(target);
148146
this.set((prev) => {
149147
const point = target ? { x: target.x, y: target.y ?? 0 } : prev.point;
150-
const content = this.getContent?.(point) ?? null;
151-
return { visible: !!content, pinned: false, point, content };
148+
return { visible: true, pinned: false, point };
152149
});
153150
}
154151
// If the click point is missing or matches the current position and it wasn't recently dismissed - it is pinned in this position.
155152
else if (new Date().getTime() - this.lastDismissTime > LAST_DISMISS_DELAY) {
156153
this.set((prev) => {
157154
const point = target ? { x: target.x, y: target.y ?? 0 } : prev.point;
158-
const content = this.getContent?.(point) ?? null;
159-
return { visible: !!content, pinned: !!content, point, content };
155+
return { visible: true, pinned: true, point };
160156
});
161157
}
162158
};

src/core/interfaces-core.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,8 @@ export interface CloudscapeHighchartsBase {
6565
noData?: ChartNoDataProps;
6666
}
6767

68-
export interface TooltipProps<TooltipState> {
69-
state: TooltipState;
70-
getContent: TooltipContentGetter<TooltipState>;
68+
export interface TooltipProps {
69+
getContent: (point: Point) => null | TooltipContent;
7170
}
7271

7372
export interface TooltipContent {
@@ -81,8 +80,6 @@ export interface Point {
8180
y: number;
8281
}
8382

84-
export type TooltipContentGetter<State> = (point: Point) => null | ((state: State) => TooltipContent);
85-
8683
export interface ChartNoDataProps {
8784
statusType?: "finished" | "loading" | "error";
8885
empty?: React.ReactNode;

src/pie-chart/chart-tooltip-pie.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import Box from "@cloudscape-design/components/box";
55

6-
import { TooltipContentGetter } from "../core/interfaces-core";
6+
import { TooltipContent } from "../core/interfaces-core";
77
import ChartSeriesDetails from "../internal/components/series-details";
88
import { ChartSeriesMarker } from "../internal/components/series-marker";
99
import { InternalPieChartOptions, PieChartProps } from "./interfaces-pie";
@@ -15,11 +15,11 @@ export function useChartTooltipPie(
1515
tooltip?: PieChartProps.TooltipProps;
1616
},
1717
) {
18-
const getContent: TooltipContentGetter<undefined> = (point: { x: number; y: number }) => {
18+
const getContent = (point: { x: number; y: number }): null | TooltipContent => {
1919
const chart = getChart();
2020
if (!chart) {
2121
console.warn("Chart instance is not available.");
22-
return () => ({ title: null, body: null });
22+
return null;
2323
}
2424
const series = props.options.series[0] as undefined | PieChartProps.Series;
2525
const matchedChartSeries = chart.series.find((s) => (s.userOptions.id ?? s.name) === (series?.id ?? series?.name));
@@ -62,10 +62,10 @@ export function useChartTooltipPie(
6262

6363
const footer = props.tooltip?.footer?.(tooltipDetails);
6464

65-
const body = (content ?? footer) ? content : null;
65+
const body = content;
6666

67-
return () => ({ title, body, footer });
67+
return { title, body, footer };
6868
};
6969

70-
return { getContent, state: undefined };
70+
return { getContent };
7171
}

0 commit comments

Comments
 (0)