From a01f85f243c8060f52cbe933bec001d4a9e5d2f0 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Fri, 20 Sep 2019 14:00:29 -0700 Subject: [PATCH 1/3] feat(loaders): deprecate ScheduleContainer BREAKING --- packages/loaders/package.json | 1 + packages/loaders/src/Dots.example.md | 18 +- packages/loaders/src/Dots.js | 171 ++++---- packages/loaders/src/Dots.spec.js | 393 ++++++++++-------- packages/loaders/src/Skeleton.js | 1 - packages/loaders/src/Spinner.js | 204 ++++----- packages/loaders/src/Spinner.spec.js | 27 +- .../containers/ScheduleContainer.example.md | 10 +- .../src/containers/ScheduleContainer.js | 10 + .../loaders/src/hooks/useCSSSVGAnimation.js | 39 ++ packages/loaders/src/styled-elements.js | 41 +- packages/loaders/src/styled-elements.spec.js | 42 +- packages/loaders/src/utils/animations.js | 109 ++++- packages/loaders/src/utils/animations.spec.js | 32 -- packages/loaders/src/utils/dot-coordinates.js | 85 ---- .../loaders/src/utils/dot-coordinates.spec.js | 58 --- 16 files changed, 618 insertions(+), 623 deletions(-) create mode 100644 packages/loaders/src/hooks/useCSSSVGAnimation.js delete mode 100644 packages/loaders/src/utils/animations.spec.js delete mode 100644 packages/loaders/src/utils/dot-coordinates.js delete mode 100644 packages/loaders/src/utils/dot-coordinates.spec.js diff --git a/packages/loaders/package.json b/packages/loaders/package.json index d26af7cb6c5..8aff8e88236 100644 --- a/packages/loaders/package.json +++ b/packages/loaders/package.json @@ -19,6 +19,7 @@ "start": "../../utils/scripts/start.sh" }, "dependencies": { + "@zendeskgarden/container-schedule": "^1.1.0", "@zendeskgarden/css-variables": "^6.1.0", "classnames": "^2.2.5", "polished": "^2.3.3" diff --git a/packages/loaders/src/Dots.example.md b/packages/loaders/src/Dots.example.md index a939af6d11a..99460b66085 100644 --- a/packages/loaders/src/Dots.example.md +++ b/packages/loaders/src/Dots.example.md @@ -82,7 +82,7 @@ const Color = ({ name, color, includeSample }) => @@ -104,14 +104,14 @@ const Color = ({ name, color, includeSample }) => - - + + setState({ velocity: parseFloat(event.target.value) })} - min={-0.5} - max={1} - step={0.05} + value={state.duration} + onChange={event => setState({ duration: parseFloat(event.target.value) })} + min={625} + max={2500} + step={625} /> @@ -139,7 +139,7 @@ const Color = ({ name, color, includeSample }) => - + diff --git a/packages/loaders/src/Dots.js b/packages/loaders/src/Dots.js index 56cada58653..d25747d1383 100644 --- a/packages/loaders/src/Dots.js +++ b/packages/loaders/src/Dots.js @@ -5,112 +5,87 @@ * found at http://www.apache.org/licenses/LICENSE-2.0. */ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; +import { useSchedule } from '@zendeskgarden/container-schedule'; -import { retrieveXCoordinate, retrieveYCoordinate, KEYFRAME_MAX } from './utils/dot-coordinates'; -import { DotsCircle, StyledSVG } from './styled-elements'; -import ScheduleContainer from './containers/ScheduleContainer'; +import { + DotsOneCircle, + DotsTwoCircle, + DotsThreeCircle, + StyledSVG, + LoadingPlaceholder +} from './styled-elements'; +import { useCSSSVGAnimation } from './hooks/useCSSSVGAnimation'; const COMPONENT_ID = 'loaders.dots'; -export default class Dots extends React.Component { - static propTypes = { - /** - * Size of the loader. Can inherit from `font-size` styling. - **/ - size: PropTypes.any, - /** - * Velocity (speed) of the animation. Between -1 and 1. - * This should only be maniuplated at extreme sizes. - **/ - velocity: PropTypes.number, - /** - * Color of the loader. Can inherit from `color` styling. - **/ - color: PropTypes.string, - /** - * Delay in MS to begin loader rendering. This helps prevent - * quick flashes of the loader during normal loading times. - **/ - delayMS: PropTypes.number - }; +/** @component */ +export default function Dots({ + size = 'inherit', + color = 'inherit', + duration = 1250, + delayMS = 750, + ...other +}) { + const { delayComplete } = useSchedule({ duration, delayMS }); + const noAnimatedSVGSupport = useCSSSVGAnimation(); + const dotOne = useRef(null); + const dotTwo = useRef(null); + const dotThree = useRef(null); - static defaultProps = { - size: 'inherit', - color: 'inherit', - velocity: 0.05, - delayMS: 750 - }; + useEffect(() => { + if (noAnimatedSVGSupport && delayComplete) { + const transforms = [ + window.getComputedStyle(dotOne.current).getPropertyValue('transform'), + window.getComputedStyle(dotTwo.current).getPropertyValue('transform'), + window.getComputedStyle(dotThree.current).getPropertyValue('transform') + ]; - state = { - frame: 0, - timestamp: 0 - }; - - performAnimationFrame = (timestamp = 0) => { - const { velocity } = this.props; - - let pinnedVelocity = velocity; - - if (velocity < -1) { - pinnedVelocity = -0.9; - } else if (velocity > 1) { - pinnedVelocity = 1; + dotOne.current.setAttribute('transform', transforms[0]); + dotTwo.current.setAttribute('transform', transforms[1]); + dotThree.current.setAttribute('transform', transforms[2]); } + }); - this.setState(prevState => { - const factor = 1000 + 1000 * pinnedVelocity; - const elapsed = (timestamp - prevState.timestamp) / factor; - const frame = prevState.frame + (elapsed % KEYFRAME_MAX); - - return { frame, timestamp }; - }); - }; - - retrieveFrame = offset => { - const loop = KEYFRAME_MAX * 2; - - return (this.state.frame + offset * loop) % loop; - }; - - render() { - const { size, color, delayMS, ...other } = this.props; - const dotOneFrame = this.retrieveFrame(0); - const dotTwoFrame = this.retrieveFrame(1 / 3); - const dotThreeFrame = this.retrieveFrame(2 / 3); - - return ( - - {() => ( - - - - - - - - )} - - ); + if (!delayComplete && delayMS !== 0) { + return  ; } + + return ( + + + + + + + + ); } + +Dots.propTypes = { + /** + * Size of the loader. Can inherit from `font-size` styling. + **/ + size: PropTypes.any, + /** + * Duration (ms) of the animation. Default is 1250ms. + **/ + duration: PropTypes.number, + /** + * Color of the loader. Can inherit from `color` styling. + **/ + color: PropTypes.string, + /** + * Delay in MS to begin loader rendering. This helps prevent + * quick flashes of the loader during normal loading times. + **/ + delayMS: PropTypes.number +}; diff --git a/packages/loaders/src/Dots.spec.js b/packages/loaders/src/Dots.spec.js index e9986e98778..d87eadd4c8c 100644 --- a/packages/loaders/src/Dots.spec.js +++ b/packages/loaders/src/Dots.spec.js @@ -6,16 +6,25 @@ */ import React from 'react'; -import { render } from 'garden-test-utils'; +import { render, act } from 'garden-test-utils'; +import mockDate from 'mockdate'; import Dots from './Dots'; jest.useFakeTimers(); +const DEFAULT_DATE = new Date(2019, 1, 5, 1, 1, 1); + describe('Dots', () => { beforeEach(() => { clearTimeout.mockClear(); global.cancelAnimationFrame = jest.fn(); global.requestAnimationFrame = jest.fn(); + global.document.elementFromPoint = jest.fn(); + mockDate.set(DEFAULT_DATE); + }); + + afterEach(() => { + mockDate.reset(); }); describe('Loading delay', () => { @@ -28,7 +37,9 @@ describe('Dots', () => { it('shows loader after initial delay', () => { const { queryByTestId } = render(); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(queryByTestId('dots')).not.toBeNull(); }); @@ -38,196 +49,238 @@ describe('Dots', () => { it('updates animation after request animation frame', () => { const { container } = render(); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(container.querySelector('g')).toMatchInlineSnapshot(` - - - - - -`); - - // Requestion animation with 1000 MS delay - requestAnimationFrame.mock.calls[0][0](1000); + .c0 { + -webkit-animation: dMEWJg 1250ms linear infinite; + animation: dMEWJg 1250ms linear infinite; + } + + .c1 { + -webkit-animation: hiIgQS 1250ms linear infinite; + animation: hiIgQS 1250ms linear infinite; + } + + .c2 { + -webkit-animation: jfCXbz 1250ms linear infinite; + animation: jfCXbz 1250ms linear infinite; + } + + + + + + + `); + + act(() => { + // move time forward 1 second + mockDate.set(DEFAULT_DATE.setSeconds(2)); + requestAnimationFrame.mock.calls[0][0](); + }); expect(container.querySelector('g')).toMatchInlineSnapshot(` - - - - - -`); + + + + + + `); }); it('updates animation after request animation frame with negative bound velocity', () => { const { container } = render(); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(container.querySelector('g')).toMatchInlineSnapshot(` - - - - - -`); - - // Requestion animation with 1000 MS delay - requestAnimationFrame.mock.calls[0][0](1000); + .c0 { + -webkit-animation: dMEWJg 1250ms linear infinite; + animation: dMEWJg 1250ms linear infinite; + } + + .c1 { + -webkit-animation: hiIgQS 1250ms linear infinite; + animation: hiIgQS 1250ms linear infinite; + } + + .c2 { + -webkit-animation: jfCXbz 1250ms linear infinite; + animation: jfCXbz 1250ms linear infinite; + } + + + + + + + `); + + act(() => { + // move time forward 1 second + mockDate.set(DEFAULT_DATE.setSeconds(2)); + requestAnimationFrame.mock.calls[0][0](); + }); expect(container.querySelector('g')).toMatchInlineSnapshot(` - - - - - -`); + + + + + + `); }); it('updates animation after request animation frame with positive bound velocity', () => { const { container } = render(); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(container.querySelector('g')).toMatchInlineSnapshot(` - - - - - -`); - - // Requestion animation with 1000 MS delay - requestAnimationFrame.mock.calls[0][0](1000); + .c0 { + -webkit-animation: dMEWJg 1250ms linear infinite; + animation: dMEWJg 1250ms linear infinite; + } + + .c1 { + -webkit-animation: hiIgQS 1250ms linear infinite; + animation: hiIgQS 1250ms linear infinite; + } + + .c2 { + -webkit-animation: jfCXbz 1250ms linear infinite; + animation: jfCXbz 1250ms linear infinite; + } + + + + + + + `); + + act(() => { + // move time forward 1 second + mockDate.set(DEFAULT_DATE.setSeconds(2)); + requestAnimationFrame.mock.calls[0][0](); + }); expect(container.querySelector('g')).toMatchInlineSnapshot(` - - - - - -`); + + + + + + `); }); }); }); diff --git a/packages/loaders/src/Skeleton.js b/packages/loaders/src/Skeleton.js index 56480e348f0..c94bf0ed274 100644 --- a/packages/loaders/src/Skeleton.js +++ b/packages/loaders/src/Skeleton.js @@ -85,7 +85,6 @@ const StyledSkeleton = styled.div.attrs({ ${props => retrieveTheme(COMPONENT_ID, props)}; `; -/* eslint-enable */ /** * Loader used to create Skeleton objects diff --git a/packages/loaders/src/Spinner.js b/packages/loaders/src/Spinner.js index 739ae981bd5..5a9623a106d 100644 --- a/packages/loaders/src/Spinner.js +++ b/packages/loaders/src/Spinner.js @@ -13,125 +13,95 @@ import { DASHARRAY_FRAMES, ROTATION_FRAMES } from './utils/spinner-coordinates'; -import { SpinnerCircle, StyledSVG } from './styled-elements'; -import ScheduleContainer from './containers/ScheduleContainer'; +import { SpinnerCircle, StyledSVG, LoadingPlaceholder } from './styled-elements'; +import { useSchedule } from '@zendeskgarden/container-schedule'; const COMPONENT_ID = 'loaders.spinner'; - -export default class Spinner extends React.Component { - constructor(props) { - super(props); - - this.strokeWidthValues = this.computeFrames(STROKE_WIDTH_FRAMES); - this.rotationValues = this.computeFrames(ROTATION_FRAMES); - this.dasharrayValues = this.computeFrames(DASHARRAY_FRAMES); +const totalFrames = 100; + +const computeFrames = (frames, duration) => { + return Object.entries(frames).reduce((acc, item, index, arr) => { + const [frame, value] = item; + const [nextFrame, nextValue] = arr[index + 1] || [totalFrames, arr[0][1]]; + const diff = nextFrame - frame; + const frameHz = 1000 / 60; + + let subDuration = (duration / (totalFrames - 1)) * diff; + let lastValue = value; + + acc[frame] = value; + for (let idx = 0; idx < diff; idx++) { + lastValue = lastValue + (nextValue - lastValue) * (frameHz / subDuration); + subDuration = (duration / (totalFrames - 1)) * (diff - idx); + + acc[parseInt(frame, 10) + idx + 1] = lastValue; + } + acc[nextFrame] = nextValue; + + return acc; + }, {}); +}; + +/** @component */ +export default function Spinner({ + size = 'inherit', + duration = 1250, + color = 'inherit', + delayMS = 750, + ...other +}) { + const strokeWidthValues = computeFrames(STROKE_WIDTH_FRAMES, duration); + const rotationValues = computeFrames(ROTATION_FRAMES, duration); + const dasharrayValues = computeFrames(DASHARRAY_FRAMES, duration); + + const { elapsed, delayComplete } = useSchedule({ duration, delayMS }); + const frame = (elapsed * 100).toFixed(0); + + const strokeWidthValue = strokeWidthValues[frame]; + const rotationValue = rotationValues[frame]; + const dasharrayValue = dasharrayValues[frame]; + + const WIDTH = 80; + const HEIGHT = 80; + + if (!delayComplete && delayMS !== 0) { + return  ; } - static propTypes = { - /** - * Size of the loader. Can inherit from `font-size` styling. - **/ - size: PropTypes.any, - /** - * Duration (ms) of the animation. Default is 1250ms. - **/ - duration: PropTypes.number, - /** - * Color of the loader. Can inherit from `color` styling. - **/ - color: PropTypes.string, - /** - * Delay in MS to begin loader rendering. This helps prevent - * quick flashes of the loader during normal loading times. - **/ - delayMS: PropTypes.number - }; - - static defaultProps = { - size: 'inherit', - color: 'inherit', - delayMS: 750, - duration: 1250 - }; - - state = { - frame: 0, - rawFrame: 0, - totalFrames: 100, - delayComplete: false, - timestamp: 0 - }; - - computeFrames = frames => { - const { duration } = this.props; - const { totalFrames } = this.state; - - return Object.entries(frames).reduce((acc, item, index, arr) => { - const [frame, value] = item; - const [nextFrame, nextValue] = arr[index + 1] || [totalFrames, arr[0][1]]; - const diff = nextFrame - frame; - const frameHz = 1000 / 60; - - let subDuration = (duration / (totalFrames - 1)) * diff; - let lastValue = value; - - acc[frame] = value; - for (let idx = 0; idx < diff; idx++) { - lastValue = lastValue + (nextValue - lastValue) * (frameHz / subDuration); - subDuration = (duration / (totalFrames - 1)) * (diff - idx); - - acc[parseInt(frame, 10) + idx + 1] = lastValue; - } - acc[nextFrame] = nextValue; - - return acc; - }, {}); - }; - - performAnimationFrame = (nowTime = 0) => { - const { totalFrames, rawFrame, timestamp } = this.state; - const { duration } = this.props; - const elapsedTime = nowTime - timestamp; - - this.setState(() => { - const frameMultiplier = (totalFrames + 1) / duration; - const nextValue = rawFrame + elapsedTime * frameMultiplier; - const actualFrame = Math.floor(nextValue); - const frame = actualFrame % totalFrames; - const currentRawFrame = nextValue % totalFrames; - - return { frame, rawFrame: currentRawFrame, timestamp: nowTime }; - }); - }; - - render() { - const { size, color, delayMS, ...other } = this.props; - const { frame } = this.state; - const strokeWidthValue = this.strokeWidthValues[frame]; - const rotationValue = this.rotationValues[frame]; - const dasharrayValue = this.dasharrayValues[frame]; - const WIDTH = 80; - const HEIGHT = 80; - - return ( - - {() => ( - - - - )} - - ); - } + return ( + + + + ); } + +Spinner.propTypes = { + /** + * Size of the loader. Can inherit from `font-size` styling. + **/ + size: PropTypes.any, + /** + * Duration (ms) of the animation. Default is 1250ms. + **/ + duration: PropTypes.number, + /** + * Color of the loader. Can inherit from `color` styling. + **/ + color: PropTypes.string, + /** + * Delay in MS to begin loader rendering. This helps prevent + * quick flashes of the loader during normal loading times. + **/ + delayMS: PropTypes.number +}; diff --git a/packages/loaders/src/Spinner.spec.js b/packages/loaders/src/Spinner.spec.js index 7242d320e1f..005e49f745e 100644 --- a/packages/loaders/src/Spinner.spec.js +++ b/packages/loaders/src/Spinner.spec.js @@ -6,16 +6,24 @@ */ import React from 'react'; -import { render } from 'garden-test-utils'; +import { render, act } from 'garden-test-utils'; +import mockDate from 'mockdate'; import Spinner from './Spinner'; jest.useFakeTimers(); +const DEFAULT_DATE = new Date(2019, 1, 5, 1, 1, 1); + describe('Spinner', () => { beforeEach(() => { clearTimeout.mockClear(); global.cancelAnimationFrame = jest.fn(); global.requestAnimationFrame = jest.fn(); + mockDate.set(DEFAULT_DATE); + }); + + afterEach(() => { + mockDate.reset(); }); describe('Loading delay', () => { @@ -28,7 +36,9 @@ describe('Spinner', () => { it('shows loader after initial delay', () => { const { queryByTestId } = render(); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(queryByTestId('spinner')).not.toBeNull(); }); @@ -38,7 +48,9 @@ describe('Spinner', () => { it('updates animation after request animation frame', () => { const { container } = render(); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(container.firstChild.firstChild).toMatchInlineSnapshot( ` @@ -57,13 +69,16 @@ describe('Spinner', () => { ` ); - // Requestion animation with 1000 MS delay - requestAnimationFrame.mock.calls[0][0](1000); + act(() => { + // move time forward 1 second + mockDate.set(DEFAULT_DATE.setSeconds(2)); + requestAnimationFrame.mock.calls[0][0](); + }); expect(container.firstChild.firstChild).toMatchInlineSnapshot( ` { + const [canAnimateSVG, setAnimateSVG] = useState(true); + + useEffect(() => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + + svg.setAttribute('viewBox', '0 0 2 2'); + svg.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 2px; height: 2px;'); + + rect.setAttribute('style', 'transform: translate(1px, 1px);'); + rect.setAttribute('width', '1'); + rect.setAttribute('height', '1'); + + svg.appendChild(rect); + + document.body.appendChild(svg); + + const result = document.elementFromPoint(1, 1) === svg; + + svg.parentNode.removeChild(svg); + + setAnimateSVG(result); + }, []); + + return canAnimateSVG; +}; diff --git a/packages/loaders/src/styled-elements.js b/packages/loaders/src/styled-elements.js index 015d572c13e..110400fb51b 100644 --- a/packages/loaders/src/styled-elements.js +++ b/packages/loaders/src/styled-elements.js @@ -7,19 +7,42 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { retrieveTheme } from '@zendeskgarden/react-theming'; -export const DotsCircle = styled.circle.attrs(props => ({ - cx: 9, - cy: 9, - r: 9, - transform: props.transform +import { dotOneKeyframes, dotTwoKeyframes, dotThreeKeyframes } from './utils/animations'; + +const DotsCircle = styled.circle.attrs(() => ({ + cy: 36, + r: 9 }))``; -DotsCircle.propTypes = { - transform: PropTypes.string -}; +export const DotsOneCircle = styled(DotsCircle).attrs(() => ({ + cx: 9 +}))` + animation: ${({ duration }) => + css` + ${dotOneKeyframes} ${duration}ms linear infinite + `}; +`; + +export const DotsTwoCircle = styled(DotsCircle).attrs(() => ({ + cx: 40 +}))` + animation: ${({ duration }) => + css` + ${dotTwoKeyframes} ${duration}ms linear infinite + `}; +`; + +export const DotsThreeCircle = styled(DotsCircle).attrs(() => ({ + cx: 71 +}))` + animation: ${({ duration }) => + css` + ${dotThreeKeyframes} ${duration}ms linear infinite + `}; +`; export const SpinnerCircle = styled.circle.attrs(props => ({ cx: 40, diff --git a/packages/loaders/src/styled-elements.spec.js b/packages/loaders/src/styled-elements.spec.js index 0df927c92bf..9d04e13c56f 100644 --- a/packages/loaders/src/styled-elements.spec.js +++ b/packages/loaders/src/styled-elements.spec.js @@ -7,18 +7,28 @@ import React from 'react'; import { render } from 'garden-test-utils'; -import { DotsCircle, SpinnerCircle, StyledSVG } from './styled-elements'; +import { + DotsOneCircle, + DotsTwoCircle, + DotsThreeCircle, + SpinnerCircle, + StyledSVG +} from './styled-elements'; describe('Loader styled-elements', () => { - describe('DotsCircle', () => { - it('applies transform correctly', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('circle')).toHaveAttribute('transform', '2'); + describe('DotsCircles', () => { + const cx = [9, 40, 71]; + + [DotsOneCircle, DotsTwoCircle, DotsThreeCircle].forEach((Circle, index) => { + it(`applies correct cx=${cx[index]} coord`, () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('circle')).toHaveAttribute('cx', `${cx[index]}`); + }); }); }); @@ -44,26 +54,28 @@ describe('Loader styled-elements', () => { }); describe('StyledSVG', () => { + const props = { 'data-garden-id': 'StyledSVG' }; + it('applies font-size if provided', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('font-size', '12px'); }); it('defaults font-size to inherit if not provided', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('font-size', 'inherit'); }); it('applies color if provided', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('color', 'red'); }); it('defaults color to inherit if not provided', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('color', 'inherit'); }); @@ -72,7 +84,7 @@ describe('Loader styled-elements', () => { const width = '2em'; const height = '4em'; - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveAttribute('width', width); expect(container.firstChild).toHaveAttribute('height', height); diff --git a/packages/loaders/src/utils/animations.js b/packages/loaders/src/utils/animations.js index 31bd615eb22..60ec3fd768e 100644 --- a/packages/loaders/src/utils/animations.js +++ b/packages/loaders/src/utils/animations.js @@ -5,28 +5,93 @@ * found at http://www.apache.org/licenses/LICENSE-2.0. */ -/** - * Accelerating from zero velocity - * @param {Number} time Time - */ -export function easeInCubic(time) { - return time * time * time; -} +import { keyframes } from 'styled-components'; -/** - * Decelerating to zero velocity - * @param {Number} time Time - */ -export function easeOutCubic(time) { - const value = time - 1; +/* stylelint-disable rule-empty-line-before */ +export const dotOneKeyframes = keyframes` + 0% { transform: translate(0, 5px); } + 3% { transform: translate(1px, -5px); } + 6% { transform: translate(3px, -15px); } + 8% { transform: translate(5px, -18px); } + 9% { transform: translate(7px, -21px); } + 11% { transform: translate(8px, -22px); } + 13% { transform: translate(9px, -23px); } + 16% { transform: translate(12px, -25px); } + 18% { transform: translate(13px, -26px); } + 23% { transform: translate(18px, -26px); } + 24% { transform: translate(19px, -25px); } + 28% { transform: translate(22px, -23px); } + 31% { transform: translate(24px, -21px); } + 33% { transform: translate(26px, -18px); } + 34% { transform: translate(28px, -14px); } + 36% { transform: translate(29px, -12px); } + 38% { transform: translate(30px, -5px); } + 39% { transform: translate(31px, 5px); } + 54% { transform: translate(31px, 3px); } + 59% { transform: translate(33px); } + 61% { transform: translate(43px); } + 63% { transform: translate(48px); } + 64% { transform: translate(51px); } + 66% { transform: translate(53px); } + 68% { transform: translate(55px); } + 69% { transform: translate(57px); } + 76% { transform: translate(60px); } + 81% { transform: translate(61px); } + 83%, 100% { transform: translate(62px); } +`; - return value * value * value + 1; -} +export const dotTwoKeyframes = keyframes` + 4% { transform: translate(0); } + 6% { transform: translate(-1px); } + 8% { transform: translate(-2px); } + 9% { transform: translate(-5px); } + 11% { transform: translate(-7px); } + 13% { transform: translate(-12px); } + 14% { transform: translate(-17px); } + 16% { transform: translate(-19px); } + 18% { transform: translate(-22px); } + 19% { transform: translate(-25px); } + 21% { transform: translate(-26px); } + 23% { transform: translate(-27px); } + 24% { transform: translate(-28px); } + 26% { transform: translate(-29px); } + 29% { transform: translate(-30px); } + 33%, 89% { transform: translate(-31px); } + 91% { transform: translate(-31px, 1px); } + 94% { transform: translate(-31px, 2px); } + 98% { transform: translate(-31px, 3px); } + 99% { transform: translate(-31px, 4px); } + 100% { transform: translate(-31px, 5px); } +`; -/** - * Acceleration until halfway, then deceleration - * @param {Number} time Time - */ -export function easeInOutCubic(time) { - return time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; -} +export const dotThreeKeyframes = keyframes` + 39% { transform: translate(0); } + 44% { transform: translate(0, 1px); } + 46% { transform: translate(0, 2px); } + 48% { transform: translate(0, 3px); } + 49% { transform: translate(0, 4px); } + 51% { transform: translate(0, 5px); } + 53% { transform: translate(-1px, -6px); } + 54% { transform: translate(-2px, -13px); } + 56% { transform: translate(-3px, -15px); } + 58% { transform: translate(-5px, -19px); } + 59% { transform: translate(-7px, -21px); } + 61% { transform: translate(-8px, -22px); } + 63% { transform: translate(-9px, -24px); } + 64% { transform: translate(-11px, -25px); } + 66% { transform: translate(-12px, -26px); } + 74% { transform: translate(-19px, -26px); } + 76% { transform: translate(-20px, -25px); } + 78% { transform: translate(-22px, -24px); } + 81% { transform: translate(-24px, -21px); } + 83% { transform: translate(-26px, -19px); } + 84% { transform: translate(-28px, -15px); } + 86% { transform: translate(-29px, -13px); } + 88% { transform: translate(-30px, -6px); } + 89% { transform: translate(-31px, 5px); } + 91% { transform: translate(-31px, 4px); } + 93% { transform: translate(-31px, 3px); } + 94% { transform: translate(-31px, 2px); } + 98% { transform: translate(-31px, 1px); } + 100% { transform: translate(-31px); } +`; diff --git a/packages/loaders/src/utils/animations.spec.js b/packages/loaders/src/utils/animations.spec.js deleted file mode 100644 index 0dd3622c78f..00000000000 --- a/packages/loaders/src/utils/animations.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import { easeInCubic, easeOutCubic, easeInOutCubic } from './animations'; - -describe('animations', () => { - describe('easeInCubic()', () => { - it('returns correct value based on time', () => { - expect(easeInCubic(2)).toBe(8); - }); - }); - - describe('easeOutCubic()', () => { - it('returns correct value based on time', () => { - expect(easeOutCubic(2)).toBe(2); - }); - }); - - describe('easeInOutCubic()', () => { - it('returns correct value when time is less than 0.5', () => { - expect(easeInOutCubic(0.45)).toBeCloseTo(0.3645); - }); - - it('returns correct value when time is greater than 0.5', () => { - expect(easeInOutCubic(3)).toBe(33); - }); - }); -}); diff --git a/packages/loaders/src/utils/dot-coordinates.js b/packages/loaders/src/utils/dot-coordinates.js deleted file mode 100644 index 554cb5d7e95..00000000000 --- a/packages/loaders/src/utils/dot-coordinates.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import { easeInCubic, easeInOutCubic, easeOutCubic } from './animations'; - -export const KEYFRAME_1 = 0.166666667; -export const KEYFRAME_2 = 0.55; -export const KEYFRAME_3 = 1.166666667; -export const KEYFRAME_4 = 1.333333333; -export const KEYFRAME_5 = 1.533333333; -export const KEYFRAME_MAX = 1.766666667; - -const WIDTH = 80; -const HEIGHT = WIDTH * 0.9; -const CIRCLE_RADIUS = WIDTH * 0.1125; -const MID_X = WIDTH / 2 - CIRCLE_RADIUS; -const MID_Y = HEIGHT / 2 - CIRCLE_RADIUS; -const BOTTOM = MID_Y + 5; - -/** - * Retrieve the X coordinate value - * @param {Number} frame The current frame - */ -export function retrieveXCoordinate(frame) { - let retVal; - - const _frame = frame % KEYFRAME_MAX; - - if (_frame < KEYFRAME_1) { - return MID_X; - } else if (_frame < KEYFRAME_2) { - const frameValue = _frame - KEYFRAME_1; - const frameMaximum = KEYFRAME_2 - KEYFRAME_1; - const easeValue = easeInOutCubic(frameValue / frameMaximum); - - retVal = MID_X - easeValue * MID_X; - } else if (_frame < KEYFRAME_4) { - retVal = 0; - } else { - const frameValue = _frame - KEYFRAME_4; - const frameMaximum = KEYFRAME_MAX - KEYFRAME_4; - - retVal = MID_X * (frameValue / frameMaximum); - } - - if (frame >= KEYFRAME_MAX) { - retVal = MID_X * 2 - retVal; - } - - return retVal; -} - -/** - * Retrieve the Y coordinate value - * @param {Number} frame The current frame - */ -export function retrieveYCoordinate(frame) { - const _frame = frame % KEYFRAME_MAX; - - if (_frame < KEYFRAME_1) { - return (_frame / KEYFRAME_1) * -1 * (BOTTOM - MID_Y) + BOTTOM; - } else if (_frame < KEYFRAME_3) { - return MID_Y; - } else if (_frame < KEYFRAME_4) { - const frameValue = _frame - KEYFRAME_3; - const frameMaximum = KEYFRAME_4 - KEYFRAME_3; - - return (frameValue / frameMaximum) * (BOTTOM - MID_Y) + MID_Y; - } else if (_frame < KEYFRAME_5) { - const frameValue = _frame - KEYFRAME_4; - const frameMaximum = KEYFRAME_5 - KEYFRAME_4; - const easeValue = easeOutCubic(frameValue / frameMaximum); - - return BOTTOM - easeValue * BOTTOM; - } - const frameValue = _frame - KEYFRAME_5; - const frameMaximum = KEYFRAME_MAX - KEYFRAME_5; - const easeValue = easeInCubic(frameValue / frameMaximum); - - return easeValue * BOTTOM; -} diff --git a/packages/loaders/src/utils/dot-coordinates.spec.js b/packages/loaders/src/utils/dot-coordinates.spec.js deleted file mode 100644 index 5d386095680..00000000000 --- a/packages/loaders/src/utils/dot-coordinates.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import { retrieveXCoordinate, retrieveYCoordinate } from './dot-coordinates'; - -describe('Dot Coordinates', () => { - describe('retrieveXCoordinate()', () => { - it('returns MID_X if sub-frame is within KEYFRAME_1', () => { - expect(retrieveXCoordinate(0.111)).toBe(31); - }); - - it('returns eased MID_X if sub-frame is within KEYFRAME_2', () => { - expect(retrieveXCoordinate(0.5)).toBeCloseTo(0.27517); - }); - - it('returns 0 if sub-frame is within KEYFRAME_4', () => { - expect(retrieveXCoordinate(1.3)).toBe(0); - }); - - it('returns MID_X upper transform if sub-frame is within KEYFRAME_MAX', () => { - expect(retrieveXCoordinate(1.8)).toBe(31); - }); - - it('returns MID_X lower transform if sub-frame otherwise', () => { - expect(retrieveXCoordinate(1.6)).toBeCloseTo(19.0769); - }); - - it('returns correct value if sub-frame is greater than KEYFRAME_MAX', () => { - expect(retrieveXCoordinate(3)).toBe(62); - }); - }); - - describe('retrieveYCoordinate()', () => { - it('returns correct value if sub-frame is within KEYFRAME_1', () => { - expect(retrieveYCoordinate(0.111)).toBeCloseTo(28.67); - }); - - it('returns correct value if sub-frame is within KEYFRAME_3', () => { - expect(retrieveYCoordinate(0.5)).toBe(27); - }); - - it('returns correct value if sub-frame is within KEYFRAME_4', () => { - expect(retrieveYCoordinate(1.3)).toBeCloseTo(31.0); - }); - - it('returns correct value if sub-frame is within KEYFRAME_5', () => { - expect(retrieveYCoordinate(1.5)).toBeCloseTo(0.148); - }); - - it('returns correct value otherwise', () => { - expect(retrieveYCoordinate(1.7)).toBeCloseTo(11.6618); - }); - }); -}); From d3af9cd5f70dbac664b4ab87aeed141234bbca89 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 23 Sep 2019 09:08:50 -0700 Subject: [PATCH 2/3] Remove ScheduleContainer --- .../containers/ScheduleContainer.example.md | 17 ---- .../src/containers/ScheduleContainer.js | 89 ------------------- .../src/containers/ScheduleContainer.spec.js | 68 -------------- packages/loaders/styleguide.config.js | 4 - 4 files changed, 178 deletions(-) delete mode 100644 packages/loaders/src/containers/ScheduleContainer.example.md delete mode 100644 packages/loaders/src/containers/ScheduleContainer.js delete mode 100644 packages/loaders/src/containers/ScheduleContainer.spec.js diff --git a/packages/loaders/src/containers/ScheduleContainer.example.md b/packages/loaders/src/containers/ScheduleContainer.example.md deleted file mode 100644 index fa149503f0a..00000000000 --- a/packages/loaders/src/containers/ScheduleContainer.example.md +++ /dev/null @@ -1,17 +0,0 @@ -## DEPRECATION WARNING - -This component has been deprecated in favor of the API provided in the -[@zendeskgarden/container-schedule](https://www.npmjs.com/package/@zendeskgarden/container-schedule) -package. - -This component will be removed in a future major release. - -```jsx static -initialState = { - count: 0 -}; - - setState({ count: state.count + 1 })}> - {() =>

