@ledgerhq/lumen-ui-rnative-visualization 0.1.12 → 0.1.14

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 (51) hide show
  1. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
  2. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js +52 -0
  3. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js.map +1 -0
  4. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js +81 -0
  5. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js.map +1 -0
  6. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js +340 -0
  7. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js.map +1 -0
  8. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js +12 -0
  9. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js.map +1 -0
  10. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js +145 -0
  11. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js.map +1 -0
  12. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js +253 -0
  13. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js.map +1 -0
  14. package/dist/module/lib/Components/Scrubber/Scrubber.js +40 -21
  15. package/dist/module/lib/Components/Scrubber/Scrubber.js.map +1 -1
  16. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js +80 -30
  17. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js.map +1 -1
  18. package/dist/module/lib/Components/Scrubber/Scrubber.test.js +10 -5
  19. package/dist/module/lib/Components/Scrubber/Scrubber.test.js.map +1 -1
  20. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +51 -27
  21. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  22. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  23. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.d.ts +9 -0
  24. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.d.ts.map +1 -0
  25. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.d.ts +12 -0
  26. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.d.ts.map +1 -0
  27. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts +10 -0
  28. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts.map +1 -0
  29. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts +65 -0
  30. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts.map +1 -0
  31. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts +14 -2
  32. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts.map +1 -1
  33. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
  34. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts +1 -1
  35. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts.map +1 -1
  36. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +103 -4
  37. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -1
  38. package/package.json +4 -4
  39. package/src/lib/Components/CartesianChart/CartesianChart.tsx +0 -1
  40. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.tsx +54 -0
  41. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.tsx +259 -0
  42. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.tsx +105 -0
  43. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.ts +9 -0
  44. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.ts +264 -0
  45. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.ts +229 -0
  46. package/src/lib/Components/Scrubber/Scrubber.stories.tsx +104 -42
  47. package/src/lib/Components/Scrubber/Scrubber.test.tsx +7 -5
  48. package/src/lib/Components/Scrubber/Scrubber.tsx +47 -24
  49. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +56 -28
  50. package/src/lib/Components/Scrubber/index.ts +10 -1
  51. package/src/lib/Components/Scrubber/types.ts +109 -4
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { DrawingArea } from '../../utils/types';
2
3
  export type ScrubberContextValue = {
3
4
  /**
4
5
  * Whether scrubbing interactions are enabled.
@@ -32,12 +33,104 @@ export type ScrubberProviderProps = {
32
33
  */
33
34
  onScrubberPositionChange?: (index: number | undefined) => void;
34
35
  };
