@carto/ps-react-ui 6.0.0 → 6.1.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/dist/actions-BWlS5tpP.js +1150 -0
- package/dist/actions-BWlS5tpP.js.map +1 -0
- package/dist/category-alzjKzvQ.js +839 -0
- package/dist/category-alzjKzvQ.js.map +1 -0
- package/dist/{download-Vlzn2jzb.js → download-B4sykOFm.js} +94 -83
- package/dist/download-B4sykOFm.js.map +1 -0
- package/dist/{echart-DjcbtKgi.js → echart-CV8F50gn.js} +6 -5
- package/dist/echart-CV8F50gn.js.map +1 -0
- package/dist/{formula-CdHWtepq.js → formula-DJS-JLpk.js} +10 -16
- package/dist/{formula-CdHWtepq.js.map → formula-DJS-JLpk.js.map} +1 -1
- package/dist/legend.js +34 -21
- package/dist/legend.js.map +1 -1
- package/dist/{markdown-CY-67NRE.js → markdown-Bn2nKCbn.js} +12 -18
- package/dist/{markdown-CY-67NRE.js.map → markdown-Bn2nKCbn.js.map} +1 -1
- package/dist/{range-DHQSsl2_.js → range-BpU2zBmq.js} +2 -2
- package/dist/{range-DHQSsl2_.js.map → range-BpU2zBmq.js.map} +1 -1
- package/dist/{spread-DuM5oTI1.js → spread-CQwFIwuz.js} +10 -16
- package/dist/{spread-DuM5oTI1.js.map → spread-CQwFIwuz.js.map} +1 -1
- package/dist/{stores-B8CHbgw-.js → stores-DcBOZlU3.js} +30 -24
- package/dist/stores-DcBOZlU3.js.map +1 -0
- package/dist/{table-BE4uO9pp.js → table-DEdW_fDj.js} +7 -13
- package/dist/{table-BE4uO9pp.js.map → table-DEdW_fDj.js.map} +1 -1
- package/dist/types/legend/stores/types.d.ts +4 -2
- package/dist/types/widgets/actions/download/csv-item.d.ts +3 -3
- package/dist/types/widgets/actions/download/index.d.ts +2 -1
- package/dist/types/widgets/actions/download/png-item.d.ts +2 -2
- package/dist/types/widgets/actions/download/types.d.ts +12 -1
- package/dist/types/widgets/actions/download/widget-download.d.ts +26 -0
- package/dist/types/widgets/actions/fullscreen/fullscreen.d.ts +20 -13
- package/dist/types/widgets/actions/index.d.ts +1 -1
- package/dist/types/widgets/actions/searcher/filter.d.ts +3 -2
- package/dist/types/widgets/actions/searcher/searcher-toggle.d.ts +3 -0
- package/dist/types/widgets/bar/download.d.ts +6 -20
- package/dist/types/widgets/category/category-ui.d.ts +9 -1
- package/dist/types/widgets/category/download.d.ts +5 -10
- package/dist/types/widgets/category/helpers.d.ts +10 -0
- package/dist/types/widgets/echart/style.d.ts +1 -0
- package/dist/types/widgets/formula/download.d.ts +5 -15
- package/dist/types/widgets/histogram/download.d.ts +3 -8
- package/dist/types/widgets/index.d.ts +1 -1
- package/dist/types/widgets/markdown/download.d.ts +5 -11
- package/dist/types/widgets/pie/download.d.ts +3 -8
- package/dist/types/widgets/scatterplot/download.d.ts +6 -11
- package/dist/types/widgets/spread/download.d.ts +6 -12
- package/dist/types/widgets/stores/index.d.ts +1 -1
- package/dist/types/widgets/stores/widget-store-registry.d.ts +14 -0
- package/dist/types/widgets/table/download.d.ts +7 -13
- package/dist/types/widgets/timeseries/download.d.ts +7 -12
- package/dist/types/widgets/toolbox/toolbox.d.ts +1 -1
- package/dist/types/widgets/wrapper/style.d.ts +31 -2
- package/dist/types/widgets/wrapper/widget-actions.d.ts +2 -2
- package/dist/types/widgets/wrapper/widget-wrapper.d.ts +2 -2
- package/dist/{utils-BStJWhZN.js → utils-C-eWwOhE.js} +2 -2
- package/dist/{utils-BStJWhZN.js.map → utils-C-eWwOhE.js.map} +1 -1
- package/dist/widgets/actions.js +4 -4
- package/dist/widgets/bar.js +11 -17
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +1 -1
- package/dist/widgets/echart.js +1 -1
- package/dist/widgets/formula.js +1 -1
- package/dist/widgets/histogram.js +14 -20
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +1 -1
- package/dist/widgets/pie.js +13 -19
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/range.js +1 -1
- package/dist/widgets/scatterplot.js +13 -19
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/spread.js +1 -1
- package/dist/widgets/stores.js +2 -2
- package/dist/widgets/table.js +1 -1
- package/dist/widgets/timeseries.js +16 -22
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets.js +364 -376
- package/dist/widgets.js.map +1 -1
- package/dist/{zoom-toggle-D_VuA01l.js → zoom-toggle-MmLjMTEw.js} +2 -2
- package/dist/{zoom-toggle-D_VuA01l.js.map → zoom-toggle-MmLjMTEw.js.map} +1 -1
- package/package.json +1 -1
- package/src/legend/components/legend-item/legend-item-ui.test.tsx +22 -1
- package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +22 -6
- package/src/legend/stores/types.ts +4 -2
- package/src/widgets/actions/download/csv-item.test.tsx +3 -3
- package/src/widgets/actions/download/csv-item.tsx +6 -6
- package/src/widgets/actions/download/download.tsx +5 -3
- package/src/widgets/actions/download/index.ts +2 -1
- package/src/widgets/actions/download/png-item.test.tsx +5 -3
- package/src/widgets/actions/download/png-item.tsx +4 -4
- package/src/widgets/actions/download/types.ts +15 -1
- package/src/widgets/actions/download/widget-download.test.ts +30 -0
- package/src/widgets/actions/download/widget-download.ts +41 -0
- package/src/widgets/actions/fullscreen/fullscreen-store.test.tsx +155 -0
- package/src/widgets/actions/fullscreen/fullscreen.test.tsx +39 -41
- package/src/widgets/actions/fullscreen/fullscreen.tsx +198 -96
- package/src/widgets/actions/index.ts +3 -0
- package/src/widgets/actions/searcher/filter.test.ts +28 -0
- package/src/widgets/actions/searcher/filter.ts +22 -4
- package/src/widgets/actions/searcher/searcher-toggle.tsx +16 -8
- package/src/widgets/actions/searcher/searcher.test.tsx +22 -0
- package/src/widgets/actions/searcher/searcher.tsx +17 -7
- package/src/widgets/bar/download.test.tsx +5 -3
- package/src/widgets/bar/download.tsx +11 -35
- package/src/widgets/category/category-ui.tsx +26 -14
- package/src/widgets/category/category.tsx +37 -2
- package/src/widgets/category/components/category-legend.tsx +1 -2
- package/src/widgets/category/download.test.ts +4 -2
- package/src/widgets/category/download.ts +14 -27
- package/src/widgets/category/helpers.test.ts +17 -0
- package/src/widgets/category/helpers.ts +16 -0
- package/src/widgets/echart/style.ts +6 -0
- package/src/widgets/formula/download.test.tsx +3 -1
- package/src/widgets/formula/download.tsx +10 -30
- package/src/widgets/histogram/download.test.ts +6 -4
- package/src/widgets/histogram/download.ts +13 -26
- package/src/widgets/index.ts +1 -0
- package/src/widgets/markdown/download.test.ts +2 -2
- package/src/widgets/markdown/download.tsx +11 -26
- package/src/widgets/pie/download.test.ts +7 -3
- package/src/widgets/pie/download.ts +12 -25
- package/src/widgets/scatterplot/download.test.ts +4 -2
- package/src/widgets/scatterplot/download.ts +15 -28
- package/src/widgets/spread/download.test.ts +3 -1
- package/src/widgets/spread/download.ts +13 -27
- package/src/widgets/stores/index.ts +2 -0
- package/src/widgets/stores/widget-store-registry.test.ts +25 -0
- package/src/widgets/stores/widget-store-registry.ts +22 -0
- package/src/widgets/table/download.test.ts +5 -3
- package/src/widgets/table/download.ts +15 -29
- package/src/widgets/timeseries/download.test.ts +6 -4
- package/src/widgets/timeseries/download.ts +16 -29
- package/src/widgets/toolbox/toolbox.tsx +1 -1
- package/src/widgets/wrapper/style.ts +38 -2
- package/src/widgets/wrapper/widget-actions.tsx +2 -2
- package/src/widgets/wrapper/widget-wrapper.test.tsx +32 -2
- package/src/widgets/wrapper/widget-wrapper.tsx +62 -36
- package/dist/actions-KsX1xdDM.js +0 -1024
- package/dist/actions-KsX1xdDM.js.map +0 -1
- package/dist/category-CsdDqDzo.js +0 -833
- package/dist/category-CsdDqDzo.js.map +0 -1
- package/dist/download-Vlzn2jzb.js.map +0 -1
- package/dist/echart-DjcbtKgi.js.map +0 -1
- package/dist/stores-B8CHbgw-.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zoom-toggle-D_VuA01l.js","names":["Icon = ZoomInIcon","{\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps","initialEnabled = false","{ ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }","axes ? axes.includes('x') : true","axes ? axes.includes('y') : false","createAddZoom(theme, { axes: resolved })","useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])","{ initialEnabled }","() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }","useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])","{ ...styles.toggle, ...(enabled && styles.toggleActive) }","enabled && styles.toggleActive","<Icon fontSize='small' {...iconProps} />","<IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>","enabled ? 'active' : undefined","<Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>"],"sources":["../src/widgets/actions/zoom-toggle/transforms.ts","../src/widgets/actions/zoom-toggle/labels.ts","../src/widgets/actions/zoom-toggle/style.ts","../src/widgets/actions/zoom-toggle/zoom-toggle.tsx"],"sourcesContent":["import type { Theme } from '@mui/material'\n\n/**\n * v1's slider handle — 9×36 SVG with a white pill body, secondary-colored\n * stroke, and three grip lines. Inlined as a data URL so the chart can\n * render it without a network fetch.\n */\nconst SLIDER_HANDLE_ICON =\n 'image://data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIzNiIgdmlld0JveD0iMCAwIDkgMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNSIgeT0iLTAuNSIgd2lkdGg9IjgiIGhlaWdodD0iMTgiIHJ4PSI0IiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAwIDI3KSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzM1OEJFNyIvPgo8cmVjdCB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMyAyMykiIGZpbGw9IiMzNThCRTciLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDMgMTkpIiBmaWxsPSIjMzU4QkU3Ii8+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAzIDE1KSIgZmlsbD0iIzM1OEJFNyIvPgo8L3N2Zz4='\n\nconst SLIDER_HEIGHT = 32\nconst SLIDER_BOTTOM = 0\n\n/** Which chart axis (or axes) zoom applies to. */\nexport type ZoomAxis = 'x' | 'y'\n\nexport interface CreateAddZoomOptions {\n /**\n * Axes to enable zoom on. Default `['x']` (matches bar / histogram /\n * timeseries — horizontal pan/zoom on the category axis). Pass\n * `['x', 'y']` for a scatterplot-style 2D zoom (mouse-wheel scales both\n * axes; a horizontal slider sits at the bottom for x-range and a\n * vertical slider sits on the right for y-range).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Theme-bound zoom config transform: adds one inside `dataZoom`\n * (mouse-wheel + drag) per axis plus a styled slider per axis with the\n * v1 chrome (blue-tinted filler, secondary-color data preview, white\n * pill handles, CARTO typography). The x-slider is horizontal at the\n * bottom; the y-slider is vertical on the right.\n *\n * Returned function has stable identity for a given theme + axes\n * combination; pair with `useMemo` at the call site.\n *\n * **No `start` / `end`** are emitted: ECharts defaults to `0–100` on the\n * first `setOption` and then keeps the runtime slider state across\n * subsequent merges (no `replaceMerge` for `dataZoom`). Re-running the\n * transform on data/formatter/RelativeData updates therefore preserves the\n * user's dragged range instead of snapping it back.\n */\nexport function createAddZoom(\n theme: Theme,\n options: CreateAddZoomOptions = {},\n): (option: unknown) => unknown {\n const axes = options.axes ?? ['x']\n const includeX = axes.includes('x')\n const includeY = axes.includes('y')\n const sliderStyles = getZoomSliderStyles(theme)\n const baseSliderProps = {\n throttle: 0,\n showDetail: false,\n brushSelect: false,\n moveHandleSize: 8,\n handleSize: '100%',\n ...sliderStyles,\n }\n return (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n const dataZoom: object[] = []\n if (includeX) {\n dataZoom.push({ type: 'inside', xAxisIndex: [0], throttle: 0 })\n }\n if (includeY) {\n dataZoom.push({ type: 'inside', yAxisIndex: [0], throttle: 0 })\n }\n if (includeX) {\n dataZoom.push({\n type: 'slider',\n xAxisIndex: [0],\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n handleIcon: SLIDER_HANDLE_ICON,\n ...baseSliderProps,\n })\n }\n if (includeY) {\n // Vertical slider — `width` is its thickness, `right` pins it to\n // the right edge. We omit `handleIcon` so ECharts renders its\n // default vertical handle (the inline SVG is shaped for a\n // horizontal slider and looks wrong when rotated).\n dataZoom.push({\n type: 'slider',\n yAxisIndex: [0],\n width: SLIDER_HEIGHT,\n right: SLIDER_BOTTOM,\n ...baseSliderProps,\n })\n }\n return { ...cfg, dataZoom }\n }\n}\n\n/**\n * Back-compat untyped transform — produces the bare `dataZoom` config from\n * the original v2 implementation. Use {@link createAddZoom} when a theme is\n * available so the slider matches the v1 styling.\n *\n * `start` / `end` are intentionally omitted so ECharts preserves the user's\n * runtime range across subsequent `setOption` merges (see {@link createAddZoom}).\n */\nexport const addZoom = (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n return {\n ...cfg,\n dataZoom: [\n { type: 'inside' },\n {\n type: 'slider',\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n },\n ],\n }\n}\n\n/** Style block matching v1's `getEChartZoomSliderStyles`. */\nfunction getZoomSliderStyles(theme: Theme) {\n const secondary = theme.palette.secondary?.main ?? '#358BE7'\n return {\n fillerColor: 'rgba(53, 139, 231, 0.25)',\n borderColor: 'rgba(53, 139, 231, 0.3)',\n borderWidth: 0.5,\n backgroundColor: 'transparent',\n borderRadius: 4,\n dataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n selectedDataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n handleStyle: {\n color: theme.palette.common?.white ?? '#fff',\n borderColor: 'rgba(3, 111, 226, 0.08)',\n borderWidth: 1,\n shadowBlur: 3,\n shadowColor: 'rgba(0, 0, 0, 0.1)',\n shadowOffsetX: 0,\n shadowOffsetY: 1,\n },\n textStyle: {\n color: theme.palette.black?.[60] ?? theme.palette.text?.primary,\n fontSize: parseInt(\n (theme.typography.overlineDelicate?.fontSize as string | undefined) ??\n '11',\n ),\n fontFamily:\n theme.typography.overlineDelicate?.fontFamily ??\n theme.typography.caption?.fontFamily,\n },\n }\n}\n\n/**\n * Visual layout constants used by the data-fusion mergers (bar, histogram,\n * timeseries) when laying out the slider against the legend / grid.\n */\nexport const ZOOM_LAYOUT = {\n /** Slider height in px (matches `createAddZoom` output). */\n sliderHeight: SLIDER_HEIGHT,\n /** Vertical gap between chart grid and the slider. */\n sliderGap: 8,\n /** Slider `bottom` when a legend is shown — sits above the legend row. */\n sliderBottomWithLegend: 28,\n} as const\n","export interface ZoomToggleLabels {\n on: string\n off: string\n}\n\nexport const DEFAULT_ZOOM_TOGGLE_LABELS: ZoomToggleLabels = {\n on: 'Disable zoom',\n off: 'Enable zoom',\n}\n","import type { SxProps, Theme } from '@mui/material'\n\nexport const styles = {\n toggle: {\n p: 0.5,\n '& .MuiSvgIcon-root': { fontSize: 20 },\n },\n toggleActive: {\n background: (theme: Theme) => theme.palette.primary.relatedLight,\n },\n} satisfies Record<string, SxProps<Theme>>\n","import { useCallback, useMemo, type ComponentType } from 'react'\nimport { IconButton, useTheme, type SvgIconProps } from '@mui/material'\nimport { ZoomIn as ZoomInIcon } from '@mui/icons-material'\nimport { Tooltip } from '../../../components'\nimport {\n getEchartInstance,\n useSingleTransform,\n useWidgetId,\n} from '../../stores'\nimport { createAddZoom, type ZoomAxis } from './transforms'\nimport { DEFAULT_ZOOM_TOGGLE_LABELS, type ZoomToggleLabels } from './labels'\nimport { styles } from './style'\n\nconst ZOOM_DESCRIPTOR = {\n id: 'zoom-toggle',\n type: 'config' as const,\n order: 20,\n}\n\nexport interface ZoomToggleProps {\n initialEnabled?: boolean\n labels?: Partial<ZoomToggleLabels>\n icon?: ComponentType<SvgIconProps>\n iconProps?: SvgIconProps\n /**\n * Which axes the zoom controls. Default `['x']` — matches bar /\n * histogram / timeseries (horizontal pan on the category axis). Pass\n * `['x', 'y']` for 2D zoom on a scatterplot-style chart (mouse-wheel\n * scales both axes; horizontal slider at bottom + vertical slider on\n * the right).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Toggle a `dataZoom` slider + wheel-zoom on the chart. The transform is\n * theme-bound so the slider gets v1's CARTO chrome (blue-tinted filler,\n * secondary-color preview, white pill handles).\n *\n * `dataZoom` is intentionally NOT in `replaceMergeKeys` — ECharts merges\n * subsequent `setOption` calls so the user's dragged range survives\n * unrelated re-renders (data updates, formatter swaps, RelativeData toggles).\n *\n * On disable the transform is unregistered, but ECharts' merge semantics\n * would otherwise *keep* the live `dataZoom` component visible. To remove\n * the slider immediately we issue a one-shot\n * `setOption({}, { replaceMerge: ['dataZoom'] })` against the live chart\n * instance via {@link getEchartInstance} — this bypasses the pipeline so\n * unrelated transforms aren't disturbed.\n */\nexport function ZoomToggle({\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps) {\n const id = useWidgetId()\n const theme = useTheme()\n const _labels = { ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }\n // Reduce `axes` to two structural booleans before memoizing so the memo\n // depends on stable primitive values, not an array reference (callers\n // typically pass an inline `axes={['x', 'y']}` literal that changes\n // identity each render). Default — no `axes` prop — is x-axis only,\n // matching `createAddZoom`'s default.\n const includeX = axes ? axes.includes('x') : true\n const includeY = axes ? axes.includes('y') : false\n const transform = useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])\n const { enabled, toggle } = useSingleTransform(\n id,\n ZOOM_DESCRIPTOR,\n transform,\n { initialEnabled },\n )\n\n const handleToggle = useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])\n\n const tooltip = enabled ? _labels.on : _labels.off\n\n return (\n <Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"zoom-toggle-MmLjMTEw.js","names":["Icon = ZoomInIcon","{\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps","initialEnabled = false","{ ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }","axes ? axes.includes('x') : true","axes ? axes.includes('y') : false","createAddZoom(theme, { axes: resolved })","useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])","{ initialEnabled }","() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }","useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])","{ ...styles.toggle, ...(enabled && styles.toggleActive) }","enabled && styles.toggleActive","<Icon fontSize='small' {...iconProps} />","<IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>","enabled ? 'active' : undefined","<Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>"],"sources":["../src/widgets/actions/zoom-toggle/transforms.ts","../src/widgets/actions/zoom-toggle/labels.ts","../src/widgets/actions/zoom-toggle/style.ts","../src/widgets/actions/zoom-toggle/zoom-toggle.tsx"],"sourcesContent":["import type { Theme } from '@mui/material'\n\n/**\n * v1's slider handle — 9×36 SVG with a white pill body, secondary-colored\n * stroke, and three grip lines. Inlined as a data URL so the chart can\n * render it without a network fetch.\n */\nconst SLIDER_HANDLE_ICON =\n 'image://data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIzNiIgdmlld0JveD0iMCAwIDkgMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNSIgeT0iLTAuNSIgd2lkdGg9IjgiIGhlaWdodD0iMTgiIHJ4PSI0IiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAwIDI3KSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzM1OEJFNyIvPgo8cmVjdCB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMyAyMykiIGZpbGw9IiMzNThCRTciLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDMgMTkpIiBmaWxsPSIjMzU4QkU3Ii8+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAzIDE1KSIgZmlsbD0iIzM1OEJFNyIvPgo8L3N2Zz4='\n\nconst SLIDER_HEIGHT = 32\nconst SLIDER_BOTTOM = 0\n\n/** Which chart axis (or axes) zoom applies to. */\nexport type ZoomAxis = 'x' | 'y'\n\nexport interface CreateAddZoomOptions {\n /**\n * Axes to enable zoom on. Default `['x']` (matches bar / histogram /\n * timeseries — horizontal pan/zoom on the category axis). Pass\n * `['x', 'y']` for a scatterplot-style 2D zoom (mouse-wheel scales both\n * axes; a horizontal slider sits at the bottom for x-range and a\n * vertical slider sits on the right for y-range).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Theme-bound zoom config transform: adds one inside `dataZoom`\n * (mouse-wheel + drag) per axis plus a styled slider per axis with the\n * v1 chrome (blue-tinted filler, secondary-color data preview, white\n * pill handles, CARTO typography). The x-slider is horizontal at the\n * bottom; the y-slider is vertical on the right.\n *\n * Returned function has stable identity for a given theme + axes\n * combination; pair with `useMemo` at the call site.\n *\n * **No `start` / `end`** are emitted: ECharts defaults to `0–100` on the\n * first `setOption` and then keeps the runtime slider state across\n * subsequent merges (no `replaceMerge` for `dataZoom`). Re-running the\n * transform on data/formatter/RelativeData updates therefore preserves the\n * user's dragged range instead of snapping it back.\n */\nexport function createAddZoom(\n theme: Theme,\n options: CreateAddZoomOptions = {},\n): (option: unknown) => unknown {\n const axes = options.axes ?? ['x']\n const includeX = axes.includes('x')\n const includeY = axes.includes('y')\n const sliderStyles = getZoomSliderStyles(theme)\n const baseSliderProps = {\n throttle: 0,\n showDetail: false,\n brushSelect: false,\n moveHandleSize: 8,\n handleSize: '100%',\n ...sliderStyles,\n }\n return (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n const dataZoom: object[] = []\n if (includeX) {\n dataZoom.push({ type: 'inside', xAxisIndex: [0], throttle: 0 })\n }\n if (includeY) {\n dataZoom.push({ type: 'inside', yAxisIndex: [0], throttle: 0 })\n }\n if (includeX) {\n dataZoom.push({\n type: 'slider',\n xAxisIndex: [0],\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n handleIcon: SLIDER_HANDLE_ICON,\n ...baseSliderProps,\n })\n }\n if (includeY) {\n // Vertical slider — `width` is its thickness, `right` pins it to\n // the right edge. We omit `handleIcon` so ECharts renders its\n // default vertical handle (the inline SVG is shaped for a\n // horizontal slider and looks wrong when rotated).\n dataZoom.push({\n type: 'slider',\n yAxisIndex: [0],\n width: SLIDER_HEIGHT,\n right: SLIDER_BOTTOM,\n ...baseSliderProps,\n })\n }\n return { ...cfg, dataZoom }\n }\n}\n\n/**\n * Back-compat untyped transform — produces the bare `dataZoom` config from\n * the original v2 implementation. Use {@link createAddZoom} when a theme is\n * available so the slider matches the v1 styling.\n *\n * `start` / `end` are intentionally omitted so ECharts preserves the user's\n * runtime range across subsequent `setOption` merges (see {@link createAddZoom}).\n */\nexport const addZoom = (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n return {\n ...cfg,\n dataZoom: [\n { type: 'inside' },\n {\n type: 'slider',\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n },\n ],\n }\n}\n\n/** Style block matching v1's `getEChartZoomSliderStyles`. */\nfunction getZoomSliderStyles(theme: Theme) {\n const secondary = theme.palette.secondary?.main ?? '#358BE7'\n return {\n fillerColor: 'rgba(53, 139, 231, 0.25)',\n borderColor: 'rgba(53, 139, 231, 0.3)',\n borderWidth: 0.5,\n backgroundColor: 'transparent',\n borderRadius: 4,\n dataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n selectedDataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n handleStyle: {\n color: theme.palette.common?.white ?? '#fff',\n borderColor: 'rgba(3, 111, 226, 0.08)',\n borderWidth: 1,\n shadowBlur: 3,\n shadowColor: 'rgba(0, 0, 0, 0.1)',\n shadowOffsetX: 0,\n shadowOffsetY: 1,\n },\n textStyle: {\n color: theme.palette.black?.[60] ?? theme.palette.text?.primary,\n fontSize: parseInt(\n (theme.typography.overlineDelicate?.fontSize as string | undefined) ??\n '11',\n ),\n fontFamily:\n theme.typography.overlineDelicate?.fontFamily ??\n theme.typography.caption?.fontFamily,\n },\n }\n}\n\n/**\n * Visual layout constants used by the data-fusion mergers (bar, histogram,\n * timeseries) when laying out the slider against the legend / grid.\n */\nexport const ZOOM_LAYOUT = {\n /** Slider height in px (matches `createAddZoom` output). */\n sliderHeight: SLIDER_HEIGHT,\n /** Vertical gap between chart grid and the slider. */\n sliderGap: 8,\n /** Slider `bottom` when a legend is shown — sits above the legend row. */\n sliderBottomWithLegend: 28,\n} as const\n","export interface ZoomToggleLabels {\n on: string\n off: string\n}\n\nexport const DEFAULT_ZOOM_TOGGLE_LABELS: ZoomToggleLabels = {\n on: 'Disable zoom',\n off: 'Enable zoom',\n}\n","import type { SxProps, Theme } from '@mui/material'\n\nexport const styles = {\n toggle: {\n p: 0.5,\n '& .MuiSvgIcon-root': { fontSize: 20 },\n },\n toggleActive: {\n background: (theme: Theme) => theme.palette.primary.relatedLight,\n },\n} satisfies Record<string, SxProps<Theme>>\n","import { useCallback, useMemo, type ComponentType } from 'react'\nimport { IconButton, useTheme, type SvgIconProps } from '@mui/material'\nimport { ZoomIn as ZoomInIcon } from '@mui/icons-material'\nimport { Tooltip } from '../../../components'\nimport {\n getEchartInstance,\n useSingleTransform,\n useWidgetId,\n} from '../../stores'\nimport { createAddZoom, type ZoomAxis } from './transforms'\nimport { DEFAULT_ZOOM_TOGGLE_LABELS, type ZoomToggleLabels } from './labels'\nimport { styles } from './style'\n\nconst ZOOM_DESCRIPTOR = {\n id: 'zoom-toggle',\n type: 'config' as const,\n order: 20,\n}\n\nexport interface ZoomToggleProps {\n initialEnabled?: boolean\n labels?: Partial<ZoomToggleLabels>\n icon?: ComponentType<SvgIconProps>\n iconProps?: SvgIconProps\n /**\n * Which axes the zoom controls. Default `['x']` — matches bar /\n * histogram / timeseries (horizontal pan on the category axis). Pass\n * `['x', 'y']` for 2D zoom on a scatterplot-style chart (mouse-wheel\n * scales both axes; horizontal slider at bottom + vertical slider on\n * the right).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Toggle a `dataZoom` slider + wheel-zoom on the chart. The transform is\n * theme-bound so the slider gets v1's CARTO chrome (blue-tinted filler,\n * secondary-color preview, white pill handles).\n *\n * `dataZoom` is intentionally NOT in `replaceMergeKeys` — ECharts merges\n * subsequent `setOption` calls so the user's dragged range survives\n * unrelated re-renders (data updates, formatter swaps, RelativeData toggles).\n *\n * On disable the transform is unregistered, but ECharts' merge semantics\n * would otherwise *keep* the live `dataZoom` component visible. To remove\n * the slider immediately we issue a one-shot\n * `setOption({}, { replaceMerge: ['dataZoom'] })` against the live chart\n * instance via {@link getEchartInstance} — this bypasses the pipeline so\n * unrelated transforms aren't disturbed.\n */\nexport function ZoomToggle({\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps) {\n const id = useWidgetId()\n const theme = useTheme()\n const _labels = { ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }\n // Reduce `axes` to two structural booleans before memoizing so the memo\n // depends on stable primitive values, not an array reference (callers\n // typically pass an inline `axes={['x', 'y']}` literal that changes\n // identity each render). Default — no `axes` prop — is x-axis only,\n // matching `createAddZoom`'s default.\n const includeX = axes ? axes.includes('x') : true\n const includeY = axes ? axes.includes('y') : false\n const transform = useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])\n const { enabled, toggle } = useSingleTransform(\n id,\n ZOOM_DESCRIPTOR,\n transform,\n { initialEnabled },\n )\n\n const handleToggle = useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])\n\n const tooltip = enabled ? _labels.on : _labels.off\n\n return (\n <Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>\n )\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
3
|
import { formatNumber } from '@carto/ps-utils'
|
|
4
4
|
import { LegendItemUI } from './legend-item-ui'
|
|
5
5
|
import { LegendRampUI } from '../legend-ramp/legend-ramp-ui'
|
|
@@ -174,6 +174,27 @@ describe('<LegendRampUI> ordering (regression)', () => {
|
|
|
174
174
|
expect(screen.getByText('d').style.left).toBe('100%')
|
|
175
175
|
})
|
|
176
176
|
|
|
177
|
+
it('discrete steps show their value range on hover, even with hidden labels', async () => {
|
|
178
|
+
const data: LegendRampVariable = {
|
|
179
|
+
type: 'ramp',
|
|
180
|
+
isContinuous: false,
|
|
181
|
+
colors: ['#aaa', '#bbb', '#ccc'],
|
|
182
|
+
stops: [
|
|
183
|
+
{ label: '0', value: 0 },
|
|
184
|
+
{ label: '', value: 10 },
|
|
185
|
+
{ label: '', value: 20 },
|
|
186
|
+
{ label: '30', value: 30 },
|
|
187
|
+
],
|
|
188
|
+
formatValue: (v) => `${v}%`,
|
|
189
|
+
}
|
|
190
|
+
render(<LegendRampUI data={data} />)
|
|
191
|
+
const steps = screen.getAllByRole('img')
|
|
192
|
+
// Hidden labels fall back to the raw value in the accessible name.
|
|
193
|
+
expect(steps[1]!.getAttribute('aria-label')).toBe('10 – 20')
|
|
194
|
+
fireEvent.mouseOver(steps[1]!)
|
|
195
|
+
expect((await screen.findByRole('tooltip')).textContent).toBe('10% – 20%')
|
|
196
|
+
})
|
|
197
|
+
|
|
177
198
|
it('invalid discrete (stops ≠ colors + 1) renders an error, not a bar', () => {
|
|
178
199
|
const data: LegendRampVariable = {
|
|
179
200
|
type: 'ramp',
|
|
@@ -55,8 +55,11 @@ export function LegendRampUI({
|
|
|
55
55
|
const invalidDiscrete = !isContinuous && !isBoundary
|
|
56
56
|
|
|
57
57
|
// Boundary range for assistive tech (valid discrete only reaches here).
|
|
58
|
+
// Falls back to the raw value when a label is left empty to hide it.
|
|
59
|
+
const stopText = (index: number): string =>
|
|
60
|
+
stops[index]!.label || String(stops[index]!.value)
|
|
58
61
|
const stepAriaLabel = (index: number): string =>
|
|
59
|
-
`${
|
|
62
|
+
`${stopText(index)} – ${stopText(index + 1)}`
|
|
60
63
|
|
|
61
64
|
const attributeHeader = attribute && (
|
|
62
65
|
<Box sx={styles.attribute}>
|
|
@@ -106,13 +109,26 @@ export function LegendRampUI({
|
|
|
106
109
|
sx={{ width: '100%', height: '100%', background: gradient }}
|
|
107
110
|
/>
|
|
108
111
|
) : (
|
|
112
|
+
// Each step's tooltip shows its full range from the stop values,
|
|
113
|
+
// so it works even when only the outer labels are rendered.
|
|
109
114
|
colors.map((color, index) => (
|
|
110
|
-
<
|
|
115
|
+
<Tooltip
|
|
111
116
|
key={index}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
title={
|
|
118
|
+
<>
|
|
119
|
+
{formatValue(stops[index]!.value)} –{' '}
|
|
120
|
+
{formatValue(stops[index + 1]!.value)}
|
|
121
|
+
</>
|
|
122
|
+
}
|
|
123
|
+
placement='top'
|
|
124
|
+
arrow
|
|
125
|
+
>
|
|
126
|
+
<Box
|
|
127
|
+
role='img'
|
|
128
|
+
aria-label={stepAriaLabel(index)}
|
|
129
|
+
sx={{ ...styles.step, backgroundColor: color }}
|
|
130
|
+
/>
|
|
131
|
+
</Tooltip>
|
|
116
132
|
))
|
|
117
133
|
)}
|
|
118
134
|
</Box>
|
|
@@ -96,7 +96,8 @@ export interface LegendRampVariable {
|
|
|
96
96
|
* Ordered low→high stops. Discrete ramps **require `colors.length + 1`**
|
|
97
97
|
* stops — the boundaries between color steps, including both outer edges —
|
|
98
98
|
* and each label is centered on its boundary line (keep labels short, e.g.
|
|
99
|
-
* `20K`; there is no collision handling
|
|
99
|
+
* `20K`; there is no collision handling; pass `''` to hide one — each step's
|
|
100
|
+
* tooltip still shows its full range). A discrete ramp with any other
|
|
100
101
|
* stop count is incoherent and renders an inline error instead of the bar.
|
|
101
102
|
* Continuous ramps show only the first/last endpoints.
|
|
102
103
|
*/
|
|
@@ -110,7 +111,8 @@ export interface LegendRampVariable {
|
|
|
110
111
|
*/
|
|
111
112
|
avg?: number
|
|
112
113
|
/**
|
|
113
|
-
* Formats `avg`
|
|
114
|
+
* Formats `avg` and, on discrete ramps, the stop values shown in each
|
|
115
|
+
* step's range tooltip. Default `String(value)` — pass e.g.
|
|
114
116
|
* `(v) => formatNumber(v, 'es-ES')` or `(v) => `${v}%``. Use a stable
|
|
115
117
|
* reference (same contract as the other variable fields).
|
|
116
118
|
*/
|
|
@@ -47,7 +47,7 @@ describe('buildCsvDownloadItem', () => {
|
|
|
47
47
|
['a', 1],
|
|
48
48
|
],
|
|
49
49
|
})
|
|
50
|
-
const handle = await item.resolve()
|
|
50
|
+
const handle = await item.resolve({ id: null })
|
|
51
51
|
expect(handle.url).toBe('blob:csv')
|
|
52
52
|
expect(handle.filename).toBe('sales.csv')
|
|
53
53
|
expect(csvText).toBe('name,value\na,1')
|
|
@@ -60,7 +60,7 @@ describe('buildCsvDownloadItem', () => {
|
|
|
60
60
|
filename: 'sales',
|
|
61
61
|
getRows: () => [['=SUM(A1)']],
|
|
62
62
|
})
|
|
63
|
-
await item.resolve()
|
|
63
|
+
await item.resolve({ id: null })
|
|
64
64
|
expect(csvText).toBe("'=SUM(A1)")
|
|
65
65
|
})
|
|
66
66
|
|
|
@@ -69,7 +69,7 @@ describe('buildCsvDownloadItem', () => {
|
|
|
69
69
|
filename: 'table',
|
|
70
70
|
getCsv: () => 'col\n=raw',
|
|
71
71
|
})
|
|
72
|
-
const handle = await item.resolve()
|
|
72
|
+
const handle = await item.resolve({ id: null })
|
|
73
73
|
expect(handle.filename).toBe('table.csv')
|
|
74
74
|
// getCsv is an escape hatch — content is used as-is, no extra escaping.
|
|
75
75
|
expect(csvText).toBe('col\n=raw')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DOWNLOAD_ITEM_IDS } from './constants'
|
|
2
2
|
import { downloadToCSV } from './exports'
|
|
3
3
|
import { CSVIcon } from './icons'
|
|
4
|
-
import type { DownloadItem } from './types'
|
|
4
|
+
import type { DownloadContext, DownloadItem } from './types'
|
|
5
5
|
|
|
6
6
|
interface BuildCsvDownloadItemBase {
|
|
7
7
|
/** Base filename (without extension). The item appends `.csv`. */
|
|
@@ -23,7 +23,7 @@ export type BuildCsvDownloadItemArgs =
|
|
|
23
23
|
* through `toCsvString` so escaping (incl. the spreadsheet
|
|
24
24
|
* formula-injection guard) stays consistent across widgets.
|
|
25
25
|
*/
|
|
26
|
-
getRows: () => readonly (readonly unknown[])[]
|
|
26
|
+
getRows: (ctx: DownloadContext) => readonly (readonly unknown[])[]
|
|
27
27
|
getCsv?: never
|
|
28
28
|
})
|
|
29
29
|
| (BuildCsvDownloadItemBase & {
|
|
@@ -32,7 +32,7 @@ export type BuildCsvDownloadItemArgs =
|
|
|
32
32
|
* (e.g. Table) that already serialise their own CSV with bespoke
|
|
33
33
|
* header/cell handling.
|
|
34
34
|
*/
|
|
35
|
-
getCsv: () => string
|
|
35
|
+
getCsv: (ctx: DownloadContext) => string
|
|
36
36
|
getRows?: never
|
|
37
37
|
})
|
|
38
38
|
|
|
@@ -49,9 +49,9 @@ export function buildCsvDownloadItem(
|
|
|
49
49
|
id: DOWNLOAD_ITEM_IDS.csv,
|
|
50
50
|
label: args.label ?? 'CSV',
|
|
51
51
|
icon: <CSVIcon fontSize='small' />,
|
|
52
|
-
resolve: () => {
|
|
52
|
+
resolve: (ctx) => {
|
|
53
53
|
if (args.getCsv) {
|
|
54
|
-
const csv = args.getCsv()
|
|
54
|
+
const csv = args.getCsv(ctx)
|
|
55
55
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
|
56
56
|
const url = URL.createObjectURL(blob)
|
|
57
57
|
return Promise.resolve({
|
|
@@ -60,7 +60,7 @@ export function buildCsvDownloadItem(
|
|
|
60
60
|
revoke: () => URL.revokeObjectURL(url),
|
|
61
61
|
})
|
|
62
62
|
}
|
|
63
|
-
const handle = downloadToCSV(args.getRows())
|
|
63
|
+
const handle = downloadToCSV(args.getRows(ctx))
|
|
64
64
|
return Promise.resolve({
|
|
65
65
|
url: handle.url,
|
|
66
66
|
filename: `${args.filename}.csv`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useState, type ComponentType } from 'react'
|
|
1
|
+
import { useCallback, useContext, useState, type ComponentType } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
CircularProgress,
|
|
4
4
|
IconButton,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '@mui/material'
|
|
11
11
|
import { FileDownload as DownloadIcon } from '@mui/icons-material'
|
|
12
12
|
import { Tooltip } from '../../../components'
|
|
13
|
+
import { WidgetContext } from '../../stores'
|
|
13
14
|
import { triggerLinkDownload } from './exports'
|
|
14
15
|
import type { DownloadItem } from './types'
|
|
15
16
|
import { DEFAULT_DOWNLOAD_LABELS, type DownloadLabels } from './labels'
|
|
@@ -37,6 +38,7 @@ export function Download({
|
|
|
37
38
|
}: DownloadProps) {
|
|
38
39
|
const _labels = { ...DEFAULT_DOWNLOAD_LABELS, ...labels }
|
|
39
40
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
|
41
|
+
const id = useContext(WidgetContext)
|
|
40
42
|
const [isDownloading, setIsDownloading] = useState(false)
|
|
41
43
|
const [error, setError] = useState<Error | null>(null)
|
|
42
44
|
|
|
@@ -59,7 +61,7 @@ export function Download({
|
|
|
59
61
|
setIsDownloading(true)
|
|
60
62
|
setError(null)
|
|
61
63
|
item
|
|
62
|
-
.resolve()
|
|
64
|
+
.resolve({ id })
|
|
63
65
|
.then(({ url, filename, revoke }) => {
|
|
64
66
|
// `item.filename` overrides what `resolve()` returns — lets consumers
|
|
65
67
|
// template the filename with widget metadata declaratively.
|
|
@@ -78,7 +80,7 @@ export function Download({
|
|
|
78
80
|
setIsDownloading(false)
|
|
79
81
|
})
|
|
80
82
|
},
|
|
81
|
-
[close, isDownloading, onError],
|
|
83
|
+
[close, id, isDownloading, onError],
|
|
82
84
|
)
|
|
83
85
|
|
|
84
86
|
const triggerLabel = isDownloading
|
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
export { CSVIcon, PNGIcon, MarkdownIcon } from './icons'
|
|
11
11
|
export { buildPngDownloadItem, type BuildPngDownloadItemArgs } from './png-item'
|
|
12
12
|
export { buildCsvDownloadItem, type BuildCsvDownloadItemArgs } from './csv-item'
|
|
13
|
+
export { withPngItem, type WidgetDownloadArgs } from './widget-download'
|
|
13
14
|
export { DOWNLOAD_ITEM_IDS, type DownloadItemId } from './constants'
|
|
14
|
-
export type { DownloadItem } from './types'
|
|
15
|
+
export type { DownloadItem, DownloadContext } from './types'
|
|
15
16
|
export { DEFAULT_DOWNLOAD_LABELS, type DownloadLabels } from './labels'
|
|
@@ -44,7 +44,7 @@ describe('buildPngDownloadItem', () => {
|
|
|
44
44
|
filename: 'sales',
|
|
45
45
|
getCaptureEl: () => document.createElement('div'),
|
|
46
46
|
})
|
|
47
|
-
const handle = await item.resolve()
|
|
47
|
+
const handle = await item.resolve({ id: null })
|
|
48
48
|
expect(handle.url).toBe('blob:png')
|
|
49
49
|
expect(handle.filename).toBe('sales.png')
|
|
50
50
|
handle.revoke?.()
|
|
@@ -56,7 +56,9 @@ describe('buildPngDownloadItem', () => {
|
|
|
56
56
|
filename: 'demo',
|
|
57
57
|
getCaptureEl: () => null,
|
|
58
58
|
})
|
|
59
|
-
await expect(item.resolve()).rejects.toThrow(
|
|
59
|
+
await expect(item.resolve({ id: null })).rejects.toThrow(
|
|
60
|
+
/No PNG capture element/,
|
|
61
|
+
)
|
|
60
62
|
})
|
|
61
63
|
|
|
62
64
|
it('forwards pixelRatio and backgroundColor overrides', async () => {
|
|
@@ -66,7 +68,7 @@ describe('buildPngDownloadItem', () => {
|
|
|
66
68
|
pixelRatio: 4,
|
|
67
69
|
backgroundColor: '#fff',
|
|
68
70
|
})
|
|
69
|
-
const handle = await item.resolve()
|
|
71
|
+
const handle = await item.resolve({ id: null })
|
|
70
72
|
expect(handle.filename).toBe('sales.png')
|
|
71
73
|
})
|
|
72
74
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DOWNLOAD_ITEM_IDS } from './constants'
|
|
2
2
|
import { downloadDOMToPNG } from './exports'
|
|
3
3
|
import { PNGIcon } from './icons'
|
|
4
|
-
import type { DownloadItem } from './types'
|
|
4
|
+
import type { DownloadContext, DownloadItem } from './types'
|
|
5
5
|
|
|
6
6
|
export interface BuildPngDownloadItemArgs {
|
|
7
7
|
/** Base filename (without extension). The item appends `.png`. */
|
|
@@ -11,7 +11,7 @@ export interface BuildPngDownloadItemArgs {
|
|
|
11
11
|
* download config doesn't capture a stale reference. Wire it to
|
|
12
12
|
* `() => getCaptureEl(id)` from `widgets/stores`.
|
|
13
13
|
*/
|
|
14
|
-
getCaptureEl: () => HTMLElement | null
|
|
14
|
+
getCaptureEl: (ctx: DownloadContext) => HTMLElement | null
|
|
15
15
|
/** html2canvas `scale`. Default 2. */
|
|
16
16
|
pixelRatio?: number
|
|
17
17
|
/** html2canvas `backgroundColor`. Default transparent (`null`). */
|
|
@@ -33,8 +33,8 @@ export function buildPngDownloadItem(
|
|
|
33
33
|
id: DOWNLOAD_ITEM_IDS.png,
|
|
34
34
|
label: args.label ?? 'PNG',
|
|
35
35
|
icon: <PNGIcon fontSize='small' />,
|
|
36
|
-
resolve: async () => {
|
|
37
|
-
const el = args.getCaptureEl()
|
|
36
|
+
resolve: async (ctx) => {
|
|
37
|
+
const el = args.getCaptureEl(ctx)
|
|
38
38
|
if (!el) {
|
|
39
39
|
throw new Error('[widgets] No PNG capture element available')
|
|
40
40
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { ReactElement } from 'react'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Passed to {@link DownloadItem.resolve} by `<Widget.Download>`.
|
|
5
|
+
*/
|
|
6
|
+
export interface DownloadContext {
|
|
7
|
+
/**
|
|
8
|
+
* Id of the widget store the menu was opened from — the dialog's own
|
|
9
|
+
* store when opened from `FullScreen.Slot`'s header, so an export matches
|
|
10
|
+
* what the user is looking at. `null` outside a `<Widget.Provider>`.
|
|
11
|
+
*/
|
|
12
|
+
id: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export interface DownloadItem {
|
|
4
16
|
/** Unique key (also used as menu label key). */
|
|
5
17
|
id: string
|
|
@@ -28,5 +40,7 @@ export interface DownloadItem {
|
|
|
28
40
|
* Resolves to a Blob URL the trigger will click. Optional `revoke` is
|
|
29
41
|
* called after the click is dispatched.
|
|
30
42
|
*/
|
|
31
|
-
resolve: (
|
|
43
|
+
resolve: (
|
|
44
|
+
ctx: DownloadContext,
|
|
45
|
+
) => Promise<{ url: string; filename: string; revoke?: () => void }>
|
|
32
46
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { buildCsvDownloadItem } from './csv-item'
|
|
3
|
+
import { withPngItem } from './widget-download'
|
|
4
|
+
|
|
5
|
+
vi.mock('html2canvas', () => ({
|
|
6
|
+
default: () =>
|
|
7
|
+
Promise.resolve({
|
|
8
|
+
toBlob: (cb: (blob: Blob | null) => void) => cb(new Blob(['png'])),
|
|
9
|
+
}),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
const csv = buildCsvDownloadItem({ filename: 'f', getRows: () => [] })
|
|
13
|
+
|
|
14
|
+
describe('withPngItem', () => {
|
|
15
|
+
it('returns the items unchanged without getCaptureEl', () => {
|
|
16
|
+
expect(withPngItem({ filename: 'f' }, csv)).toEqual([csv])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('puts a PNG item first when getCaptureEl is given, and uses it', async () => {
|
|
20
|
+
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:png')
|
|
21
|
+
const getCaptureEl = vi.fn(() => document.createElement('div'))
|
|
22
|
+
const [png, rest] = withPngItem({ filename: 'f', getCaptureEl }, csv)
|
|
23
|
+
expect(png?.id).toBe('png')
|
|
24
|
+
expect(rest).toBe(csv)
|
|
25
|
+
await expect(png!.resolve({ id: 'w' })).resolves.toMatchObject({
|
|
26
|
+
filename: 'f.png',
|
|
27
|
+
})
|
|
28
|
+
expect(getCaptureEl).toHaveBeenCalledWith({ id: 'w' })
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { buildPngDownloadItem } from './png-item'
|
|
2
|
+
import type { DownloadContext, DownloadItem } from './types'
|
|
3
|
+
|
|
4
|
+
/** Arguments every per-widget `create*DownloadConfig` shares. */
|
|
5
|
+
export interface WidgetDownloadArgs<T> {
|
|
6
|
+
/** Base filename (without extension). */
|
|
7
|
+
filename: string
|
|
8
|
+
/**
|
|
9
|
+
* Reads the data to export at click time. `ctx.id` is the store the menu
|
|
10
|
+
* was opened from — the fullscreen dialog's own while it's open. See
|
|
11
|
+
* `widgetStoreData` in `widgets/stores`.
|
|
12
|
+
*/
|
|
13
|
+
getData: (ctx: DownloadContext) => T
|
|
14
|
+
/**
|
|
15
|
+
* Reads the element the PNG item rasterises. When given, a PNG item is
|
|
16
|
+
* prepended to the menu. See `widgetCaptureEl` in `widgets/stores`.
|
|
17
|
+
*/
|
|
18
|
+
getCaptureEl?: (ctx: DownloadContext) => HTMLElement | null
|
|
19
|
+
/** PNG `pixelRatio` (default 2). */
|
|
20
|
+
pngPixelRatio?: number
|
|
21
|
+
/** PNG `backgroundColor` (default transparent). */
|
|
22
|
+
pngBackgroundColor?: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `items`, with a PNG item in front when `args.getCaptureEl` is given — the
|
|
27
|
+
* order every widget's download menu uses.
|
|
28
|
+
*/
|
|
29
|
+
export function withPngItem(
|
|
30
|
+
args: Omit<WidgetDownloadArgs<unknown>, 'getData'>,
|
|
31
|
+
...items: DownloadItem[]
|
|
32
|
+
): DownloadItem[] {
|
|
33
|
+
if (!args.getCaptureEl) return items
|
|
34
|
+
const png = buildPngDownloadItem({
|
|
35
|
+
filename: args.filename,
|
|
36
|
+
getCaptureEl: args.getCaptureEl,
|
|
37
|
+
pixelRatio: args.pngPixelRatio,
|
|
38
|
+
backgroundColor: args.pngBackgroundColor,
|
|
39
|
+
})
|
|
40
|
+
return [png, ...items]
|
|
41
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
2
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import { Provider } from '../../provider/widget-provider'
|
|
4
|
+
import type { ECharts } from 'echarts'
|
|
5
|
+
import {
|
|
6
|
+
clearAllWidgetStores,
|
|
7
|
+
getWidgetStore,
|
|
8
|
+
hasWidgetStore,
|
|
9
|
+
setEchartInstance,
|
|
10
|
+
useWidget,
|
|
11
|
+
useWidgetId,
|
|
12
|
+
} from '../../stores'
|
|
13
|
+
import { Download } from '../download/download'
|
|
14
|
+
import { FullScreen, fullScreenStoreId } from './fullscreen'
|
|
15
|
+
|
|
16
|
+
afterEach(() => clearAllWidgetStores())
|
|
17
|
+
|
|
18
|
+
// A stand-in for any action: reads and writes one flag on whichever store
|
|
19
|
+
// its copy runs on.
|
|
20
|
+
function Flag() {
|
|
21
|
+
const id = useWidgetId()
|
|
22
|
+
const on = useWidget(id, (s) => s.transformStates.flag?.enabled ?? false)
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type='button'
|
|
26
|
+
data-testid={`flag-${id}`}
|
|
27
|
+
aria-pressed={on}
|
|
28
|
+
onClick={() =>
|
|
29
|
+
getWidgetStore(id).setState((s) => ({
|
|
30
|
+
transformStates: { ...s.transformStates, flag: { enabled: !on } },
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
flag
|
|
35
|
+
</button>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function DataProbe() {
|
|
40
|
+
const id = useWidgetId()
|
|
41
|
+
const data = useWidget(id, (s) => s.rawData)
|
|
42
|
+
return <span data-testid={`data-${id}`}>{String(data)}</span>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Widget({ data = 'a' }: { data?: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<Provider id='w' data={data}>
|
|
48
|
+
<FullScreen.Trigger labels={{ open: 'OpenT', close: 'CloseT' }} />
|
|
49
|
+
<FullScreen.Slot>
|
|
50
|
+
<Flag />
|
|
51
|
+
<DataProbe />
|
|
52
|
+
</FullScreen.Slot>
|
|
53
|
+
</Provider>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const dialogId = fullScreenStoreId('w')
|
|
58
|
+
const flag = (id: string) =>
|
|
59
|
+
screen.getByTestId(`flag-${id}`).getAttribute('aria-pressed')
|
|
60
|
+
|
|
61
|
+
async function close() {
|
|
62
|
+
fireEvent.click(screen.getByLabelText('CloseT'))
|
|
63
|
+
await waitFor(() =>
|
|
64
|
+
expect(screen.queryByTestId(`flag-${dialogId}`)).toBeNull(),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('<FullScreen.Slot> — the dialog runs on its own store', () => {
|
|
69
|
+
it('starts from the card, runs apart while open, and hands back on close', async () => {
|
|
70
|
+
render(<Widget />)
|
|
71
|
+
fireEvent.click(screen.getByTestId('flag-w'))
|
|
72
|
+
expect(flag('w')).toBe('true')
|
|
73
|
+
|
|
74
|
+
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
75
|
+
// Seeded from the card.
|
|
76
|
+
expect(flag(dialogId)).toBe('true')
|
|
77
|
+
|
|
78
|
+
// Changes in the dialog don't reach the card while open…
|
|
79
|
+
fireEvent.click(screen.getByTestId(`flag-${dialogId}`))
|
|
80
|
+
expect(flag(dialogId)).toBe('false')
|
|
81
|
+
expect(flag('w')).toBe('true')
|
|
82
|
+
|
|
83
|
+
// …and do once it closes. The dialog's store goes with it.
|
|
84
|
+
await close()
|
|
85
|
+
expect(flag('w')).toBe('false')
|
|
86
|
+
expect(hasWidgetStore(dialogId)).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('keeps the consumer props flowing into the dialog while it is open', () => {
|
|
90
|
+
const { rerender } = render(<Widget data='a' />)
|
|
91
|
+
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
92
|
+
expect(screen.getByTestId(`data-${dialogId}`).textContent).toBe('a')
|
|
93
|
+
rerender(<Widget data='b' />)
|
|
94
|
+
expect(screen.getByTestId(`data-${dialogId}`).textContent).toBe('b')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('reopening starts from the card again, not from the last dialog', async () => {
|
|
98
|
+
render(<Widget />)
|
|
99
|
+
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
100
|
+
await close()
|
|
101
|
+
act(() =>
|
|
102
|
+
getWidgetStore('w').setState({
|
|
103
|
+
transformStates: { flag: { enabled: true } },
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
107
|
+
expect(flag(dialogId)).toBe('true')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('downloads from the dialog header resolve against the dialog store', async () => {
|
|
111
|
+
const resolve = vi.fn(() =>
|
|
112
|
+
Promise.resolve({ url: 'blob:x', filename: 'x.csv' }),
|
|
113
|
+
)
|
|
114
|
+
render(
|
|
115
|
+
<Provider id='w' data='a'>
|
|
116
|
+
<FullScreen.Trigger labels={{ open: 'OpenT', close: 'CloseT' }} />
|
|
117
|
+
<FullScreen.Slot
|
|
118
|
+
actions={<Download items={[{ id: 'csv', label: 'CSV', resolve }]} />}
|
|
119
|
+
>
|
|
120
|
+
<span>body</span>
|
|
121
|
+
</FullScreen.Slot>
|
|
122
|
+
</Provider>,
|
|
123
|
+
)
|
|
124
|
+
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
125
|
+
fireEvent.click(screen.getByLabelText('Download'))
|
|
126
|
+
fireEvent.click(await screen.findByText('CSV'))
|
|
127
|
+
expect(resolve).toHaveBeenCalledWith({ id: dialogId })
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Legend / dataZoom / brush live in the chart instance, not the store.
|
|
131
|
+
// The Slot moves them through the instance registry, so Widget.Echart
|
|
132
|
+
// doesn't need to know about fullscreen.
|
|
133
|
+
it('starts the dialog chart from the card chart and hands it back on close', async () => {
|
|
134
|
+
const fakeChart = (option: object) =>
|
|
135
|
+
({
|
|
136
|
+
getOption: vi.fn(() => option),
|
|
137
|
+
setOption: vi.fn(),
|
|
138
|
+
dispatchAction: vi.fn(),
|
|
139
|
+
isDisposed: () => false,
|
|
140
|
+
}) as unknown as ECharts & {
|
|
141
|
+
setOption: ReturnType<typeof vi.fn>
|
|
142
|
+
}
|
|
143
|
+
const card = fakeChart({ from: 'card' })
|
|
144
|
+
const dialog = fakeChart({ from: 'dialog' })
|
|
145
|
+
render(<Widget />)
|
|
146
|
+
act(() => setEchartInstance('w', card))
|
|
147
|
+
|
|
148
|
+
fireEvent.click(screen.getByLabelText('OpenT'))
|
|
149
|
+
act(() => setEchartInstance(dialogId, dialog))
|
|
150
|
+
expect(dialog.setOption).toHaveBeenCalledWith({ from: 'card' })
|
|
151
|
+
|
|
152
|
+
await close()
|
|
153
|
+
expect(card.setOption).toHaveBeenCalledWith({ from: 'dialog' })
|
|
154
|
+
})
|
|
155
|
+
})
|