diff --git a/package.json b/package.json
index cdcd285..73d82eb 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,11 @@
{
"name": "react-time-duration-input",
- "version": "0.0.0-development",
+ "version": "1.0.0",
"description": "React component for input time durations",
"main": "dist/index.js",
"scripts": {
"test": "jest",
+ "test-watch": "jest --watch",
"coverage": "jest --coverage",
"lint": "eslint ./src/**/*.js",
"prepublishOnly": "babel ./src --ignore src/**/*.test.js --out-dir ./dist -s inline",
diff --git a/src/components/TimeDurationInput/index.js b/src/components/TimeDurationInput/index.js
index b90b823..735d72a 100644
--- a/src/components/TimeDurationInput/index.js
+++ b/src/components/TimeDurationInput/index.js
@@ -1,18 +1,19 @@
import React, { useState, useCallback, useEffect } from 'react'
import PropTypes from 'prop-types'
-export function TimeDurationInput ({ value, onChange, className }) {
- const [ duration, setDuration ] = useState(convertValueToDuration(value))
+export function TimeDurationInput ({ value, scale, onChange, className }) {
+ const [ duration, setDuration ] = useState(convertFromValue(value, scale))
+
useEffect(() => {
- const newDuration = convertValueToDuration(value)
+ const newDuration = convertFromValue(value, scale)
if (newDuration !== duration) setDuration(newDuration)
- }, [ value ])
+ }, [ value, scale ])
const onInputChange = useCallback(({ target }) => {
setDuration(target.value)
- const newValue = convertDurationToValue(target.value)
+ const newValue = convertToValue(target.value, scale)
if (!isNaN(newValue)) onChange(newValue)
- }, [ onChange ])
+ }, [ onChange, scale ])
return (
@@ -21,14 +22,32 @@ export function TimeDurationInput ({ value, onChange, className }) {
TimeDurationInput.propTypes = {
value: PropTypes.number,
+ scale: PropTypes.oneOf([ 'd', 'h', 'm', 's', 'ms' ]),
onChange: PropTypes.func,
className: PropTypes.string
}
TimeDurationInput.defaultProps = {
+ scale: 'ms',
onChange: () => {}
}
+export const SCALE_CONVERSIONS = {
+ ms: 1,
+ s: 1000,
+ m: 60000,
+ h: 3600000,
+ d: 86400000
+}
+
+export function convertValueFromScale (value, scale) {
+ return value * (SCALE_CONVERSIONS[scale] || 1)
+}
+
+export function convertValueToScale (value, scale) {
+ return value / (SCALE_CONVERSIONS[scale] || 1)
+}
+
export function convertValueToDuration (value) {
const milliseconds = Math.round(value % 1000)
const seconds = Math.floor(value / 1000 % 60)
@@ -51,4 +70,8 @@ export function convertDurationToValue (duration) {
return (((days * 24 + hours) * 60 + minutes) * 60 + seconds) * 1000 + milliseconds
}
+export const convertFromValue = (value, scale) => convertValueToDuration(convertValueFromScale(value, scale))
+
+export const convertToValue = (duration, scale) => convertValueToScale(convertDurationToValue(duration), scale)
+
export default TimeDurationInput
diff --git a/src/components/TimeDurationInput/index.test.js b/src/components/TimeDurationInput/index.test.js
index b4b4032..5adac93 100644
--- a/src/components/TimeDurationInput/index.test.js
+++ b/src/components/TimeDurationInput/index.test.js
@@ -207,4 +207,106 @@ describe('TimeDurationInput', () => {
})
})
})
+
+ describe('scaled values', () => {
+ describe('when value = 250 and scale = "ms"', () => {
+ it('displays "250ms" in the input element', () => {
+ const { getByTestId } = render()
+
+ expect(getByTestId('duration-input')).toHaveValue('250ms')
+ })
+ })
+
+ describe('when value = 250 and scale = "s"', () => {
+ it('displays "4m 10s" in the input element', () => {
+ const { getByTestId } = render()
+
+ expect(getByTestId('duration-input')).toHaveValue('4m 10s')
+ })
+ })
+
+ describe('when value = 250 and scale = "m"', () => {
+ it('displays "4h 10m" in the input element', () => {
+ const { getByTestId } = render()
+
+ expect(getByTestId('duration-input')).toHaveValue('4h 10m')
+ })
+ })
+
+ describe('when value = 250 and scale = "h"', () => {
+ it('displays "10d 10h" in the input element', () => {
+ const { getByTestId } = render()
+
+ expect(getByTestId('duration-input')).toHaveValue('10d 10h')
+ })
+ })
+
+ describe('when value = 250 and scale = "d"', () => {
+ it('displays "250d" in the input element', () => {
+ const { getByTestId } = render()
+
+ expect(getByTestId('duration-input')).toHaveValue('250d')
+ })
+ })
+
+ describe('when scale = "ms" and input is changed to "1h 45m"', () => {
+ it('calls onChange with value 6300000', () => {
+ const onChange = jest.fn()
+ const { getByTestId } = render()
+
+ fireEvent.change(getByTestId('duration-input'), { target: { value: '1h 45m' } })
+
+ expect(onChange.mock.calls.length).toBe(1)
+ expect(onChange.mock.calls[0][0]).toBe(6300000)
+ })
+ })
+
+ describe('when scale = "s" and input is changed to "1h 45m"', () => {
+ it('calls onChange with value 6300', () => {
+ const onChange = jest.fn()
+ const { getByTestId } = render()
+
+ fireEvent.change(getByTestId('duration-input'), { target: { value: '1h 45m' } })
+
+ expect(onChange.mock.calls.length).toBe(1)
+ expect(onChange.mock.calls[0][0]).toBe(6300)
+ })
+ })
+
+ describe('when scale = "m" and input is changed to "1h 45m"', () => {
+ it('calls onChange with value 105', () => {
+ const onChange = jest.fn()
+ const { getByTestId } = render()
+
+ fireEvent.change(getByTestId('duration-input'), { target: { value: '1h 45m' } })
+
+ expect(onChange.mock.calls.length).toBe(1)
+ expect(onChange.mock.calls[0][0]).toBe(105)
+ })
+ })
+
+ describe('when scale = "h" and input is changed to "1h 45m"', () => {
+ it('calls onChange with value 1.75', () => {
+ const onChange = jest.fn()
+ const { getByTestId } = render()
+
+ fireEvent.change(getByTestId('duration-input'), { target: { value: '1h 45m' } })
+
+ expect(onChange.mock.calls.length).toBe(1)
+ expect(onChange.mock.calls[0][0]).toBeCloseTo(1.75)
+ })
+ })
+
+ describe('when scale = "d" and input is changed to "1h 45m"', () => {
+ it('calls onChange with value 0.0729167 (approx.)', () => {
+ const onChange = jest.fn()
+ const { getByTestId } = render()
+
+ fireEvent.change(getByTestId('duration-input'), { target: { value: '1h 45m' } })
+
+ expect(onChange.mock.calls.length).toBe(1)
+ expect(onChange.mock.calls[0][0]).toBeCloseTo(0.0729167)
+ })
+ })
+ })
})
diff --git a/src/components/TimeDurationInput/scale.test.js b/src/components/TimeDurationInput/scale.test.js
new file mode 100644
index 0000000..33ed8c7
--- /dev/null
+++ b/src/components/TimeDurationInput/scale.test.js
@@ -0,0 +1,43 @@
+import { convertValueFromScale, convertValueToScale } from './index'
+
+const SCENARIOS = [
+ { value: 0, scales: { d: 0, h: 0, m: 0, s: 0, ms: 0 } }
+]
+
+describe('convertValueFromScale(value, scale)', () => {
+ describe('when scale is invalid', () => {
+ it('returns the passed value', () => {
+ const value = Math.random()
+ expect(convertValueFromScale(value, 'invalid')).toBeCloseTo(value)
+ })
+ })
+
+ SCENARIOS.forEach(({ value, scales }) => {
+ Object.keys(scales).forEach(scale => {
+ describe(`when value = ${scales[scale]} and scale = "${scale}"`, () => {
+ it(`returns ${value}`, () => {
+ expect(convertValueFromScale(scales[scale], scale)).toBeCloseTo(value)
+ })
+ })
+ })
+ })
+})
+
+describe('convertValueToScale(value, scale)', () => {
+ describe('when scale is invalid', () => {
+ it('returns the passed value', () => {
+ const value = Math.random()
+ expect(convertValueToScale(value, 'invalid')).toBeCloseTo(value)
+ })
+ })
+
+ SCENARIOS.forEach(({ value, scales }) => {
+ Object.keys(scales).forEach(scale => {
+ describe(`when value = ${value} and scale = "${scale}"`, () => {
+ it(`returns ${scales[scale]}`, () => {
+ expect(convertValueToScale(value, scale)).toBeCloseTo(scales[scale])
+ })
+ })
+ })
+ })
+})