Skip to content

Commit

Permalink
feat(web, mobile): allow passing string as a single color to colors prop
Browse files Browse the repository at this point in the history
  • Loading branch information
vydimitrov committed Jul 21, 2020
1 parent ae12279 commit f5ba08c
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 35 deletions.
32 changes: 26 additions & 6 deletions packages/mobile/__tests__/CountdownCircleTimer.test.js
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -37,7 +41,19 @@ describe('snapshot tests', () => {
it('renders with single color', () => {
const tree = renderer
.create(
<CountdownCircleTimer duration={5} colors={[['#004777']]}>
<CountdownCircleTimer duration={5} colors={[['#004777', 1]]}>
{({ remainingTime }) => <Text>{remainingTime}</Text>}
</CountdownCircleTimer>
)
.toJSON()

expect(tree).toMatchSnapshot()
})

it('renders with single color provided as a string', () => {
const tree = renderer
.create(
<CountdownCircleTimer duration={5} colors="#004777">
{({ remainingTime }) => <Text>{remainingTime}</Text>}
</CountdownCircleTimer>
)
Expand All @@ -51,7 +67,10 @@ describe('snapshot tests', () => {
.create(
<CountdownCircleTimer
duration={5}
colors={[['#004777', 0.4], ['#aabbcc']]}
colors={[
['#004777', 0.4],
['#aabbcc', 0.6],
]}
isLinearGradient
>
{({ remainingTime }) => <Text>{remainingTime}</Text>}
Expand All @@ -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(
<CountdownCircleTimer {...fixture} isPlaying>
<CountdownCircleTimer {...fixture} isPlaying onComplete={onComplete}>
{({ remainingTime }) => <Text>{remainingTime}</Text>}
</CountdownCircleTimer>
)
Expand All @@ -87,6 +107,7 @@ describe('functional tests', () => {
})

expect(getByText('0')).toBeTruthy()
expect(onComplete).toHaveBeenCalledWith(10)
})
})
})
Expand Down Expand Up @@ -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

Expand Down
Expand Up @@ -303,6 +303,105 @@ exports[`snapshot tests renders with single color 1`] = `
</View>
`;

exports[`snapshot tests renders with single color provided as a string 1`] = `
<View
accessibilityLabel="Countdown timer"
accessible={true}
style={
Object {
"height": 180,
"position": "relative",
"width": 180,
}
}
>
<RNSVGSvgView
bbHeight={180}
bbWidth={180}
focusable={false}
height={180}
style={
Array [
Object {
"backgroundColor": "transparent",
"borderWidth": 0,
},
Object {
"flex": 0,
"height": 180,
"width": 180,
},
]
}
width={180}
>
<RNSVGGroup>
<RNSVGPath
d="m 90,6
a 84,84 0 1,0 0,168
a 84,84 0 1,0 0,-168"
fill={null}
propList={
Array [
"fill",
"stroke",
"strokeWidth",
]
}
stroke={4292467161}
strokeWidth={12}
/>
<RNSVGPath
d="m 90,6
a 84,84 0 1,0 0,168
a 84,84 0 1,0 0,-168"
fill={null}
propList={
Array [
"fill",
"stroke",
"strokeWidth",
"strokeDasharray",
"strokeDashoffset",
"strokeLinecap",
]
}
stroke={4278208375}
strokeDasharray={
Array [
527.7875658030853,
527.7875658030853,
]
}
strokeDashoffset={null}
strokeLinecap={1}
strokeWidth={12}
/>
</RNSVGGroup>
</RNSVGSvgView>
<View
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
style={
Object {
"alignItems": "center",
"display": "flex",
"height": "100%",
"justifyContent": "center",
"left": 0,
"position": "absolute",
"top": 0,
"width": "100%",
}
}
>
<Text>
5
</Text>
</View>
</View>
`;

exports[`snapshot tests renders with time 1`] = `
<View
accessibilityLabel="Countdown timer"
Expand Down
5 changes: 4 additions & 1 deletion packages/mobile/src/utils/getStroke.js
Expand Up @@ -3,8 +3,11 @@ export const getStroke = ({
animatedElapsedTime,
durationMilliseconds,
}) => {
const colorsLength = colors.length
if (typeof colors === 'string') {
return colors
}

const colorsLength = colors.length
if (colorsLength === 1) {
return colors[0][0]
}
Expand Down
4 changes: 2 additions & 2 deletions packages/mobile/types/CountdownCircleTimer.d.ts
Expand Up @@ -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 */
Expand Down
6 changes: 4 additions & 2 deletions packages/shared/components/DefsLinearGradient.jsx
Expand Up @@ -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
Expand Down
55 changes: 38 additions & 17 deletions 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)
}
3 changes: 1 addition & 2 deletions packages/shared/utils/countdownCircleTimerProps.js
Expand Up @@ -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,
Expand Down
27 changes: 25 additions & 2 deletions packages/web/__tests__/CountdownCircleTimer.test.js
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)']
Expand All @@ -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 = () => (
<CountdownCircleTimer {...fixture} colors="#004777" />
)
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)

Expand Down
3 changes: 2 additions & 1 deletion packages/web/src/utils/colors.js
Expand Up @@ -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) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/web/types/CountdownCircleTimer.d.ts
Expand Up @@ -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 */
Expand Down

0 comments on commit f5ba08c

Please sign in to comment.