35
- export type ScrubberProps = {
36
+ export type ChartTooltipItemData = {
37
+ /**
38
+ * Label displayed on the left side of the row.
39
+ */
40
+ label: string | number;
41
+ /**
42
+ * Value displayed on the right side of the row.
43
+ */
44
+ value: string | number;
45
+ };
46
+ export type ChartTooltipItemProps = ChartTooltipItemData & {
47
+ /**
48
+ * X position in SVG coordinate space.
49
+ * @default 0
50
+ */
51
+ x?: number;
52
+ /**
53
+ * Y midline position in SVG coordinate space.
54
+ * @default 0
55
+ */
56
+ y?: number;
36
57
  /**
37
- * Formats a label string shown above the reference line for a given data index.
38
- * When omitted, no label is rendered.
58
+ * Width allocated to this row, used to right-align the value text.
39
59
  */
40
- label?: (dataIndex: number) => string;
60
+ width: number;
61
+ /**
62
+ * Optional callback ref forwarded to the label `<Text>` element.
63
+ * Useful to measure the label's natural width via `getBBox` for auto-fit layouts.
64
+ */
65
+ labelRef?: (el: SvgBBoxElement | null) => void;
66
+ /**
67
+ * Optional callback ref forwarded to the value `<Text>` element.
68
+ * Useful to measure the value's natural width via `getBBox` for auto-fit layouts.
69
+ */
70
+ valueRef?: (el: SvgBBoxElement | null) => void;
71
+ };
72
+ /**
73
+ * Minimal interface for an SVG element that supports `getBBox`.
74
+ * Handles both react-native-svg (returns `Rect | undefined`) and web DOM SVG
75
+ * (returns `DOMRect` synchronously or a Promise on native bridge).
76
+ */
77
+ export type SvgBBoxElement = {
78
+ getBBox: () => {
79
+ width: number;
80
+ } | undefined | Promise<{
81
+ width: number;
82
+ } | undefined>;
83
+ };
84
+ export type ScrubberTooltipLayoutProps = {
85
+ /**
86
+ * Horizontal gap in pixels between the scrubber line and the tooltip box.
87
+ * @default 10
88
+ */
89
+ offset?: number;
90
+ /**
91
+ * Minimum width in pixels. The tooltip auto-fits to the rendered content
92
+ * but never collapses below this floor; raise it to avoid jitter when
93
+ * value length changes between indices.
94
+ * @default 80
95
+ */
96
+ minWidth?: number;
97
+ };
98
+ export type ScrubberTooltipProps = ScrubberTooltipLayoutProps & {
99
+ /**
100
+ * Horizontal pixel position of the scrubber line within the SVG coordinate
101
+ * space. Used to place the tooltip left or right of the line.
102
+ */
103
+ pixelX: number;
104
+ /**
105
+ * Bounding box of the chart's drawing area (x, y, width, height). Used to
106
+ * constrain the tooltip so it never overflows the chart boundaries.
107
+ */
108
+ drawingArea: DrawingArea;
109
+ /**
110
+ * Optional title displayed at the top of the tooltip. Omit to render
111
+ * the tooltip without a title row.
112
+ */
113
+ title?: string | number;
114
+ /**
115
+ * List of label/value pairs rendered as rows inside the tooltip body.
116
+ */
117
+ items: ChartTooltipItemData[];
118
+ };
119
+ /**
120
+ * Return value of the `tooltip` callback on {@link ScrubberProps}.
121
+ */
122
+ export type ScrubberTooltipContent = ScrubberTooltipLayoutProps & {
123
+ /**
124
+ * Optional header. Static value or callback per data index.
125
+ * A callback may return `undefined` to suppress the title.
126
+ */
127
+ title?: string | number | ((index: number) => string | number | undefined);
128
+ /**
129
+ * Tooltip rows for this index. Return an empty array to hide the tooltip.
130
+ */
131
+ items: ChartTooltipItemData[];
132
+ };
133
+ export type ScrubberProps = {
41
134
  /**
42
135
  * Hides the vertical reference line.
43
136
  * @default false
@@ -53,5 +146,11 @@ export type ScrubberProps = {
53
146
  * @default false
54
147
  */
55
148
  showBeacons?: boolean;
149
+ /**
150
+ * Produces tooltip content for the active data index. When set, {@link DefaultScrubberTooltip}
151
+ * is rendered. Optional `offset` and `minWidth` on the returned object tune layout.
152
+ * Return `{ items: [] }` to hide the tooltip at an index.
153
+ */
154
+ tooltip?: (dataIndex: number) => ScrubberTooltipContent;
56
155
  };
57
156
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Scrubber/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB;;OAEG;IACH,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC;;OAEG;IACH,wBAAwB,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB;;OAEG;IACH,wBAAwB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAChE,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Scrubber/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB;;OAEG;IACH,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC;;OAEG;IACH,wBAAwB,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB;;OAEG;IACH,wBAAwB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAChE,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,GAAG;IACzD;;;OAGG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX;;;OAGG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MACL;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GACjB,SAAS,GACT,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;CAC5C,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,0BAA0B,GAAG;IAC9D;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC;IACzB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB;;OAEG;IACH,KAAK,EAAE,oBAAoB,EAAE,CAAC;CAC/B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,0BAA0B,GAAG;IAChE;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IAC3E;;OAEG;IACH,KAAK,EAAE,oBAAoB,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,sBAAsB,CAAC;CACzD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-ui-rnative-visualization",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "keywords": [
@@ -31,7 +31,7 @@
31
31
  "!**/*.tsbuildinfo"
32
32
  ],
