diff --git a/packages/mobile/__tests__/CountdownCircleTimer.test.js b/packages/mobile/__tests__/CountdownCircleTimer.test.js index 26e6a90..5390007 100644 --- a/packages/mobile/__tests__/CountdownCircleTimer.test.js +++ b/packages/mobile/__tests__/CountdownCircleTimer.test.js @@ -9,7 +9,11 @@ Math.random = () => 0.124578 const fixture = { duration: 10, - colors: [['#004777', 0.33], ['#F7B801', 0.33], ['#A30000']], + colors: [ + ['#004777', 0.33], + ['#F7B801', 0.33], + ['#A30000', 0.33], + ], } describe('snapshot tests', () => { @@ -37,7 +41,19 @@ describe('snapshot tests', () => { it('renders with single color', () => { const tree = renderer .create( - + + {({ remainingTime }) => {remainingTime}} + + ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + it('renders with single color provided as a string', () => { + const tree = renderer + .create( + {({ remainingTime }) => {remainingTime}} ) @@ -51,7 +67,10 @@ describe('snapshot tests', () => { .create( {({ remainingTime }) => {remainingTime}} @@ -74,10 +93,11 @@ describe('functional tests', () => { expect(getByText('4')).toBeTruthy() }) - it('should display 0 at the end of the countdown', () => { + it('should call onComplete at the end of the countdown', () => { global.withAnimatedTimeTravelEnabled(() => { + const onComplete = jest.fn() const { getByText } = render( - + {({ remainingTime }) => {remainingTime}} ) @@ -87,6 +107,7 @@ describe('functional tests', () => { }) expect(getByText('0')).toBeTruthy() + expect(onComplete).toHaveBeenCalledWith(10) }) }) }) @@ -145,7 +166,6 @@ describe('behaviour tests', () => { it('should clear repeat timeout when the component is unmounted', () => { const clearTimeoutMock = jest.fn() - const cancelAnimationFrameMock = jest.fn() global.clearTimeout = clearTimeoutMock diff --git a/packages/mobile/__tests__/__snapshots__/CountdownCircleTimer.test.js.snap b/packages/mobile/__tests__/__snapshots__/CountdownCircleTimer.test.js.snap index 4fb6a24..3567328 100644 --- a/packages/mobile/__tests__/__snapshots__/CountdownCircleTimer.test.js.snap +++ b/packages/mobile/__tests__/__snapshots__/CountdownCircleTimer.test.js.snap @@ -303,6 +303,105 @@ exports[`snapshot tests renders with single color 1`] = ` `; +exports[`snapshot tests renders with single color provided as a string 1`] = ` + + + + + + + + + + 5 + + + +`; + exports[`snapshot tests renders with time 1`] = ` { - const colorsLength = colors.length + if (typeof colors === 'string') { + return colors + } + const colorsLength = colors.length if (colorsLength === 1) { return colors[0][0] } diff --git a/packages/mobile/types/CountdownCircleTimer.d.ts b/packages/mobile/types/CountdownCircleTimer.d.ts index 9591d83..ee7e758 100644 --- a/packages/mobile/types/CountdownCircleTimer.d.ts +++ b/packages/mobile/types/CountdownCircleTimer.d.ts @@ -20,8 +20,8 @@ type Colors = { export interface CountdownCircleTimerProps { /** Countdown duration in seconds */ duration: number - /** Array of tuples: 1st param - color in HEX format; 2nd param - time to transition to next color represented as a fraction of the total duration */ - colors: Colors + /** Single color as a string or an array of tuples: 1st param - color in HEX format; 2nd param - time to transition to next color represented as a fraction of the total duration */ + colors: string | Colors /** Set the initial remaining time if it is different than the duration */ initialRemainingTime?: number /** Width and height of the SVG element. Default: 180 */ diff --git a/packages/shared/components/DefsLinearGradient.jsx b/packages/shared/components/DefsLinearGradient.jsx index 1d829ce..30120d8 100644 --- a/packages/shared/components/DefsLinearGradient.jsx +++ b/packages/shared/components/DefsLinearGradient.jsx @@ -3,8 +3,10 @@ import PropTypes from 'prop-types' import { countdownCircleTimerProps } from '..' const getStopProps = (colors) => { - if (colors.length === 1) { - return [{ offset: 1, stopColor: colors[0][0], key: 0 }] + const isColorsString = typeof colors === 'string' + if (isColorsString || colors.length === 1) { + const stopColor = isColorsString ? colors : colors[0][0] + return [{ offset: 1, stopColor, key: 0 }] } const colorsLength = colors.length diff --git a/packages/shared/utils/colorsValidator.js b/packages/shared/utils/colorsValidator.js index 3be3a01..d2fdac2 100644 --- a/packages/shared/utils/colorsValidator.js +++ b/packages/shared/utils/colorsValidator.js @@ -1,22 +1,43 @@ -export const colorsValidator = ( - propValue, - key, - componentName, - location, - propFullName -) => { - const color = propValue[0] - const duration = propValue[1] +const isHexColor = (color) => color.match(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/) +const getErrorInProp = (propName, componentName) => + `Invalid prop '${propName}' supplied to '${componentName}'` +const getHexColorError = (propName, componentName, type = 'array') => + new Error( + `${getErrorInProp(propName, componentName)}. Expect ${ + type === 'array' ? 'an array of tuples where the first element is a' : '' + } HEX color code string. + .` + ) - if (!color.match(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)) { - return new Error( - `Invalid prop '${propFullName}[0]' supplied to '${componentName}'.Expect a color with HEX color code.` - ) +const validateColorsAsString = (colors, propName, componentName) => { + if (!isHexColor(colors)) { + return getHexColorError(propName, componentName, 'string') } +} + +const validateColorsAsArray = (colors, propName, componentName) => { + for (let index = 0; index < colors.length; index += 1) { + const color = colors[index][0] + const duration = colors[index][1] - if (!(duration === undefined || (duration >= 0 && duration <= 1))) { - return new Error( - `Invalid prop '${propFullName}[1]' supplied to '${componentName}'. Expect a number of color transition duration with value between 0 and 1.` - ) + if (!isHexColor(color)) { + return getHexColorError(propName, componentName) + } + + if (!(duration === undefined || (duration >= 0 && duration <= 1))) { + return new Error( + `${getErrorInProp(propName, componentName)}. + Expect an array of tuples where the second element is a number between 0 and 1 representing color transition duration.` + ) + } } } + +export const colorsValidator = (props, propName, componentName) => { + const colors = props[propName] + if (typeof colors === 'string') { + return validateColorsAsString(colors, propName, componentName) + } + + return validateColorsAsArray(colors, propName, componentName) +} diff --git a/packages/shared/utils/countdownCircleTimerProps.js b/packages/shared/utils/countdownCircleTimerProps.js index d7c46e6..cb15d85 100644 --- a/packages/shared/utils/countdownCircleTimerProps.js +++ b/packages/shared/utils/countdownCircleTimerProps.js @@ -3,8 +3,7 @@ import { colorsValidator } from '.' export const countdownCircleTimerProps = { duration: PropTypes.number.isRequired, - colors: PropTypes.arrayOf(PropTypes.arrayOf(colorsValidator).isRequired) - .isRequired, + colors: colorsValidator, children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), size: PropTypes.number, strokeWidth: PropTypes.number, diff --git a/packages/web/__tests__/CountdownCircleTimer.test.js b/packages/web/__tests__/CountdownCircleTimer.test.js index 1352579..f5c864b 100644 --- a/packages/web/__tests__/CountdownCircleTimer.test.js +++ b/packages/web/__tests__/CountdownCircleTimer.test.js @@ -11,7 +11,11 @@ const useElapsedTime = require('use-elapsed-time') const fixture = { duration: 10, - colors: [['#004777', 0.33], ['#F7B801', 0.33], ['#A30000']], + colors: [ + ['#004777', 0.33], + ['#F7B801', 0.33], + ['#A30000', 0.33], + ], } describe('snapshot tests', () => { @@ -170,7 +174,7 @@ describe('functional tests', () => { expect(path).toHaveAttribute('stroke', 'rgba(163, 0, 0, 1)') }) - it('should the same color at the beginning and end of the animation if only one color is provided', () => { + it('should the same color at the beginning and end of the animation if only one color in an array of colors is provided', () => { useElapsedTime.__setElapsedTime(0) const expectedPathProps = ['stroke', 'rgba(0, 71, 119, 1)'] @@ -189,6 +193,25 @@ describe('functional tests', () => { expect(pathEnd).toHaveAttribute(...expectedPathProps) }) + it('should the same color at the beginning and end of the animation if only one color as a string is provided', () => { + useElapsedTime.__setElapsedTime(0) + + const expectedPathProps = ['stroke', 'rgba(0, 71, 119, 1)'] + const component = () => ( + + ) + const { container, rerender } = render(component()) + + const pathBeginning = container.querySelectorAll('path')[1] + expect(pathBeginning).toHaveAttribute(...expectedPathProps) + + useElapsedTime.__setElapsedTime(10) + rerender(component()) + + const pathEnd = container.querySelectorAll('path')[1] + expect(pathEnd).toHaveAttribute(...expectedPathProps) + }) + it('should transition colors for which the durations sums up to 1', () => { useElapsedTime.__setElapsedTime(0) diff --git a/packages/web/src/utils/colors.js b/packages/web/src/utils/colors.js index 3908033..987121d 100644 --- a/packages/web/src/utils/colors.js +++ b/packages/web/src/utils/colors.js @@ -48,7 +48,8 @@ const getColorsBase = (colors, isGradient) => { } export const getNormalizedColors = (colors, duration, isGradient) => { - const colorsBase = getColorsBase(colors, isGradient) + const allColors = typeof colors === 'string' ? [[colors, 1]] : colors + const colorsBase = getColorsBase(allColors, isGradient) let colorsTotalDuration = 0 return colorsBase.map((color, index) => { diff --git a/packages/web/types/CountdownCircleTimer.d.ts b/packages/web/types/CountdownCircleTimer.d.ts index 61582fc..9053acd 100644 --- a/packages/web/types/CountdownCircleTimer.d.ts +++ b/packages/web/types/CountdownCircleTimer.d.ts @@ -17,8 +17,8 @@ type Colors = { export interface CountdownCircleTimerProps { /** Countdown duration in seconds */ duration: number - /** Array of tuples: 1st param - color in HEX format; 2nd param - time to transition to next color represented as a fraction of the total duration */ - colors: Colors + /** Single color as a string or an array of tuples: 1st param - color in HEX format; 2nd param - time to transition to next color represented as a fraction of the total duration */ + colors: string | Colors /** Set the initial remaining time if it is different than the duration */ initialRemainingTime?: number /** Width and height of the SVG element. Default: 180 */