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]) + }) + }) + }) + }) +})