33
33
  "dependencies": {
34
- "@ledgerhq/lumen-utils-shared": "0.1.3",
34
+ "@ledgerhq/lumen-utils-shared": "0.1.4",
35
35
  "d3-array": "^3.2.4",
36
36
  "d3-scale": "^4.0.2",
37
37
  "d3-shape": "^3.2.0"
@@ -44,8 +44,8 @@
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@types/react": "^19.0.0",
47
- "@ledgerhq/lumen-design-core": "0.1.14",
48
- "@ledgerhq/lumen-ui-rnative": "0.1.35",
47
+ "@ledgerhq/lumen-design-core": "0.1.15",
48
+ "@ledgerhq/lumen-ui-rnative": "0.1.37",
49
49
  "react": "^19.0.0",
50
50
  "react-native": "~0.79.7",
51
51
  "react-native-svg": "^15.0.0",
@@ -30,7 +30,6 @@ export function CartesianChart({
30
30
  const [measuredWidth, setMeasuredWidth] = useState<number | undefined>(width);
31
31
 
32
32
  const needsMeasurement = width === undefined;
33
-
34
33
  const handleLayout = useCallback(
35
34
  (e: LayoutChangeEvent) => {
36
35
  if (needsMeasurement) {
@@ -0,0 +1,54 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import { memo } from 'react';
3
+ import { G, Text as SvgText } from 'react-native-svg';
4
+
5
+ import type { ChartTooltipItemProps } from '../types';
6
+ import { PADDING_X } from './constants';
7
+
8
+ /**
9
+ * Memoized so it only re-renders when one of its primitive props actually
10
+ * changes. Combined with the stable per-index ref callbacks built in
11
+ * `DefaultScrubberTooltip`, this keeps the row out of the per-scrub-frame
12
+ * reconciliation path when its label / value / position have not changed.
13
+ */
14
+ export const ChartTooltipItem = memo(function ChartTooltipItem({
15
+ label,
16
+ value,
17
+ x = 0,
18
+ y = 0,
19
+ width,
20
+ labelRef,
21
+ valueRef,
22
+ }: Readonly<ChartTooltipItemProps>) {
23
+ const { theme } = useTheme();
24
+
25
+ return (
26
+ <G>
27
+ <SvgText
28
+ ref={labelRef}
29
+ x={x + PADDING_X}
30
+ y={y}
31
+ textAnchor='start'
32
+ alignmentBaseline='central'
33
+ fill={theme.colors.text.muted}
34
+ fontSize={theme.typographies.body4.fontSize}
35
+ fontFamily={theme.fontFamilies.sans}
36
+ >
37
+ {String(label)}
38
+ </SvgText>
39
+ <SvgText
40
+ ref={valueRef}
41
+ x={x + width - PADDING_X}
42
+ y={y}
43
+ textAnchor='end'
44
+ alignmentBaseline='central'
45
+ fill={theme.colors.text.base}
46
+ fontSize={theme.typographies.body4.fontSize}
47
+ fontWeight={theme.typographies.body4.fontWeight}
48
+ fontFamily={theme.fontFamilies.sans}
49
+ >
50
+ {String(value)}
51
+ </SvgText>
52
+ </G>
53
+ );
54
+ });
@@ -0,0 +1,259 @@
1
+ import { describe, expect, it, jest } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { ThemeProvider } from '@ledgerhq/lumen-ui-rnative';
4
+ import { render } from '@testing-library/react-native';
5
+ import type { ComponentProps, ReactNode } from 'react';
6
+
7
+ import { CartesianChart } from '../../CartesianChart';
8
+ import { ScrubberContextProvider } from '../context';
9
+ import { Scrubber } from '../Scrubber';
10
+ import type { ScrubberContextValue, ScrubberTooltipContent } from '../types';
11
+
12
+ const sampleSeries = [
13
+ { id: 's1', stroke: '#7B61FF', data: [10, 20, 30, 40, 50] },
14
+ ];
15
+
16
+ const Wrapper = ({ children }: { children: ReactNode }) => (
17
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
18
+ {children}
19
+ </ThemeProvider>
20
+ );
21
+
22
+ const activeScrubberContext: ScrubberContextValue = {
23
+ enableScrubbing: true,
24
+ scrubberPosition: 2,
25
+ onScrubberPositionChange: () => undefined,
26
+ };
27
+
28
+ const idleScrubberContext: ScrubberContextValue = {
29
+ enableScrubbing: true,
30
+ scrubberPosition: undefined,
31
+ onScrubberPositionChange: () => undefined,
32
+ };
33
+
34
+ const renderTooltip = ({
35
+ scrubberProps = { tooltip: () => ({ items: [] }) },
36
+ scrubberContext = activeScrubberContext,
37
+ width = 400,
38
+ height = 200,
39
+ }: {
40
+ scrubberProps?: ComponentProps<typeof Scrubber>;
41
+ scrubberContext?: ScrubberContextValue;
42
+ width?: number;
43
+ height?: number;
44
+ } = {}) =>
45
+ render(
46
+ <Wrapper>
47
+ <CartesianChart series={sampleSeries} width={width} height={height}>
48
+ <ScrubberContextProvider value={scrubberContext}>
49
+ <Scrubber {...scrubberProps} />
50
+ </ScrubberContextProvider>
51
+ </CartesianChart>
52
+ </Wrapper>,
53
+ );
54
+
55
+ describe('DefaultScrubberTooltip', () => {
56
+ it('renders nothing when no scrubber position is active', () => {
57
+ const { queryByTestId } = renderTooltip({
58
+ scrubberProps: {
59
+ tooltip: () => ({ items: [{ label: 'Date', value: 'Jan' }] }),
60
+ },
61
+ scrubberContext: idleScrubberContext,
62
+ });
63
+ expect(queryByTestId('chart-tooltip')).toBeNull();
64
+ });
65
+
66
+ it('renders nothing when items callback returns an empty array', () => {
67
+ const { queryByTestId } = renderTooltip({
68
+ scrubberProps: { tooltip: () => ({ items: [] }) },
69
+ });
70
+ expect(queryByTestId('chart-tooltip')).toBeNull();
71
+ });
72
+
73
+ it('renders the tooltip group when scrubber is active and items are provided', () => {
74
+ const { getByTestId } = renderTooltip({
75
+ scrubberProps: {
76
+ tooltip: () => ({ items: [{ label: 'Date', value: 'Jan' }] }),
77
+ },
78
+ });
79
+ expect(getByTestId('chart-tooltip')).toBeTruthy();
80
+ });
81
+
82
+ it('renders the background rect', () => {
83
+ const { getByTestId } = renderTooltip({
84
+ scrubberProps: {
85
+ tooltip: () => ({ items: [{ label: 'Date', value: 'Jan' }] }),
86
+ },
87
+ });
88
+ expect(getByTestId('chart-tooltip-rect')).toBeTruthy();
89
+ });
90
+
91
+ it('renders item label and value text', () => {
92
+ const { getByText } = renderTooltip({
93
+ scrubberProps: {
94
+ tooltip: () => ({ items: [{ label: 'Date', value: 'Jan' }] }),
95
+ },
96
+ });
97
+ expect(getByText('Date')).toBeTruthy();
98
+ expect(getByText('Jan')).toBeTruthy();
99
+ });
100
+
101
+ it('renders multiple items', () => {
102
+ const { getByText } = renderTooltip({
103
+ scrubberProps: {
104
+ tooltip: () => ({
105
+ items: [
106
+ { label: 'Date', value: 'Jan' },
107
+ { label: 'Price', value: '$10' },
108
+ ],
109
+ }),
110
+ },
111
+ });
112
+ expect(getByText('Date')).toBeTruthy();
113
+ expect(getByText('Jan')).toBeTruthy();
114
+ expect(getByText('Price')).toBeTruthy();
115
+ expect(getByText('$10')).toBeTruthy();
116
+ });
117
+
118
+ it('passes the active data index to the tooltip callback', () => {
119
+ const tooltip = jest.fn().mockReturnValue({
120
+ items: [{ label: 'i', value: '0' }],
121
+ }) as unknown as (index: number) => ScrubberTooltipContent;
122
+ renderTooltip({ scrubberProps: { tooltip } });
123
+ expect(tooltip).toHaveBeenCalledWith(
124
+ activeScrubberContext.scrubberPosition,
125
+ );
126
+ });
127
+
128
+ it('suppresses the tooltip when items returns empty for the active index', () => {
129
+ const { queryByTestId } = renderTooltip({
130
+ scrubberProps: { tooltip: () => ({ items: [] }) },
131
+ });
132
+ expect(queryByTestId('chart-tooltip')).toBeNull();
133
+ });
134
+
135
+ describe('positioning logic', () => {
136
+ const defaultTooltip = (): ScrubberTooltipContent => ({
137
+ items: [{ label: 'Date', value: 'Jan' }],
138
+ });
139
+
140
+ const scrubberAt = (position: number): ScrubberContextValue => ({
141
+ enableScrubbing: true,
142
+ scrubberPosition: position,
143
+ onScrubberPositionChange: () => undefined,
144
+ });
145
+
146
+ it('places tooltip to the right of the scrubber when mid-chart', () => {
147
+ const { getByTestId } = renderTooltip({
148
+ scrubberProps: { tooltip: defaultTooltip },
149
+ scrubberContext: scrubberAt(2),
150
+ });
151
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(210);
152
+ });
153
+
154
+ it('flips tooltip left when right side would overflow (index 3)', () => {
155
+ const { getByTestId } = renderTooltip({
156
+ scrubberProps: {
157
+ tooltip: () => ({ ...defaultTooltip(), minWidth: 120 }),
158
+ },
159
+ scrubberContext: scrubberAt(3),
160
+ });
161
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(145);
162
+ });
163
+
164
+ it('flips at the rightmost scrubber position (index 4)', () => {
165
+ const { getByTestId } = renderTooltip({
166
+ scrubberProps: {
167
+ tooltip: () => ({ ...defaultTooltip(), minWidth: 120 }),
168
+ },
169
+ scrubberContext: scrubberAt(4),
170
+ });
171
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(220);
172
+ });
173
+
174
+ it('custom offset shifts the no-flip tooltip position', () => {
175
+ const { getByTestId } = renderTooltip({
176
+ scrubberProps: {
177
+ tooltip: () => ({ ...defaultTooltip(), offset: 20 }),
178
+ },
179
+ scrubberContext: scrubberAt(2),
180
+ });
181
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(220);
182
+ });
183
+
184
+ it('wider minWidth can trigger a flip that the default width would not', () => {
185
+ const { getByTestId } = renderTooltip({
186
+ scrubberProps: {
187
+ tooltip: () => ({ ...defaultTooltip(), minWidth: 200 }),
188
+ },
189
+ scrubberContext: scrubberAt(2),
190
+ });
191
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(50);
192
+ });
193
+ });
194
+
195
+ describe('title in tooltip content', () => {
196
+ it('renders a static string title', () => {
197
+ const { getByTestId, getByText } = renderTooltip({
198
+ scrubberProps: {
199
+ tooltip: () => ({
200
+ title: 'My Title',
201
+ items: [{ label: 'T', value: 'V' }],
202
+ }),
203
+ },
204
+ });
205
+ expect(getByTestId('chart-tooltip-title')).toBeTruthy();
206
+ expect(getByText('My Title')).toBeTruthy();
207
+ });
208
+
209
+ it('renders the title returned by a callback', () => {
210
+ const { getByTestId } = renderTooltip({
211
+ scrubberProps: {
212
+ tooltip: () => ({
213
+ title: (i: number) => `Index ${i}`,
214
+ items: [{ label: 'T', value: 'V' }],
215
+ }),
216
+ },
217
+ });
218
+ expect(getByTestId('chart-tooltip-title')).toBeTruthy();
219
+ });
220
+
221
+ it('passes the active data index to the title callback', () => {
222
+ const title = jest.fn().mockReturnValue('Title') as unknown as (
223
+ index: number,
224
+ ) => string;
225
+ renderTooltip({
226
+ scrubberProps: {
227
+ tooltip: () => ({
228
+ title,
229
+ items: [{ label: 'T', value: 'V' }],
230
+ }),
231
+ },
232
+ });
233
+ expect(title).toHaveBeenCalledWith(
234
+ activeScrubberContext.scrubberPosition,
235
+ );
236
+ });
237
+
238
+ it('does not render a title element when title is undefined', () => {
239
+ const { queryByTestId } = renderTooltip({
240
+ scrubberProps: {
241
+ tooltip: () => ({ items: [{ label: 'T', value: 'V' }] }),
242
+ },
243
+ });
244
+ expect(queryByTestId('chart-tooltip-title')).toBeNull();
245
+ });
246
+
247
+ it('does not render a title element when title callback returns undefined', () => {
248
+ const { queryByTestId } = renderTooltip({
249
+ scrubberProps: {
250
+ tooltip: () => ({
251
+ title: () => undefined,
252
+ items: [{ label: 'T', value: 'V' }],
253
+ }),
254
+ },
255
+ });
256
+ expect(queryByTestId('chart-tooltip-title')).toBeNull();
257
+ });
258
+ });
259
+ });
@@ -0,0 +1,105 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import { G, Rect, Text as SvgText } from 'react-native-svg';
3
+
4
+ import type { ScrubberTooltipProps } from '../types';
5
+ import { ChartTooltipItem } from './ChartTooltipItem';
6
+ import {
7
+ BORDER_RADIUS,
8
+ DEFAULT_OFFSET,
9
+ DEFAULT_TOOLTIP_MIN_WIDTH,
10
+ PADDING_X,
11
+ PADDING_Y,
12
+ ROW_GAP,
13
+ ROW_HEIGHT,
14
+ } from './constants';
15
+ import {
16
+ computeItemsBaseY,
17
+ computeTooltipHeight,
18
+ computeTooltipWidth,
19
+ computeTooltipX,
20
+ useBuildRefSetters,
21
+ useTooltipMeasurement,
22
+ } from './utils';
23
+
24
+ /**
25
+ * Default structured tooltip anchored to the scrubber line.
26
+ *
27
+ * The tooltip auto-fits its width to the rendered content via `getBBox`,
28
+ * with `minWidth` acting as an optional minimum width override. Use with
29
+ * {@link ScrubberProps.tooltip}; layout options (`offset`, `minWidth`) belong
30
+ * on the object returned from the `tooltip` callback. Return `{ items: [] }`
31
+ * from the callback to hide the tooltip at a given index.
32
+ */
33
+ export function DefaultScrubberTooltip({
34
+ pixelX,
35
+ drawingArea,
36
+ title,
37
+ items,
38
+ offset = DEFAULT_OFFSET,
39
+ minWidth = DEFAULT_TOOLTIP_MIN_WIDTH,
40
+ }: Readonly<ScrubberTooltipProps>) {
41
+ const { theme } = useTheme();
42
+
43
+ const hasTitle = title !== undefined;
44
+
45
+ const { widths, titleRef, labelRefs, valueRefs } = useTooltipMeasurement(
46
+ items,
47
+ hasTitle,
48
+ title,
49
+ );
50
+
51
+ const labelRefSetters = useBuildRefSetters(labelRefs, items.length);
52
+ const valueRefSetters = useBuildRefSetters(valueRefs, items.length);
53
+
54
+ if (items.length === 0) {
55
+ return null;
56
+ }
57
+
58
+ const tooltipWidth = computeTooltipWidth(widths, hasTitle, minWidth);
59
+ const tooltipX = computeTooltipX(pixelX, offset, tooltipWidth, drawingArea);
60
+ const tooltipHeight = computeTooltipHeight(items.length, hasTitle);
61
+ const itemsBaseY = computeItemsBaseY(drawingArea.y, hasTitle);
62
+
63
+ return (
64
+ <G testID='chart-tooltip' opacity={widths === null ? 0 : 1}>
65
+ <Rect
66
+ testID='chart-tooltip-rect'
67
+ x={tooltipX}
68
+ y={drawingArea.y}
69
+ width={tooltipWidth}
70
+ height={tooltipHeight}
71
+ rx={BORDER_RADIUS}
72
+ fill={theme.colors.bg.muted}
73
+ />
74
+ {hasTitle && (
75
+ <SvgText
76
+ ref={(el) => {
77
+ titleRef.current = el as unknown as (typeof titleRef)['current'];
78
+ }}
79
+ testID='chart-tooltip-title'
80
+ x={tooltipX + PADDING_X}
81
+ y={drawingArea.y + PADDING_Y + ROW_HEIGHT / 2}
82
+ alignmentBaseline='central'
83
+ fill={theme.colors.text.base}
84
+ fontSize={theme.typographies.body4.fontSize}
85
+ fontWeight={theme.typographies.body4.fontWeight}
86
+ fontFamily={theme.fontFamilies.sans}
87
+ >
88
+ {String(title)}
89
+ </SvgText>
90
+ )}
91
+ {items.map((item, i) => (
92
+ <ChartTooltipItem
93
+ key={i}
94
+ label={item.label}
95
+ value={item.value}
96
+ x={tooltipX}
97
+ y={itemsBaseY + i * (ROW_HEIGHT + ROW_GAP) + ROW_HEIGHT / 2}
98
+ width={tooltipWidth}
99
+ labelRef={labelRefSetters[i]}
100
+ valueRef={valueRefSetters[i]}
101
+ />
102
+ ))}
103
+ </G>
104
+ );
105
+ }
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_OFFSET = 10;
2
+ export const DEFAULT_TOOLTIP_MIN_WIDTH = 80;
3
+ export const PADDING_X = 8;
4
+ export const PADDING_Y = 8;
5
+ export const ROW_HEIGHT = 16;
6
+ export const ROW_GAP = 6;
7
+ export const TITLE_GAP = 6;
8
+ export const BORDER_RADIUS = 4;
9
+ export const LABEL_VALUE_GAP = 12;