{state.count}

} -
; -``` diff --git a/packages/loaders/src/containers/ScheduleContainer.js b/packages/loaders/src/containers/ScheduleContainer.js deleted file mode 100644 index 6f6adfd723e..00000000000 --- a/packages/loaders/src/containers/ScheduleContainer.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { LoadingPlaceholder } from '../styled-elements'; - -export default class ScheduleContainer extends Component { - static propTypes = { - /** - * Size of the loader. Can inherit from `font-size` styling. - **/ - size: PropTypes.any, - /** - * Delay in MS to begin loader rendering. This helps prevent - * quick flashes of the loader during normal loading times. - **/ - delayMS: PropTypes.number, - /** - * Function to call on each animation frame. - **/ - tick: PropTypes.func, - children: PropTypes.func, - /** - * Identical to children - */ - render: PropTypes.func - }; - - static defaultProps = { - delayMS: 750 - }; - - constructor(props) { - super(props); - - this.state = { - delayComplete: false - }; - } - - componentDidMount() { - const { delayMS } = this.props; - - this.renderingDelayTimeout = setTimeout(() => { - this.setState({ delayComplete: true }, () => { - this.performAnimationFrame(); - }); - }, delayMS); - - if (process.env.NODE_ENV !== 'production') { - /* eslint-disable no-console */ - console.warn( - 'Deprecation Warning: The `ScheduleContainer` component has been deprecated. ' + - 'It will be removed in an upcoming major release. Migrate to the ' + - '`@zendeskgarden/container-schedule` package to continue receiving updates.' - ); - /* eslint-enable */ - } - } - - componentWillUnmount() { - clearTimeout(this.renderingDelayTimeout); - cancelAnimationFrame(this.tick); - } - - performAnimationFrame() { - this.tick = requestAnimationFrame(timestamp => { - this.props.tick(timestamp); - this.performAnimationFrame(); - }); - } - - render() { - const { delayMS, size, children, render = children } = this.props; - const { delayComplete } = this.state; - - if (!delayComplete && delayMS !== 0) { - return  ; - } - - return render(); - } -} diff --git a/packages/loaders/src/containers/ScheduleContainer.spec.js b/packages/loaders/src/containers/ScheduleContainer.spec.js deleted file mode 100644 index db1ecff188a..00000000000 --- a/packages/loaders/src/containers/ScheduleContainer.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import React from 'react'; -import { render } from 'garden-test-utils'; -import ScheduleContainer from './ScheduleContainer'; - -describe('ScheduleContainer', () => { - const Example = props => ( - - {() =>

Example content

} -
- ); - - beforeEach(() => { - jest.useFakeTimers(); - global.cancelAnimationFrame = jest.fn(); - global.requestAnimationFrame = jest.fn(); - }); - - it('hides content until default delay time has passed', () => { - const { queryByTestId } = render(); - - expect(queryByTestId('content')).toBeNull(); - jest.runOnlyPendingTimers(); - expect(queryByTestId('content')).not.toBeNull(); - }); - - it('hides content until custom delay time has passed', () => { - const { queryByTestId } = render(); - - expect(queryByTestId('content')).toBeNull(); - jest.runTimersToTime(50); - expect(queryByTestId('content')).not.toBeNull(); - }); - - it('shows content if delayMs is 0', () => { - const { queryByTestId } = render(); - - expect(queryByTestId('content')).not.toBeNull(); - }); - - it('removes events when component is unmounted', () => { - const { unmount } = render(); - - unmount(); - expect(clearTimeout).toHaveBeenCalled(); - expect(cancelAnimationFrame).toHaveBeenCalled(); - }); - - it('calls tick with timestamp when requestAnimationFrame is triggered', () => { - const tickSpy = jest.fn(); - - render(); - - // Run timer to start requestAnimationFrame - jest.runOnlyPendingTimers(); - - // Run first animation frame - requestAnimationFrame.mock.calls[0][0](); - - expect(tickSpy).toHaveBeenCalled(); - }); -}); diff --git a/packages/loaders/styleguide.config.js b/packages/loaders/styleguide.config.js index 509292061a4..e0bf913c6b2 100644 --- a/packages/loaders/styleguide.config.js +++ b/packages/loaders/styleguide.config.js @@ -18,10 +18,6 @@ module.exports = { { name: 'Components', components: '../../packages/loaders/src/[A-Z]*.js' - }, - { - name: 'Containers', - components: '../../packages/loaders/src/containers/[A-Z]*.js' } ] }; From f4644876d2bf848177ddb0905ae6378ccd4dc0ec Mon Sep 17 00:00:00 2001 From: Austin Green Date: Wed, 16 Oct 2019 09:33:38 -0700 Subject: [PATCH 3/3] Correct rebase issues --- packages/loaders/src/Dots.example.md | 4 ++-- packages/loaders/src/Progress.example.md | 6 +++--- packages/loaders/src/Spinner.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/loaders/src/Dots.example.md b/packages/loaders/src/Dots.example.md index 99460b66085..5a10e45ce2f 100644 --- a/packages/loaders/src/Dots.example.md +++ b/packages/loaders/src/Dots.example.md @@ -104,8 +104,8 @@ const Color = ({ name, color, includeSample }) => - - + + setState({ duration: parseFloat(event.target.value) })} diff --git a/packages/loaders/src/Progress.example.md b/packages/loaders/src/Progress.example.md index 483c9353b8b..242f42a16cb 100644 --- a/packages/loaders/src/Progress.example.md +++ b/packages/loaders/src/Progress.example.md @@ -78,7 +78,7 @@ const { zdColorGreen600, zdColorRed600 } = require('@zendeskgarden/css-variables'); -const { RangeField, Label, Range } = require('@zendeskgarden/react-ranges/src'); +const { Field: FormField, Label, Range } = require('@zendeskgarden/react-forms/src'); const { Dropdown, Field, @@ -208,7 +208,7 @@ const Color = ({ name, color, includeSample }) => - + max={100} step={1} /> - + diff --git a/packages/loaders/src/Spinner.js b/packages/loaders/src/Spinner.js index 5a9623a106d..30079bb6ecb 100644 --- a/packages/loaders/src/Spinner.js +++ b/packages/loaders/src/Spinner.js @@ -78,8 +78,8 @@ export default function Spinner({ {...other} >