Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bar & Histogram & Formula & ComparativeFormula Widgets: Add a skeleton for loading state #674

Merged
merged 17 commits into from
May 24, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Not released

- Bar & Histogram & Formula & ComparativeFormula Widgets: Add a skeleton for loading state [#674](https://github.com/CartoDB/carto-react/pull/674)

## 2.0

## 2.0.4 (2023-05-19)
Expand Down
2 changes: 1 addition & 1 deletion packages/react-ui/__tests__/widgets/BarWidgetUI.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render, fireEvent, screen } from '../widgets/utils/testUtils';
import BarWidgetUI from '../../src/widgets/BarWidgetUI';
import BarWidgetUI from '../../src/widgets/BarWidgetUI/BarWidgetUI';
import { mockEcharts } from './testUtils';

describe('BarWidgetUI', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render, screen } from '../widgets/utils/testUtils';
import FormulaWidgetUI from '../../src/widgets/FormulaWidgetUI';
import FormulaWidgetUI from '../../src/widgets/FormulaWidgetUI/FormulaWidgetUI';
import { currencyFormatter } from './testUtils';

describe('FormulaWidgetUI', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
waitForElementToBeRemoved
} from '../widgets/utils/testUtils';
import WrapperWidgetUI from '../../src/widgets/WrapperWidgetUI';
import FormulaWidgetUI from '../../src/widgets/FormulaWidgetUI';
import FormulaWidgetUI from '../../src/widgets/FormulaWidgetUI/FormulaWidgetUI';

describe('WrapperWidgetUI', () => {
const TITLE = 'test';
Expand Down
7 changes: 3 additions & 4 deletions packages/react-ui/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { theme, cartoThemeOptions, CartoTheme } from './theme/carto-theme';
import WrapperWidgetUI from './widgets/WrapperWidgetUI';
import CategoryWidgetUI from './widgets/CategoryWidgetUI';
import FormulaWidgetUI from './widgets/FormulaWidgetUI';
import BarWidgetUI from './widgets/BarWidgetUI';
import FormulaWidgetUI from './widgets/FormulaWidgetUI/FormulaWidgetUI';
import BarWidgetUI from './widgets/BarWidgetUI/BarWidgetUI';
import HistogramWidgetUI from './widgets/HistogramWidgetUI/HistogramWidgetUI';
import PieWidgetUI from './widgets/PieWidgetUI';
import LegendWidgetUI, { LEGEND_TYPES } from './widgets/legend/LegendWidgetUI';
Expand Down Expand Up @@ -35,7 +35,7 @@ import SelectField, { SelectFieldProps } from './components/atoms/SelectField';
import UploadField, {
UploadFieldProps
} from './components/molecules/UploadField/UploadField';
import AppBar, {AppBarProps} from './components/organisms/AppBar/AppBar';
import AppBar, { AppBarProps } from './components/organisms/AppBar/AppBar';
import LabelWithIndicator, {
LabelWithIndicatorProps
} from './components/atoms/LabelWithIndicator';
Expand Down Expand Up @@ -92,7 +92,6 @@ export {
UploadFieldProps,
AppBar,
AppBarProps,

LabelWithIndicator,
LabelWithIndicatorProps,
getCartoColorStylePropsForItem,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-ui/src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { theme, cartoThemeOptions } from './theme/carto-theme';
import WrapperWidgetUI from './widgets/WrapperWidgetUI';
import CategoryWidgetUI from './widgets/CategoryWidgetUI';
import FormulaWidgetUI from './widgets/FormulaWidgetUI';
import BarWidgetUI from './widgets/BarWidgetUI';
import FormulaWidgetUI from './widgets/FormulaWidgetUI/FormulaWidgetUI';
import BarWidgetUI from './widgets/BarWidgetUI/BarWidgetUI';
import HistogramWidgetUI from './widgets/HistogramWidgetUI/HistogramWidgetUI';
import PieWidgetUI from './widgets/PieWidgetUI';
import LegendWidgetUI, { LEGEND_TYPES } from './widgets/legend/LegendWidgetUI';
Expand Down
28 changes: 28 additions & 0 deletions packages/react-ui/src/theme/sections/components/feedback.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getSpacing } from '../../themeUtils';
import { commonPalette } from '../palette';

export const feedbackOverrides = {
// SnackBar
MuiSnackbar: {
Expand All @@ -7,5 +10,30 @@ export const feedbackOverrides = {
horizontal: 'center'
}
}
},

// Skeleton
MuiSkeleton: {
defaultProps: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, good to know we have some basics in common

animation: 'wave',
variant: 'rounded'
},

styleOverrides: {
root: {
backgroundColor: commonPalette.black[8]
},
rounded: {
borderRadius: getSpacing(0.5)
},
text: {
borderRadius: getSpacing(0.5)
},
wave: {
'&::after': {
background: `linear-gradient( 90deg, transparent, ${commonPalette.black[4]}, transparent )`
}
}
}
}
};
4 changes: 4 additions & 0 deletions packages/react-ui/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type FormulaWidgetUIData =
export type FormulaWidgetUI = {
data: FormulaWidgetUIData;
formatter?: Function;
isLoading?: boolean;
};

