diff --git a/assets/images/componentsThumbs/LoadingShape.svg b/assets/images/componentsThumbs/LoadingShape.svg index e847281ab..bc549df86 100644 --- a/assets/images/componentsThumbs/LoadingShape.svg +++ b/assets/images/componentsThumbs/LoadingShape.svg @@ -2,42 +2,32 @@ LoadingShape - - - - - - - - + + + - + - - - - - - + + + - - + + - - - - + - + + \ No newline at end of file diff --git a/src/components/LoadingShape/__test__/loadingShape.spec.js b/src/components/LoadingShape/__test__/loadingShape.spec.js new file mode 100644 index 000000000..a64f0b004 --- /dev/null +++ b/src/components/LoadingShape/__test__/loadingShape.spec.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import LoadingShape from '../'; +import { StyledImageIcon, StyledAvatarIcon } from '../styled'; + +describe(' { + it('should have "rounded-rect" as default shape', () => { + const component = mount(); + expect(component.prop('shape')).toBe('rounded-rect'); + }); + + it('should render the image icon when variant is "image"', () => { + const component = mount(); + expect(component.find(StyledImageIcon).length).toBe(1); + }); + + it('should render the user icon when variant is "avatar"', () => { + const component = mount(); + expect(component.find(StyledAvatarIcon).length).toBe(1); + }); +}); diff --git a/src/components/LoadingShape/icons/avatar.js b/src/components/LoadingShape/icons/avatar.js new file mode 100644 index 000000000..59ef4142c --- /dev/null +++ b/src/components/LoadingShape/icons/avatar.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const AvatarIcon = props => { + const { className, style } = props; + return ( + + + + + + + + ); +}; + +AvatarIcon.propTypes = { + className: PropTypes.string, + style: PropTypes.object, +}; + +AvatarIcon.defaultProps = { + className: undefined, + style: undefined, +}; + +export default AvatarIcon; diff --git a/src/components/LoadingShape/icons/image.js b/src/components/LoadingShape/icons/image.js new file mode 100644 index 000000000..381a560eb --- /dev/null +++ b/src/components/LoadingShape/icons/image.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ImageIcon = props => { + const { className, style } = props; + return ( + + + + + + + + + + ); +}; + +ImageIcon.propTypes = { + className: PropTypes.string, + style: PropTypes.object, +}; + +ImageIcon.defaultProps = { + className: undefined, + style: undefined, +}; + +export default ImageIcon; diff --git a/src/components/LoadingShape/index.d.ts b/src/components/LoadingShape/index.d.ts new file mode 100644 index 000000000..3e63586b1 --- /dev/null +++ b/src/components/LoadingShape/index.d.ts @@ -0,0 +1,8 @@ +import { BaseProps } from '../types'; + +export interface LoadingShapeProps extends BaseProps { + variant?: 'solid' | 'image' | 'avatar'; + shape?: 'circle' | 'rect' | 'rounded-rect' | 'square'; +} + +export default function(props: LoadingShapeProps): JSX.Element | null; diff --git a/src/components/LoadingShape/index.js b/src/components/LoadingShape/index.js new file mode 100644 index 000000000..0242921bc --- /dev/null +++ b/src/components/LoadingShape/index.js @@ -0,0 +1,62 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import RenderIf from '../RenderIf'; +import { + StyledShapeContainer, + StyledLoadingShape, + StyledImageIcon, + StyledAvatarIcon, +} from './styled'; + +/** + * LoadingShape can be used to display a placeholder where content + * is being loaded asynchronously. + */ +const LoadingShape = props => { + const { className, style, shape, variant } = props; + const shapeRef = useRef(); + const isImage = variant === 'image'; + const isAvatar = variant === 'avatar'; + + useEffect(() => { + const el = shapeRef.current; + if (shape === 'square' || shape === 'circle') { + el.style.width = `${el.offsetHeight}px`; + } else { + el.style.width = '100%'; + } + }); + + return ( + + + + + + + + + + + ); +}; + +LoadingShape.propTypes = { + /** A CSS class for the outer element, in addition to the component's base classes. */ + className: PropTypes.string, + /** An object with custom style applied to the outer element. */ + style: PropTypes.object, + /** The shape of the loading placeholder */ + shape: PropTypes.oneOf(['circle', 'rect', 'rounded-rect', 'square']), + /** The variant of the loading placeholder */ + variant: PropTypes.oneOf(['solid', 'image', 'avatar']), +}; + +LoadingShape.defaultProps = { + className: undefined, + style: undefined, + shape: 'rounded-rect', + variant: 'solid', +}; + +export default LoadingShape; diff --git a/src/components/LoadingShape/readme.md b/src/components/LoadingShape/readme.md new file mode 100644 index 000000000..06f2bb222 --- /dev/null +++ b/src/components/LoadingShape/readme.md @@ -0,0 +1,148 @@ +# LoadingShape on card +##### LoadingShapes can be used on cards, tables, lists, menus to display a placeholder where content is being loaded. + +```js +import React from 'react'; +import { Card, LoadingShape } from 'react-rainbow-components'; + +const style = { + maxWidth: '400px', + minWidth: '300px', + display: 'flex', + padding: '20px', +}; + +const imageStyle = { + width: '100px', +}; + +
+ +
+ +
+
+
+ + +
+
+ + +
+ + +
+
+
+
+
+ +``` + +# LoadingShape dynamic example +##### This is a dynamic example where you can play around with the component customization. + +```js +import React, { useState } from 'react'; +import { LoadingShape, RadioGroup, Input } from 'react-rainbow-components'; + +const styles = { + maxWidth: 400, + margin: 'auto', +}; + +const shapeContainerStyles = { + height: '150px', + display: 'flex', + justifyContent: 'center', +} + +const radioContainerStyles = { + display: 'flex', + justifyContent: 'space-evenly', +}; + +const inputStyle = { + width: '150px', +} + +const shapeOptions = [ + { value: 'rounded-rect', label: 'Rounded Rect' }, + { value: 'square', label: 'Square' }, + { value: 'circle', label: 'Circle' }, +]; + +const variantOptions = [ + { value: 'solid', label: 'Solid' }, + { value: 'image', label: 'Image' }, + { value: 'avatar', label: 'Avatar' }, +]; + +const LoadingShapeExample = () => { + const [shape, setShape] = useState('rounded-rect'); + const [variant, setVariant] = useState('solid'); + const [width, setWidth] = useState(300); + const [height, setHeight] = useState(15) + + const shapeStyle = { + height: `${height}px`, + width: `${width}px`, + }; + + const handleShapeChange = (event) => { + setShape(event.target.value); + if (event.target.value === 'circle' || event.target.value === 'square') { + setWidth(75); + setHeight(75); + } else { + setWidth(200); + setHeight(15); + } + }; + + return ( +
+
+ setWidth(event.target.value)} + /> + setHeight(event.target.value)} + /> +
+
+ + setVariant(event.target.value)} + label="Select variant" + /> +
+
+
+ +
+
+
+ ); +} + + +``` diff --git a/src/components/LoadingShape/styled/index.js b/src/components/LoadingShape/styled/index.js new file mode 100644 index 000000000..1d66c5acf --- /dev/null +++ b/src/components/LoadingShape/styled/index.js @@ -0,0 +1,122 @@ +import styled from 'styled-components'; +import attachThemeAttrs from '../../../styles/helpers/attachThemeAttrs'; +import { replaceAlpha } from '../../../styles/helpers/color'; +import darken from '../../../styles/helpers/color/darken'; +import AvatarIcon from '../icons/avatar'; +import ImageIcon from '../icons/image'; + +const StyledShapeContainer = styled.div` + position: relative; + width: 100%; + height: 100%; +`; + +const StyledImageIcon = attachThemeAttrs(styled(ImageIcon))` + color: ${props => props.palette.background.main}; + + ${props => + props.shape === 'square' && + ` + width: 85%; + `} + + ${props => + props.shape === 'circle' && + ` + width: 60%; + `} + + ${props => + (props.shape === 'rect' || props.shape === 'rounded-rect') && + ` + height: 80%; + `} +`; + +const StyledAvatarIcon = attachThemeAttrs(styled(AvatarIcon))` + color: ${props => props.palette.background.main}; + ${props => + props.shape === 'square' && + ` + width: 85%; + `} + + ${props => + props.shape === 'circle' && + ` + width: 60%; + `} + + ${props => + (props.shape === 'rect' || props.shape === 'rounded-rect') && + ` + height: 80%; + `} +`; + +const StyledLoadingShape = attachThemeAttrs(styled.div)` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + background: ${props => props.palette.background.highlight} + linear-gradient( + 90deg, + ${props => replaceAlpha(props.palette.background.highlight, 0.1)} 0%, + ${props => darken(props.palette.background.highlight, 0.1)} 50%, + ${props => replaceAlpha(props.palette.background.highlight, 0.1)} 100% + ); + background-size: 400% 400%; + animation: gradient 2.5s ease-in-out infinite; + + @keyframes gradient { + 0% { + background-position: 14% 0; + } + + 50% { + background-position: 87% 100%; + } + + 100% { + background-position: 14% 0; + } + } + + ${props => + (props.shape === 'rect' || props.shape === 'rounded-rect') && + ` + min-height: 12px; + min-width: 92px; + height: 100%; + `} + + ${props => + (props.shape === 'circle' || props.shape === 'square') && + ` + min-width: 48px; + min-height: 48px; + `} + + ${props => + (props.variant === 'image' || props.variant === 'avatar') && + ` + min-width: 48px; + min-height: 48px; + `} + + ${props => + props.shape === 'rounded-rect' && + ` + border-radius: 1rem; + `} + + ${props => + props.shape === 'circle' && + ` + border-radius: 50%; + `} +`; + +export { StyledShapeContainer, StyledImageIcon, StyledAvatarIcon, StyledLoadingShape }; diff --git a/src/components/index.d.ts b/src/components/index.d.ts index b11dc270e..d89e1e327 100644 --- a/src/components/index.d.ts +++ b/src/components/index.d.ts @@ -39,6 +39,7 @@ export { default as GoogleAddressLookup } from './GoogleAddressLookup'; export { default as HelpText } from './HelpText'; export { default as ImportRecordsFlow } from './ImportRecordsFlow'; export { default as Input } from './Input'; +export { default as LoadingShape } from './LoadingShape'; export { default as Lookup } from './Lookup'; export { default as MapMarker } from './MapMarker'; export { default as MarkdownOutput } from './MarkdownOutput'; diff --git a/src/components/index.js b/src/components/index.js index b831a3466..4c78e87d4 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -37,6 +37,7 @@ export { default as GoogleAddressLookup } from './GoogleAddressLookup'; export { default as HelpText } from './HelpText'; export { default as ImportRecordsFlow } from './ImportRecordsFlow'; export { default as Input } from './Input'; +export { default as LoadingShape } from './LoadingShape'; export { default as Lookup } from './Lookup'; export { default as MapMarker } from './MapMarker'; export { default as MarkdownOutput } from './MarkdownOutput';