From 589f7870764580399a38c9c2cdd40b89e0e51289 Mon Sep 17 00:00:00 2001 From: Marcus Notheis Date: Mon, 22 Jul 2019 10:33:44 +0200 Subject: [PATCH] fix(Charts): Show Loading Placeholder (#64) Added snapshot tests for placeholders --- config/jestsetup.ts | 2 + .../src/components/BarChart/BarChart.test.tsx | 13 +- .../src/components/BarChart/Placeholder.tsx | 3 +- .../__snapshots__/BarChart.test.tsx.snap | 118 ++++++++++ .../src/components/BarChart/demo.stories.tsx | 3 +- .../charts/src/components/BarChart/index.tsx | 211 +++++++++-------- .../ColumnChart/ColumnChart.test.tsx | 7 +- .../__snapshots__/ColumnChart.test.tsx.snap | 118 ++++++++++ .../components/ColumnChart/demo.stories.tsx | 3 +- .../src/components/ColumnChart/index.tsx | 215 +++++++++--------- .../components/DonutChart/DonutChart.test.tsx | 7 +- .../__snapshots__/DonutChart.test.tsx.snap | 81 +++++++ .../components/DonutChart/demo.stories.tsx | 3 +- .../src/components/DonutChart/index.tsx | 158 ++++++------- .../components/LineChart/LineChart.test.tsx | 7 +- .../__snapshots__/LineChart.test.tsx.snap | 81 +++++++ .../src/components/LineChart/demo.stories.tsx | 3 +- .../charts/src/components/LineChart/index.tsx | 168 +++++++------- .../src/components/PieChart/PieChart.test.tsx | 7 +- .../__snapshots__/PieChart.test.tsx.snap | 81 +++++++ .../src/components/PieChart/demo.stories.tsx | 3 +- .../charts/src/components/PieChart/index.tsx | 156 ++++++------- .../src/components/RadarChart/index.tsx | 147 ++++++------ .../serializer/content-loader-serializer.js | 66 ++++++ 24 files changed, 1117 insertions(+), 544 deletions(-) create mode 100644 packages/charts/src/components/BarChart/__snapshots__/BarChart.test.tsx.snap create mode 100644 packages/charts/src/components/ColumnChart/__snapshots__/ColumnChart.test.tsx.snap create mode 100644 packages/charts/src/components/DonutChart/__snapshots__/DonutChart.test.tsx.snap create mode 100644 packages/charts/src/components/LineChart/__snapshots__/LineChart.test.tsx.snap create mode 100644 packages/charts/src/components/PieChart/__snapshots__/PieChart.test.tsx.snap create mode 100644 shared/tests/serializer/content-loader-serializer.js diff --git a/config/jestsetup.ts b/config/jestsetup.ts index af42e14d863..809d31efd5c 100644 --- a/config/jestsetup.ts +++ b/config/jestsetup.ts @@ -2,6 +2,7 @@ import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import { createSerializer } from 'enzyme-to-json'; import jssSerializer from '@shared/tests/serializer/jss-snapshot-serializer'; +import contentLoaderSerializer from '@shared/tests/serializer/content-loader-serializer.js'; process.env.NODE_ENV = 'test'; process.env.BABEL_ENV = 'test'; @@ -22,6 +23,7 @@ expect.addSnapshotSerializer( }) ); expect.addSnapshotSerializer(jssSerializer); +expect.addSnapshotSerializer(contentLoaderSerializer); export const setupMatchMedia = () => { // @ts-ignore diff --git a/packages/charts/src/components/BarChart/BarChart.test.tsx b/packages/charts/src/components/BarChart/BarChart.test.tsx index 47b6b93cd78..852ef87b639 100644 --- a/packages/charts/src/components/BarChart/BarChart.test.tsx +++ b/packages/charts/src/components/BarChart/BarChart.test.tsx @@ -5,15 +5,15 @@ import { BarChart } from './index'; describe('BarChart', () => { test('Renders with data', () => { - renderThemedComponent(); + mountThemedComponent(); }); test('custom colors', () => { - renderThemedComponent(); + mountThemedComponent(); }); test('valueAxisFormatter', () => { - renderThemedComponent( `${d}%`} />); + mountThemedComponent( `${d}%`} />); }); test('with Ref', () => { @@ -23,7 +23,7 @@ describe('BarChart', () => { }); test('stacked', () => { - renderThemedComponent( + mountThemedComponent( { /> ); }); + + test('loading placeholder', () => { + const wrapper = mountThemedComponent(); + expect(wrapper.render()).toMatchSnapshot(); + }); }); diff --git a/packages/charts/src/components/BarChart/Placeholder.tsx b/packages/charts/src/components/BarChart/Placeholder.tsx index 4cbfef29f8b..84f7ea321f9 100644 --- a/packages/charts/src/components/BarChart/Placeholder.tsx +++ b/packages/charts/src/components/BarChart/Placeholder.tsx @@ -1,7 +1,7 @@ import React from 'react'; import ContentLoader from 'react-content-loader'; -export const BarChartPlaceholder = (props) => { +export const BarChartPlaceholder = () => { return ( { primaryColor="#6a6d70" secondaryColor="#d9d9d9" primaryOpacity={0.3} - {...props} > diff --git a/packages/charts/src/components/BarChart/__snapshots__/BarChart.test.tsx.snap b/packages/charts/src/components/BarChart/__snapshots__/BarChart.test.tsx.snap new file mode 100644 index 00000000000..314419fdb56 --- /dev/null +++ b/packages/charts/src/components/BarChart/__snapshots__/BarChart.test.tsx.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BarChart loading placeholder 1`] = ` +
+ + + Loading interface... + + + + + + + + + + + + + + + + + + + + + + + + + +
+`; diff --git a/packages/charts/src/components/BarChart/demo.stories.tsx b/packages/charts/src/components/BarChart/demo.stories.tsx index 3ced6ccab6a..b8db4e96ad9 100644 --- a/packages/charts/src/components/BarChart/demo.stories.tsx +++ b/packages/charts/src/components/BarChart/demo.stories.tsx @@ -53,4 +53,5 @@ storiesOf('Charts | BarChart', module) options={options} loading={boolean('loading')} /> - )); + )) + .add('Loading Placeholder', () => ); diff --git a/packages/charts/src/components/BarChart/index.tsx b/packages/charts/src/components/BarChart/index.tsx index 6e2584a1cba..63786742fe5 100644 --- a/packages/charts/src/components/BarChart/index.tsx +++ b/packages/charts/src/components/BarChart/index.tsx @@ -13,125 +13,124 @@ import { BarChartPlaceholder } from './Placeholder'; export interface BarChartPropTypes extends ChartBaseProps {} -const BarChart = withChartContainer( - forwardRef((props: BarChartPropTypes, ref: Ref) => { - const { - labels, - datasets, - options, - categoryAxisFormatter, - valueAxisFormatter, - getDatasetAtEvent, - getElementAtEvent, - colors, - width, - height, - noLegend - } = props as BarChartPropTypes; +const BarChartComponent = forwardRef((props: BarChartPropTypes, ref: Ref) => { + const { + labels, + datasets, + options, + categoryAxisFormatter, + valueAxisFormatter, + getDatasetAtEvent, + getElementAtEvent, + colors, + width, + height, + noLegend + } = props as BarChartPropTypes; - const theme: any = useTheme(); - const data = useChartData(labels, datasets, colors, theme.theme); + const theme: any = useTheme(); + const data = useChartData(labels, datasets, colors, theme.theme); - const chartRef = useConsolidatedRef(ref); - const legendRef: RefObject = useRef(); + const chartRef = useConsolidatedRef(ref); + const legendRef: RefObject = useRef(); - const handleLegendItemPress = useCallback( - (e) => { - const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; - const datasetIndex = parseInt(clickTarget.dataset.datasetindex); - const { chartInstance } = chartRef.current; - const meta = chartInstance.getDatasetMeta(datasetIndex); - meta.hidden = meta.hidden === null ? !chartInstance.data.datasets[datasetIndex].hidden : null; - chartInstance.update(); - clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; - }, - [legendRef.current, chartRef.current] - ); + const handleLegendItemPress = useCallback( + (e) => { + const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; + const datasetIndex = parseInt(clickTarget.dataset.datasetindex); + const { chartInstance } = chartRef.current; + const meta = chartInstance.getDatasetMeta(datasetIndex); + meta.hidden = meta.hidden === null ? !chartInstance.data.datasets[datasetIndex].hidden : null; + chartInstance.update(); + clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; + }, + [legendRef.current, chartRef.current] + ); - useEffect(() => { - if (noLegend) { - legendRef.current.innerHTML = ''; - } else { - legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); - legendRef.current.querySelectorAll('li').forEach((legendItem) => { - legendItem.addEventListener('click', handleLegendItemPress); - }); - } - }, [chartRef.current, legendRef.current, noLegend]); + useEffect(() => { + if (noLegend) { + legendRef.current.innerHTML = ''; + } else { + legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); + legendRef.current.querySelectorAll('li').forEach((legendItem) => { + legendItem.addEventListener('click', handleLegendItemPress); + }); + } + }, [chartRef.current, legendRef.current, noLegend]); - const barChartDefaultConfig = useMemo(() => { - return { - scales: { - xAxes: [ - { - ...DEFAULT_OPTIONS.scales.yAxes[0], - ticks: { - callback: valueAxisFormatter - } - } - ], - yAxes: [ - { - ...DEFAULT_OPTIONS.scales.xAxes[0], - ticks: { - callback: categoryAxisFormatter - } + const barChartDefaultConfig = useMemo(() => { + return { + scales: { + xAxes: [ + { + ...DEFAULT_OPTIONS.scales.yAxes[0], + ticks: { + callback: valueAxisFormatter } - ] - }, - tooltips: { - callbacks: { - label: formatTooltipLabel(categoryAxisFormatter, valueAxisFormatter, 'xLabel') } - }, - plugins: { - datalabels: { - anchor: 'end', - align: 'start', - clip: true, - display: (context) => { - const datasetMeta = context.chart.getDatasetMeta(context.datasetIndex); - const dataMeta = datasetMeta.data[context.dataIndex]; - const width = dataMeta._view.x - dataMeta._view.base; - const formattedValue = valueAxisFormatter(context.dataset.data[context.dataIndex]); - const textWidth = getTextWidth(formattedValue) + 4; // offset - return width >= textWidth; - }, - formatter: valueAxisFormatter, - color: (context) => { - const datasetMeta = context.chart.getDatasetMeta(context.datasetIndex); - const dataMeta = datasetMeta.data[context.dataIndex]; - return bestContrast(dataMeta._view.backgroundColor, [ - /* sapUiBaseText */ '#32363a', - /* sapUiContentContrastTextColor */ '#ffffff' - ]); + ], + yAxes: [ + { + ...DEFAULT_OPTIONS.scales.xAxes[0], + ticks: { + callback: categoryAxisFormatter } } + ] + }, + tooltips: { + callbacks: { + label: formatTooltipLabel(categoryAxisFormatter, valueAxisFormatter, 'xLabel') } - }; - }, [valueAxisFormatter, categoryAxisFormatter]); - - const mergedOptions = useMergedConfig(barChartDefaultConfig, options); + }, + plugins: { + datalabels: { + anchor: 'end', + align: 'start', + clip: true, + display: (context) => { + const datasetMeta = context.chart.getDatasetMeta(context.datasetIndex); + const dataMeta = datasetMeta.data[context.dataIndex]; + const width = dataMeta._view.x - dataMeta._view.base; + const formattedValue = valueAxisFormatter(context.dataset.data[context.dataIndex]); + const textWidth = getTextWidth(formattedValue) + 4; // offset + return width >= textWidth; + }, + formatter: valueAxisFormatter, + color: (context) => { + const datasetMeta = context.chart.getDatasetMeta(context.datasetIndex); + const dataMeta = datasetMeta.data[context.dataIndex]; + return bestContrast(dataMeta._view.backgroundColor, [ + /* sapUiBaseText */ '#32363a', + /* sapUiContentContrastTextColor */ '#ffffff' + ]); + } + } + } + }; + }, [valueAxisFormatter, categoryAxisFormatter]); - return ( - <> - -
- - ); - }) -); + const mergedOptions = useMergedConfig(barChartDefaultConfig, options); + return ( + <> + +
+ + ); +}); // @ts-ignore -BarChart.LoadingPlaceholder = BarChartPlaceholder; +BarChartComponent.LoadingPlaceholder = BarChartPlaceholder; +const BarChart = withChartContainer(BarChartComponent); + BarChart.defaultProps = { ...ChartBaseDefaultProps }; diff --git a/packages/charts/src/components/ColumnChart/ColumnChart.test.tsx b/packages/charts/src/components/ColumnChart/ColumnChart.test.tsx index 021635c7f1e..d6ea738e32c 100644 --- a/packages/charts/src/components/ColumnChart/ColumnChart.test.tsx +++ b/packages/charts/src/components/ColumnChart/ColumnChart.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { renderThemedComponent } from '@shared/tests/utils'; +import { mountThemedComponent, renderThemedComponent } from '@shared/tests/utils'; import { ColumnChart } from './index'; import { labels, singleDataset } from '../../test/resources/ChartProps'; @@ -7,4 +7,9 @@ describe('ColumnChart', () => { test('Renders with data', () => { renderThemedComponent(); }); + + test('loading placeholder', () => { + const wrapper = mountThemedComponent(); + expect(wrapper.render()).toMatchSnapshot(); + }); }); diff --git a/packages/charts/src/components/ColumnChart/__snapshots__/ColumnChart.test.tsx.snap b/packages/charts/src/components/ColumnChart/__snapshots__/ColumnChart.test.tsx.snap new file mode 100644 index 00000000000..dcc4b8607b2 --- /dev/null +++ b/packages/charts/src/components/ColumnChart/__snapshots__/ColumnChart.test.tsx.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColumnChart loading placeholder 1`] = ` +
+ + + Loading interface... + + + + + + + + + + + + + + + + + + + + + + + + + +
+`; diff --git a/packages/charts/src/components/ColumnChart/demo.stories.tsx b/packages/charts/src/components/ColumnChart/demo.stories.tsx index ada972065d1..e6fb82ca051 100644 --- a/packages/charts/src/components/ColumnChart/demo.stories.tsx +++ b/packages/charts/src/components/ColumnChart/demo.stories.tsx @@ -99,4 +99,5 @@ storiesOf('Charts | ColumnChart', module) options={growthLineOptions} loading={boolean('loading')} /> - )); + )) + .add('Loading Placeholder', () => ); diff --git a/packages/charts/src/components/ColumnChart/index.tsx b/packages/charts/src/components/ColumnChart/index.tsx index 08ce89e130b..040eea787d8 100644 --- a/packages/charts/src/components/ColumnChart/index.tsx +++ b/packages/charts/src/components/ColumnChart/index.tsx @@ -13,128 +13,127 @@ import { ColumnChartPlaceholder } from './Placeholder'; export interface ColumnChartPropTypes extends ChartBaseProps {} -const ColumnChart = withChartContainer( - forwardRef((props: ColumnChartPropTypes, ref: Ref) => { - const { - labels, - datasets, - categoryAxisFormatter, - valueAxisFormatter, - getDatasetAtEvent, - getElementAtEvent, - colors, - options, - width, - height, - noLegend - } = props; +const ColumnChartComponent = forwardRef((props: ColumnChartPropTypes, ref: Ref) => { + const { + labels, + datasets, + categoryAxisFormatter, + valueAxisFormatter, + getDatasetAtEvent, + getElementAtEvent, + colors, + options, + width, + height, + noLegend + } = props; - const theme: any = useTheme(); - const data = useChartData(labels, datasets, colors, theme.theme); + const theme: any = useTheme(); + const data = useChartData(labels, datasets, colors, theme.theme); - const chartRef = useConsolidatedRef(ref); - const legendRef: RefObject = useRef(); + const chartRef = useConsolidatedRef(ref); + const legendRef: RefObject = useRef(); - const handleLegendItemPress = useCallback( - (e) => { - const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; - const datasetIndex = parseInt(clickTarget.dataset.datasetindex); - const { chartInstance } = chartRef.current; - const meta = chartInstance.getDatasetMeta(datasetIndex); - meta.hidden = meta.hidden === null ? !chartInstance.data.datasets[datasetIndex].hidden : null; - chartInstance.update(); - clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; - }, - [legendRef.current, chartRef.current] - ); + const handleLegendItemPress = useCallback( + (e) => { + const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; + const datasetIndex = parseInt(clickTarget.dataset.datasetindex); + const { chartInstance } = chartRef.current; + const meta = chartInstance.getDatasetMeta(datasetIndex); + meta.hidden = meta.hidden === null ? !chartInstance.data.datasets[datasetIndex].hidden : null; + chartInstance.update(); + clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; + }, + [legendRef.current, chartRef.current] + ); - useEffect(() => { - if (noLegend) { - legendRef.current.innerHTML = ''; - } else { - legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); - legendRef.current.querySelectorAll('li').forEach((legendItem) => { - legendItem.addEventListener('click', handleLegendItemPress); - }); - } - }, [chartRef.current, legendRef.current, noLegend]); + useEffect(() => { + if (noLegend) { + legendRef.current.innerHTML = ''; + } else { + legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); + legendRef.current.querySelectorAll('li').forEach((legendItem) => { + legendItem.addEventListener('click', handleLegendItemPress); + }); + } + }, [chartRef.current, legendRef.current, noLegend]); - const columnChartDefaultConfig = useMemo(() => { - return { - scales: { - xAxes: [ - { - ...DEFAULT_OPTIONS.scales.xAxes[0], - ticks: { - callback: categoryAxisFormatter - } + const columnChartDefaultConfig = useMemo(() => { + return { + scales: { + xAxes: [ + { + ...DEFAULT_OPTIONS.scales.xAxes[0], + ticks: { + callback: categoryAxisFormatter } - ], - yAxes: [ - { - ...DEFAULT_OPTIONS.scales.yAxes[0], - ticks: { - ...DEFAULT_OPTIONS.scales.yAxes[0].ticks, - callback: valueAxisFormatter - } + } + ], + yAxes: [ + { + ...DEFAULT_OPTIONS.scales.yAxes[0], + ticks: { + ...DEFAULT_OPTIONS.scales.yAxes[0].ticks, + callback: valueAxisFormatter } - ] - }, - tooltips: { - callbacks: { - label: formatTooltipLabel(categoryAxisFormatter, valueAxisFormatter) } - }, - plugins: { - datalabels: { - display: (context) => { - const datasetMeta = context.chart.getDatasetMeta(context.datasetIndex); - const dataMeta = datasetMeta.data[context.dataIndex]; - const height = dataMeta._view.base - dataMeta._view.y; // offset - if (height < getTextHeight() + 6) { - return false; - } - const formattedValue = valueAxisFormatter(context.dataset.data[context.dataIndex]); - const textWidth = getTextWidth(formattedValue); - return textWidth < dataMeta._view.width; - }, - anchor: 'end', - align: 'start', - formatter: valueAxisFormatter, - color: (context) => { - const datasetMeta = context.chart.getDatasetMeta(context.datasetIndex); - const dataMeta = datasetMeta.data[context.dataIndex]; - return bestContrast(dataMeta._view.backgroundColor, [ - /* sapUiBaseText */ '#32363a', - /* sapUiContentContrastTextColor */ '#ffffff' - ]); + ] + }, + tooltips: { + callbacks: { + label: formatTooltipLabel(categoryAxisFormatter, valueAxisFormatter) + } + }, + plugins: { + datalabels: { + display: (context) => { + const datasetMeta = context.chart.getDatasetMeta(context.datasetIndex); + const dataMeta = datasetMeta.data[context.dataIndex]; + const height = dataMeta._view.base - dataMeta._view.y; // offset + if (height < getTextHeight() + 6) { + return false; } + const formattedValue = valueAxisFormatter(context.dataset.data[context.dataIndex]); + const textWidth = getTextWidth(formattedValue); + return textWidth < dataMeta._view.width; + }, + anchor: 'end', + align: 'start', + formatter: valueAxisFormatter, + color: (context) => { + const datasetMeta = context.chart.getDatasetMeta(context.datasetIndex); + const dataMeta = datasetMeta.data[context.dataIndex]; + return bestContrast(dataMeta._view.backgroundColor, [ + /* sapUiBaseText */ '#32363a', + /* sapUiContentContrastTextColor */ '#ffffff' + ]); } } - }; - }, [categoryAxisFormatter, valueAxisFormatter]); - - const mergedOptions = useMergedConfig(columnChartDefaultConfig, options); + } + }; + }, [categoryAxisFormatter, valueAxisFormatter]); - return ( - <> - -
- - ); - }) -); + const mergedOptions = useMergedConfig(columnChartDefaultConfig, options); + return ( + <> + +
+ + ); +}); // @ts-ignore -ColumnChart.LoadingPlaceholder = ColumnChartPlaceholder; +ColumnChartComponent.LoadingPlaceholder = ColumnChartPlaceholder; +const ColumnChart = withChartContainer(ColumnChartComponent); + ColumnChart.defaultProps = { ...ChartBaseDefaultProps }; diff --git a/packages/charts/src/components/DonutChart/DonutChart.test.tsx b/packages/charts/src/components/DonutChart/DonutChart.test.tsx index ab6803ba0e4..cb0f72c6ecc 100644 --- a/packages/charts/src/components/DonutChart/DonutChart.test.tsx +++ b/packages/charts/src/components/DonutChart/DonutChart.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { renderThemedComponent } from '@shared/tests/utils'; +import { mountThemedComponent, renderThemedComponent } from '@shared/tests/utils'; import { DonutChart } from './index'; import { labels, singleDataset } from '../../test/resources/ChartProps'; @@ -7,4 +7,9 @@ describe('DonutChart', () => { test('Renders with data', () => { renderThemedComponent(); }); + + test('loading placeholder', () => { + const wrapper = mountThemedComponent(); + expect(wrapper.render()).toMatchSnapshot(); + }); }); diff --git a/packages/charts/src/components/DonutChart/__snapshots__/DonutChart.test.tsx.snap b/packages/charts/src/components/DonutChart/__snapshots__/DonutChart.test.tsx.snap new file mode 100644 index 00000000000..e572723caa2 --- /dev/null +++ b/packages/charts/src/components/DonutChart/__snapshots__/DonutChart.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DonutChart loading placeholder 1`] = ` +
+ + + Loading interface... + + + + + + + + + + + + + + + + + + + +
+`; diff --git a/packages/charts/src/components/DonutChart/demo.stories.tsx b/packages/charts/src/components/DonutChart/demo.stories.tsx index 38d9372461a..5ca413fde66 100644 --- a/packages/charts/src/components/DonutChart/demo.stories.tsx +++ b/packages/charts/src/components/DonutChart/demo.stories.tsx @@ -27,4 +27,5 @@ storiesOf('Charts | DonutChart', module) valueAxisFormatter={(number) => `${number}$`} loading={boolean('loading')} /> - )); + )) + .add('Loading Placeholder', () => ); diff --git a/packages/charts/src/components/DonutChart/index.tsx b/packages/charts/src/components/DonutChart/index.tsx index 1e798465cee..65e7c4a462c 100644 --- a/packages/charts/src/components/DonutChart/index.tsx +++ b/packages/charts/src/components/DonutChart/index.tsx @@ -11,94 +11,94 @@ import { PieChartPlaceholder } from '../PieChart/Placeholder'; export interface DonutChartPropTypes extends ChartBaseProps {} -const DonutChart = withChartContainer( - forwardRef((props: DonutChartPropTypes, ref: Ref) => { - const { - labels, - datasets, - colors, - categoryAxisFormatter, - getDatasetAtEvent, - getElementAtEvent, - valueAxisFormatter, - options, - width, - height, - noLegend - } = props; +const DonutChartComponent = forwardRef((props: DonutChartPropTypes, ref: Ref) => { + const { + labels, + datasets, + colors, + categoryAxisFormatter, + getDatasetAtEvent, + getElementAtEvent, + valueAxisFormatter, + options, + width, + height, + noLegend + } = props; - const theme: any = useTheme(); - const data = useChartData(labels, datasets, colors, theme.theme, true); + const theme: any = useTheme(); + const data = useChartData(labels, datasets, colors, theme.theme, true); - const donutChartDefaultConfig = useMemo(() => { - return { - cutoutPercentage: 70, - tooltips: { - callbacks: { - label: formatTooltipLabelForPieCharts(categoryAxisFormatter, valueAxisFormatter) - } - }, - plugins: { - datalabels: { - anchor: 'end', - align: 'end', - color: (context) => { - return /* sapUiBaseText */ '#32363a'; - }, - formatter: valueAxisFormatter - } + const donutChartDefaultConfig = useMemo(() => { + return { + cutoutPercentage: 70, + tooltips: { + callbacks: { + label: formatTooltipLabelForPieCharts(categoryAxisFormatter, valueAxisFormatter) } - }; - }, [categoryAxisFormatter, valueAxisFormatter]); - - const mergedOptions = useMergedConfig(donutChartDefaultConfig, options); + }, + plugins: { + datalabels: { + anchor: 'end', + align: 'end', + color: (context) => { + return /* sapUiBaseText */ '#32363a'; + }, + formatter: valueAxisFormatter + } + } + }; + }, [categoryAxisFormatter, valueAxisFormatter]); - const chartRef = useConsolidatedRef(ref); - const legendRef: RefObject = useRef(); + const mergedOptions = useMergedConfig(donutChartDefaultConfig, options); - const handleLegendItemPress = useCallback( - (e) => { - const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; - const datasetIndex = parseInt(clickTarget.dataset.datasetindex); - const { chartInstance } = chartRef.current; - const meta = chartInstance.getDatasetMeta(0).data[datasetIndex]; - meta.hidden = !meta.hidden; - chartInstance.update(); - clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; - }, - [legendRef.current, chartRef.current] - ); + const chartRef = useConsolidatedRef(ref); + const legendRef: RefObject = useRef(); - useEffect(() => { - if (noLegend) { - legendRef.current.innerHTML = ''; - } else { - legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); - legendRef.current.querySelectorAll('li').forEach((legendItem) => { - legendItem.addEventListener('click', handleLegendItemPress); - }); - } - }, [chartRef.current, legendRef.current, noLegend]); + const handleLegendItemPress = useCallback( + (e) => { + const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; + const datasetIndex = parseInt(clickTarget.dataset.datasetindex); + const { chartInstance } = chartRef.current; + const meta = chartInstance.getDatasetMeta(0).data[datasetIndex]; + meta.hidden = !meta.hidden; + chartInstance.update(); + clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; + }, + [legendRef.current, chartRef.current] + ); - return ( - <> - -
- - ); - }) -); + useEffect(() => { + if (noLegend) { + legendRef.current.innerHTML = ''; + } else { + legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); + legendRef.current.querySelectorAll('li').forEach((legendItem) => { + legendItem.addEventListener('click', handleLegendItemPress); + }); + } + }, [chartRef.current, legendRef.current, noLegend]); + return ( + <> + +
+ + ); +}); // @ts-ignore -DonutChart.LoadingPlaceholder = PieChartPlaceholder; +DonutChartComponent.LoadingPlaceholder = PieChartPlaceholder; + +const DonutChart = withChartContainer(DonutChartComponent); + DonutChart.defaultProps = { ...ChartBaseDefaultProps, colors: null diff --git a/packages/charts/src/components/LineChart/LineChart.test.tsx b/packages/charts/src/components/LineChart/LineChart.test.tsx index 5efd8ff94d7..a1edc0f01ca 100644 --- a/packages/charts/src/components/LineChart/LineChart.test.tsx +++ b/packages/charts/src/components/LineChart/LineChart.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { renderThemedComponent } from '@shared/tests/utils'; +import { mountThemedComponent, renderThemedComponent } from '@shared/tests/utils'; import { LineChart } from './index'; import { labels, singleDataset } from '../../test/resources/ChartProps'; @@ -7,4 +7,9 @@ describe('LineChart', () => { test('Renders with data', () => { renderThemedComponent(); }); + + test('loading placeholder', () => { + const wrapper = mountThemedComponent(); + expect(wrapper.render()).toMatchSnapshot(); + }); }); diff --git a/packages/charts/src/components/LineChart/__snapshots__/LineChart.test.tsx.snap b/packages/charts/src/components/LineChart/__snapshots__/LineChart.test.tsx.snap new file mode 100644 index 00000000000..cad5958905b --- /dev/null +++ b/packages/charts/src/components/LineChart/__snapshots__/LineChart.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LineChart loading placeholder 1`] = ` +
+ + + Loading interface... + + + + + + + + + + + + + + + + + + + +
+`; diff --git a/packages/charts/src/components/LineChart/demo.stories.tsx b/packages/charts/src/components/LineChart/demo.stories.tsx index 104cb2b4c88..1a26017c02d 100644 --- a/packages/charts/src/components/LineChart/demo.stories.tsx +++ b/packages/charts/src/components/LineChart/demo.stories.tsx @@ -27,4 +27,5 @@ const renderStoryWithCustomColors = () => ( storiesOf('Charts | Line Chart', module) .add('Default', renderStory) .add('with custom colors', renderStoryWithCustomColors) - .add('with Formatter', renderStoryWithFormatter); + .add('with Formatter', renderStoryWithFormatter) + .add('Loading Placeholder', () => ); diff --git a/packages/charts/src/components/LineChart/index.tsx b/packages/charts/src/components/LineChart/index.tsx index dfa7d04eaf0..30abcf2f781 100644 --- a/packages/charts/src/components/LineChart/index.tsx +++ b/packages/charts/src/components/LineChart/index.tsx @@ -12,99 +12,99 @@ import { LineChartPlaceholder } from './Placeholder'; export interface LineChartPropTypes extends ChartBaseProps {} -const LineChart = withChartContainer( - forwardRef((props: LineChartPropTypes, ref: Ref) => { - const { - labels, - datasets, - colors, - options, - valueAxisFormatter, - categoryAxisFormatter, - getElementAtEvent, - getDatasetAtEvent, - width, - height, - noLegend - } = props; +const LineChartComponent = forwardRef((props: LineChartPropTypes, ref: Ref) => { + const { + labels, + datasets, + colors, + options, + valueAxisFormatter, + categoryAxisFormatter, + getElementAtEvent, + getDatasetAtEvent, + width, + height, + noLegend + } = props; - const lineChartDefaultConfig = useMemo(() => { - return { - scales: { - yAxes: [ - { - ...DEFAULT_OPTIONS.scales.yAxes[0], - display: true, - ticks: { - ...DEFAULT_OPTIONS.scales.yAxes[0].ticks, - callback: valueAxisFormatter - } + const lineChartDefaultConfig = useMemo(() => { + return { + scales: { + yAxes: [ + { + ...DEFAULT_OPTIONS.scales.yAxes[0], + display: true, + ticks: { + ...DEFAULT_OPTIONS.scales.yAxes[0].ticks, + callback: valueAxisFormatter } - ], - xAxes: DEFAULT_OPTIONS.scales.xAxes - }, - tooltips: { - callbacks: { - label: formatTooltipLabel(categoryAxisFormatter, valueAxisFormatter) - } - }, - plugins: { - datalabels: { - formatter: valueAxisFormatter } + ], + xAxes: DEFAULT_OPTIONS.scales.xAxes + }, + tooltips: { + callbacks: { + label: formatTooltipLabel(categoryAxisFormatter, valueAxisFormatter) } - }; - }, [categoryAxisFormatter, valueAxisFormatter]); - const chartOptions = useMergedConfig(lineChartDefaultConfig, options); - - const theme: any = useTheme(); - const data = useChartData(labels, datasets, colors, theme.theme); - - const chartRef = useConsolidatedRef(ref); - const legendRef: RefObject = useRef(); - const handleLegendItemPress = useCallback( - (e) => { - const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; - const datasetIndex = parseInt(clickTarget.dataset.datasetindex); - const { chartInstance } = chartRef.current; - const meta = chartInstance.getDatasetMeta(datasetIndex); - meta.hidden = meta.hidden === null ? !chartInstance.data.datasets[datasetIndex].hidden : null; - chartInstance.update(); - clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; }, - [legendRef.current, chartRef.current] - ); - - useEffect(() => { - if (noLegend) { - legendRef.current.innerHTML = ''; - } else { - legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); - legendRef.current.querySelectorAll('li').forEach((legendItem) => { - legendItem.addEventListener('click', handleLegendItemPress); - }); + plugins: { + datalabels: { + formatter: valueAxisFormatter + } } - }, [chartRef.current, legendRef.current, noLegend]); + }; + }, [categoryAxisFormatter, valueAxisFormatter]); + const chartOptions = useMergedConfig(lineChartDefaultConfig, options); + + const theme: any = useTheme(); + const data = useChartData(labels, datasets, colors, theme.theme); + + const chartRef = useConsolidatedRef(ref); + const legendRef: RefObject = useRef(); + const handleLegendItemPress = useCallback( + (e) => { + const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; + const datasetIndex = parseInt(clickTarget.dataset.datasetindex); + const { chartInstance } = chartRef.current; + const meta = chartInstance.getDatasetMeta(datasetIndex); + meta.hidden = meta.hidden === null ? !chartInstance.data.datasets[datasetIndex].hidden : null; + chartInstance.update(); + clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; + }, + [legendRef.current, chartRef.current] + ); - return ( - <> - -
- - ); - }) -); + useEffect(() => { + if (noLegend) { + legendRef.current.innerHTML = ''; + } else { + legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); + legendRef.current.querySelectorAll('li').forEach((legendItem) => { + legendItem.addEventListener('click', handleLegendItemPress); + }); + } + }, [chartRef.current, legendRef.current, noLegend]); + return ( + <> + +
+ + ); +}); // @ts-ignore -LineChart.LoadingPlaceholder = LineChartPlaceholder; +LineChartComponent.LoadingPlaceholder = LineChartPlaceholder; + +const LineChart = withChartContainer(LineChartComponent); + LineChart.defaultProps = { ...ChartBaseDefaultProps }; diff --git a/packages/charts/src/components/PieChart/PieChart.test.tsx b/packages/charts/src/components/PieChart/PieChart.test.tsx index 8001c6f98b4..4c40fc42a8a 100644 --- a/packages/charts/src/components/PieChart/PieChart.test.tsx +++ b/packages/charts/src/components/PieChart/PieChart.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { renderThemedComponent } from '@shared/tests/utils'; +import { mountThemedComponent, renderThemedComponent } from '@shared/tests/utils'; import { PieChart } from './index'; import { labels, singleDataset } from '../../test/resources/ChartProps'; @@ -7,4 +7,9 @@ describe('PieChart', () => { test('Renders with data', () => { renderThemedComponent(); }); + + test('loading placeholder', () => { + const wrapper = mountThemedComponent(); + expect(wrapper.render()).toMatchSnapshot(); + }); }); diff --git a/packages/charts/src/components/PieChart/__snapshots__/PieChart.test.tsx.snap b/packages/charts/src/components/PieChart/__snapshots__/PieChart.test.tsx.snap new file mode 100644 index 00000000000..47f07d1dbfd --- /dev/null +++ b/packages/charts/src/components/PieChart/__snapshots__/PieChart.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PieChart loading placeholder 1`] = ` +
+ + + Loading interface... + + + + + + + + + + + + + + + + + + + +
+`; diff --git a/packages/charts/src/components/PieChart/demo.stories.tsx b/packages/charts/src/components/PieChart/demo.stories.tsx index a47eb96fa88..61061cf487a 100644 --- a/packages/charts/src/components/PieChart/demo.stories.tsx +++ b/packages/charts/src/components/PieChart/demo.stories.tsx @@ -10,4 +10,5 @@ storiesOf('Charts | PieChart', module) .add('Default', () => ) .add('with Formatter', () => ( `${d}%`} loading={boolean('loading')} /> - )); + )) + .add('Loading Placeholder', () => ); diff --git a/packages/charts/src/components/PieChart/index.tsx b/packages/charts/src/components/PieChart/index.tsx index d73791f8ff1..bc500769580 100644 --- a/packages/charts/src/components/PieChart/index.tsx +++ b/packages/charts/src/components/PieChart/index.tsx @@ -11,93 +11,93 @@ import { PieChartPlaceholder } from './Placeholder'; export interface PieChartPropTypes extends ChartBaseProps {} -const PieChart = withChartContainer( - forwardRef((props: PieChartPropTypes, ref: Ref) => { - const { - labels, - datasets, - colors, - categoryAxisFormatter, - getDatasetAtEvent, - getElementAtEvent, - valueAxisFormatter, - options, - width, - height, - noLegend - } = props; +const PieChartComponent = forwardRef((props: PieChartPropTypes, ref: Ref) => { + const { + labels, + datasets, + colors, + categoryAxisFormatter, + getDatasetAtEvent, + getElementAtEvent, + valueAxisFormatter, + options, + width, + height, + noLegend + } = props; - const theme: any = useTheme(); - const data = useChartData(labels, datasets, colors, theme.theme, true); + const theme: any = useTheme(); + const data = useChartData(labels, datasets, colors, theme.theme, true); - const pieChartDefaultConfig = useMemo(() => { - return { - tooltips: { - callbacks: { - label: formatTooltipLabelForPieCharts(categoryAxisFormatter, valueAxisFormatter) - } - }, - plugins: { - datalabels: { - anchor: 'end', - align: 'end', - color: (context) => { - return /* sapUiBaseText */ '#32363a'; - }, - formatter: valueAxisFormatter - } + const pieChartDefaultConfig = useMemo(() => { + return { + tooltips: { + callbacks: { + label: formatTooltipLabelForPieCharts(categoryAxisFormatter, valueAxisFormatter) } - }; - }, [categoryAxisFormatter, valueAxisFormatter]); - - const mergedOptions = useMergedConfig(pieChartDefaultConfig, options); + }, + plugins: { + datalabels: { + anchor: 'end', + align: 'end', + color: (context) => { + return /* sapUiBaseText */ '#32363a'; + }, + formatter: valueAxisFormatter + } + } + }; + }, [categoryAxisFormatter, valueAxisFormatter]); - const chartRef = useConsolidatedRef(ref); - const legendRef: RefObject = useRef(); + const mergedOptions = useMergedConfig(pieChartDefaultConfig, options); - const handleLegendItemPress = useCallback( - (e) => { - const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; - const datasetIndex = parseInt(clickTarget.dataset.datasetindex); - const { chartInstance } = chartRef.current; - const meta = chartInstance.getDatasetMeta(0).data[datasetIndex]; - meta.hidden = !meta.hidden; - chartInstance.update(); - clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; - }, - [legendRef.current, chartRef.current] - ); + const chartRef = useConsolidatedRef(ref); + const legendRef: RefObject = useRef(); - useEffect(() => { - if (noLegend) { - legendRef.current.innerHTML = ''; - } else { - legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); - legendRef.current.querySelectorAll('li').forEach((legendItem) => { - legendItem.addEventListener('click', handleLegendItemPress); - }); - } - }, [chartRef.current, legendRef.current, noLegend]); + const handleLegendItemPress = useCallback( + (e) => { + const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; + const datasetIndex = parseInt(clickTarget.dataset.datasetindex); + const { chartInstance } = chartRef.current; + const meta = chartInstance.getDatasetMeta(0).data[datasetIndex]; + meta.hidden = !meta.hidden; + chartInstance.update(); + clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; + }, + [legendRef.current, chartRef.current] + ); - return ( - <> - -
- - ); - }) -); + useEffect(() => { + if (noLegend) { + legendRef.current.innerHTML = ''; + } else { + legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); + legendRef.current.querySelectorAll('li').forEach((legendItem) => { + legendItem.addEventListener('click', handleLegendItemPress); + }); + } + }, [chartRef.current, legendRef.current, noLegend]); + return ( + <> + +
+ + ); +}); // @ts-ignore -PieChart.LoadingPlaceholder = PieChartPlaceholder; +PieChartComponent.LoadingPlaceholder = PieChartPlaceholder; + +const PieChart = withChartContainer(PieChartComponent); + PieChart.defaultProps = { ...ChartBaseDefaultProps }; diff --git a/packages/charts/src/components/RadarChart/index.tsx b/packages/charts/src/components/RadarChart/index.tsx index 63ebcf1ff6b..f8774fc86c1 100644 --- a/packages/charts/src/components/RadarChart/index.tsx +++ b/packages/charts/src/components/RadarChart/index.tsx @@ -10,86 +10,85 @@ import { formatTooltipLabel, useMergedConfig } from '../../util/utils'; export interface RadarChartPropTypes extends ChartBaseProps {} -const RadarChart: FC = withChartContainer( - forwardRef((props: RadarChartPropTypes, ref: Ref) => { - const { - labels, - datasets, - width, - height, - options, - categoryAxisFormatter, - getDatasetAtEvent, - getElementAtEvent, - valueAxisFormatter, - colors, - noLegend - } = props; +const RadarChartComponent = forwardRef((props: RadarChartPropTypes, ref: Ref) => { + const { + labels, + datasets, + width, + height, + options, + categoryAxisFormatter, + getDatasetAtEvent, + getElementAtEvent, + valueAxisFormatter, + colors, + noLegend + } = props; - const theme: any = useTheme(); - const data = useChartData(labels, datasets, colors, theme.theme); + const theme: any = useTheme(); + const data = useChartData(labels, datasets, colors, theme.theme); - const radarChartDefaultConfig = useMemo(() => { - return { - scale: { - ticks: { - callback: valueAxisFormatter - } - }, - tooltips: { - callbacks: { - label: formatTooltipLabel(categoryAxisFormatter, valueAxisFormatter) - } - }, - plugins: { - datalabels: false + const radarChartDefaultConfig = useMemo(() => { + return { + scale: { + ticks: { + callback: valueAxisFormatter } - }; - }, [categoryAxisFormatter, valueAxisFormatter]); - const mergedOptions = useMergedConfig(radarChartDefaultConfig, options); - - const chartRef = useConsolidatedRef(ref); - const legendRef: RefObject = useRef(); - const handleLegendItemPress = useCallback( - (e) => { - const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; - const datasetIndex = parseInt(clickTarget.dataset.datasetindex); - const { chartInstance } = chartRef.current; - const meta = chartInstance.getDatasetMeta(datasetIndex); - meta.hidden = meta.hidden === null ? !chartInstance.data.datasets[datasetIndex].hidden : null; - chartInstance.update(); - clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; }, - [legendRef.current, chartRef.current] - ); - - useEffect(() => { - if (noLegend) { - legendRef.current.innerHTML = ''; - } else { - legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); - legendRef.current.querySelectorAll('li').forEach((legendItem) => { - legendItem.addEventListener('click', handleLegendItemPress); - }); + tooltips: { + callbacks: { + label: formatTooltipLabel(categoryAxisFormatter, valueAxisFormatter) + } + }, + plugins: { + datalabels: false } - }, [chartRef.current, legendRef.current, noLegend]); + }; + }, [categoryAxisFormatter, valueAxisFormatter]); + const mergedOptions = useMergedConfig(radarChartDefaultConfig, options); + + const chartRef = useConsolidatedRef(ref); + const legendRef: RefObject = useRef(); + const handleLegendItemPress = useCallback( + (e) => { + const clickTarget = (e.currentTarget as unknown) as HTMLLIElement; + const datasetIndex = parseInt(clickTarget.dataset.datasetindex); + const { chartInstance } = chartRef.current; + const meta = chartInstance.getDatasetMeta(datasetIndex); + meta.hidden = meta.hidden === null ? !chartInstance.data.datasets[datasetIndex].hidden : null; + chartInstance.update(); + clickTarget.style.textDecoration = meta.hidden ? 'line-through' : 'unset'; + }, + [legendRef.current, chartRef.current] + ); + + useEffect(() => { + if (noLegend) { + legendRef.current.innerHTML = ''; + } else { + legendRef.current.innerHTML = chartRef.current.chartInstance.generateLegend(); + legendRef.current.querySelectorAll('li').forEach((legendItem) => { + legendItem.addEventListener('click', handleLegendItemPress); + }); + } + }, [chartRef.current, legendRef.current, noLegend]); - return ( - <> - {' '} -
- - ); - }) -); + return ( + <> + {' '} +
+ + ); +}); +const RadarChart = withChartContainer(RadarChartComponent); RadarChart.defaultProps = { ...ChartBaseDefaultProps diff --git a/shared/tests/serializer/content-loader-serializer.js b/shared/tests/serializer/content-loader-serializer.js new file mode 100644 index 00000000000..f75f9b3b606 --- /dev/null +++ b/shared/tests/serializer/content-loader-serializer.js @@ -0,0 +1,66 @@ +// removing dynamic class names as well as code coverage instrumentation added by +// istanbul-instrumenter-loader + +const MARKER = '__content-loader-serializer-marker__'; +const styleUrl = /fill: url\(#([\d\w)]+)\)/i; + +const collectElements = (element, elements = []) => { + if (typeof element !== 'object') { + return elements; + } + + elements.push(element); + + if (element.children) { + element.children.forEach((child) => collectElements(child, elements)); + } + + return elements; +}; + +const markElements = (elements) => + elements.forEach((element) => { + element[MARKER] = true; + }); + +const replaceIds = (elements) => { + elements.forEach((element) => { + if (element.node.name === 'clipPath' || element.props['clip-path'] || element.node.name === 'linearGradient') { + if (element.props['clip-path']) { + element.props['clip-path'] = 'CLIP-PATH-URL'; + } + + if (element.props.style) { + element.props.style = element.props.style.replace(styleUrl, (a, b) => { + return a.replace(b, 'STYLE-URL'); + }); + } + + if (element.node.name === 'clipPath') { + element.props.id = 'CLIP-PATH-URL'; + } + + if (element.node.name === 'linearGradient') { + element.props.id = 'STYLE-URL'; + } + } + }); +}; + +module.exports = { + test(value) { + return value && !value[MARKER] && value.$$typeof === Symbol.for('react.test.json'); + }, + + print(value, serialize) { + // collect all react element nodes in the tree of the value + const elements = collectElements(value); + + // mark the collected element nodes to avoid processing them several times + markElements(elements); + + replaceIds(elements); + + return serialize(value); + } +};