export type HistogramWidgetUIData = number[];
Expand All @@ -51,6 +52,7 @@ export type HistogramWidgetUI = {
name?: string;
onSelectedBarsChange?: Function;
height?: number;
isLoading?: boolean;
};

export type BarWidgetUI = {
Expand All @@ -69,6 +71,7 @@ export type BarWidgetUI = {
height?: string | number;
filterable?: boolean;
animation?: boolean;
isLoading?: boolean;
};

export type PieWidgetUIData = { name: string; value: number }[];
Expand Down Expand Up @@ -223,6 +226,7 @@ export type ComparativeFormulaWidgetUI = {
animated?: boolean;
animationOptions?: AnimationOptions;
formatter?: (n: number) => React.ReactNode;
isLoading?: boolean;
};

export enum ORDER_TYPES {
Expand Down
22 changes: 22 additions & 0 deletions packages/react-ui/src/widgets/BarWidgetUI/BarSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Box, Skeleton } from '@mui/material';
import { SkeletonBarsGrid, SkeletonBarItem, SKELETON_HEIGHT } from '../SkeletonWidgets';

const BarSkeleton = ({ height }) => {
return (
<>
<Box mb={2}>
<Skeleton width={48} height={8} />
</Box>

<SkeletonBarsGrid style={{ height: height || SKELETON_HEIGHT }}>
<SkeletonBarItem variant='rectangular' height='20%' />
<SkeletonBarItem variant='rectangular' height='40%' />
<SkeletonBarItem variant='rectangular' height='60%' />
<SkeletonBarItem variant='rectangular' height='20%' />
<SkeletonBarItem variant='rectangular' height='80%' />
</SkeletonBarsGrid>
</>
);
};

export default BarSkeleton;
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import ReactEcharts from '../custom-components/echarts-for-react';
import ReactEcharts from '../../custom-components/echarts-for-react';
import { Grid, Link, useTheme, darken, styled } from '@mui/material';
import detectTouchScreen from './utils/detectTouchScreen';
import { processFormatterRes } from './utils/formatterUtils';
import Typography from '../components/atoms/Typography';
import detectTouchScreen from '../utils/detectTouchScreen';
import { processFormatterRes } from '../utils/formatterUtils';
import Typography from '../../components/atoms/Typography';
import BarSkeleton from './BarSkeleton';

const IS_TOUCH_SCREEN = detectTouchScreen();

Expand Down Expand Up @@ -42,7 +43,8 @@ function BarWidgetUI(props) {
stacked,
height,
filterable,
animation
animation,
isLoading
} = useProcessedProps(props);

const isMultiSeries = series.length > 1;
Expand Down Expand Up @@ -287,6 +289,8 @@ function BarWidgetUI(props) {
[filterable, clickEvent]
);

if (isLoading) return <BarSkeleton height={height} />;

return (
<div>
{onSelectedBarsChange && (
Expand Down Expand Up @@ -352,7 +356,8 @@ BarWidgetUI.propTypes = {
onSelectedBarsChange: PropTypes.func,
height: numberOrString,
filterable: PropTypes.bool,
animation: PropTypes.bool
animation: PropTypes.bool,
isLoading: PropTypes.bool
};

export default BarWidgetUI;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Skeleton } from '@mui/material';

const FormulaSkeleton = () => {
return <Skeleton height={24} width={120} />;
};

export default FormulaSkeleton;
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material';
import { animateValue } from './utils/animations';
import Typography from '../components/atoms/Typography';
import { animateValue } from '../utils/animations';
import Typography from '../../components/atoms/Typography';
import FormulaSkeleton from './FormulaSkeleton';

const Prefix = styled('span')(() => ({
marginRight: '2px'
Expand All @@ -21,7 +22,7 @@ function usePrevious(value) {
}

function FormulaWidgetUI(props) {
const { data, formatter, animation } = props;
const { data, formatter, animation, isLoading } = props;
const [value, setValue] = useState('-');
const requestRef = useRef();
const prevValue = usePrevious(value);
Expand Down Expand Up @@ -63,6 +64,8 @@ function FormulaWidgetUI(props) {

const isComplexFormat = typeof formattedValue === 'object' && formattedValue !== null;

if (isLoading) return <FormulaSkeleton />;

return isComplexFormat ? (
<Typography variant='h5' component='div' weight='medium'>
<Prefix>{formattedValue.prefix}</Prefix>
Expand Down Expand Up @@ -95,7 +98,8 @@ FormulaWidgetUI.propTypes = {
})
]),
formatter: PropTypes.func,
animation: PropTypes.bool
animation: PropTypes.bool,
isLoading: PropTypes.bool
};

export default FormulaWidgetUI;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Box, Skeleton } from '@mui/material';
import {
SKELETON_HEIGHT,
SkeletonGraphGrid,
SkeletonThinBarItem
} from '../SkeletonWidgets';

const HistogramSkeleton = ({ height }) => {
return (
<>
<Box mb={2}>
<Skeleton width={48} height={8} />
</Box>

<SkeletonGraphGrid style={{ height: height || SKELETON_HEIGHT }}>
<SkeletonThinBarItem variant='rectangular' height='20%' />
<SkeletonThinBarItem variant='rectangular' height='40%' />
<SkeletonThinBarItem variant='rectangular' height='60%' />
<SkeletonThinBarItem variant='rectangular' height='20%' />
<SkeletonThinBarItem variant='rectangular' height='80%' />
<SkeletonThinBarItem variant='rectangular' height='50%' />
<SkeletonThinBarItem variant='rectangular' height='20%' />
<SkeletonThinBarItem variant='rectangular' height='40%' />
<SkeletonThinBarItem variant='rectangular' height='60%' />
<SkeletonThinBarItem variant='rectangular' height='20%' />
<SkeletonThinBarItem variant='rectangular' height='80%' />
<SkeletonThinBarItem variant='rectangular' height='50%' />
</SkeletonGraphGrid>
</>
);
};

export default HistogramSkeleton;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { processFormatterRes } from '../utils/formatterUtils';
import detectTouchscreen from '../utils/detectTouchScreen';
import useHistogramInteractivity from './useHistogramInteractivity';
import Typography from '../../components/atoms/Typography';
import HistogramSkeleton from './HistogramSkeleton';

const IS_TOUCH_SCREEN = detectTouchscreen();

Expand Down Expand Up @@ -39,7 +40,8 @@ function HistogramWidgetUI({
tooltipFormatter,
animation,
filterable: _filterable,
height
height,
isLoading
}) {
const theme = useTheme();

Expand Down Expand Up @@ -262,6 +264,8 @@ function HistogramWidgetUI({
0
);

if (isLoading) return <HistogramSkeleton height={height} />;

return (
<div>
{filterable && (
Expand Down Expand Up @@ -311,7 +315,8 @@ HistogramWidgetUI.propTypes = {
onSelectedBarsChange: PropTypes.func,
animation: PropTypes.bool,
filterable: PropTypes.bool,
height: PropTypes.number
height: PropTypes.number,
isLoading: PropTypes.bool
};

export default HistogramWidgetUI;
Expand Down
55 changes: 55 additions & 0 deletions packages/react-ui/src/widgets/SkeletonWidgets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Skeleton, styled } from '@mui/material';

export const SKELETON_HEIGHT = 240;

export const SkeletonBarsGrid = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
width: '100%',
position: 'relative',
padding: theme.spacing(0, 2),

// Linear grid effect
'&::before': {
content: '""',
position: 'absolute',
top: 0,
right: 0,
left: 0,
bottom: 0,
backgroundSize: theme.spacing(4, 4),
backgroundImage: `linear-gradient(to bottom, ${theme.palette.grey[50]} 1px, transparent 1px)`,
transform: 'scaleY(-1)'
}
}));

export const SkeletonGraphGrid = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
width: '100%',

// Square grid effect
backgroundImage: `linear-gradient(${theme.palette.grey[50]} 0.5px, transparent 0.5px, transparent calc(100% - 0.5px), ${theme.palette.grey[50]} calc(100% - 0.5px)), linear-gradient(90deg, ${theme.palette.grey[50]} 0.5px, transparent 0.5px, transparent calc(100% - 0.5px), ${theme.palette.grey[50]} calc(100% - 0.5px))`,
backgroundSize: '8.33% 20%',
border: `0.5px solid ${theme.palette.grey[50]}`
}));

export const SkeletonBarItem = styled(Skeleton)(({ theme }) => ({
flex: 1,
maxWidth: theme.spacing(12),

'& + &': {
marginLeft: theme.spacing(1)
}
}));

export const SkeletonThinBarItem = styled(Skeleton)(({ theme }) => ({
flex: 1,
maxWidth: theme.spacing(8),

'& + &': {
marginLeft: '1px'
}
}));
Loading