Skip to content

Commit

Permalink
feat(NumberInputE): Enable stepping of floats
Browse files Browse the repository at this point in the history
  • Loading branch information
m7kvqbe1 committed Dec 21, 2021
1 parent 032b165 commit faea396
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InlineButton } from '../InlineButtons/InlineButton'
import { NUMBER_INPUT_BUTTON_TYPE } from './constants'
import { StyledDivider } from './partials/StyledDivider'
import { StyledInlineButtons } from '../InlineButtons/partials/StyledInlineButtons'
import { countDecimals } from '../../helpers'

export interface ButtonsProps {
isDisabled: boolean
Expand All @@ -15,11 +16,11 @@ export interface ButtonsProps {
name: string
onClick: (
event: React.MouseEvent<HTMLButtonElement>,
newValue: number
newValue: string
) => void
size: ComponentSizeType
step?: number
value: number
value: string
}

const iconLookup = {
Expand All @@ -34,12 +35,15 @@ export const Buttons: React.FC<ButtonsProps> = ({
step,
value,
}) => {
function onButtonClick(getNewValue: () => number) {
const digits = countDecimals(step)

function onButtonClick(getNewValue: () => string) {
return (event: React.MouseEvent<HTMLButtonElement>) => {
const target = event.currentTarget
target.blur()

const newValue = getNewValue()

onClick(event, newValue)
}
}
Expand All @@ -52,7 +56,9 @@ export const Buttons: React.FC<ButtonsProps> = ({
)} the input value`}
data-testid={`number-input-${NUMBER_INPUT_BUTTON_TYPE.DECREASE}`}
isDisabled={isDisabled}
onClick={onButtonClick(() => (value || 0) - step)}
onClick={onButtonClick(() =>
((parseFloat(value) || 0) - step).toFixed(digits)
)}
size={size}
>
{iconLookup[NUMBER_INPUT_BUTTON_TYPE.DECREASE]}
Expand All @@ -64,7 +70,9 @@ export const Buttons: React.FC<ButtonsProps> = ({
)} the input value`}
data-testid={`number-input-${NUMBER_INPUT_BUTTON_TYPE.INCREASE}`}
isDisabled={isDisabled}
onClick={onButtonClick(() => (value || 0) + step)}
onClick={onButtonClick(() =>
((parseFloat(value) || 0) + step).toFixed(digits)
)}
size={size}
>
{iconLookup[NUMBER_INPUT_BUTTON_TYPE.INCREASE]}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { isFinite, isNil } from 'lodash'
import { isNil } from 'lodash'

import { ComponentSizeType } from '../Forms'
import { InputValidationProps } from '../../common/InputValidationProps'
Expand All @@ -18,15 +18,14 @@ export interface InputProps extends InputValidationProps {
onFocus: (event: React.FormEvent<HTMLInputElement>) => void
placeholder?: string
size: ComponentSizeType
value?: number
value?: string
}

export const Input: React.FC<InputProps> = ({
hasFocus,
isDisabled,
id,
label,
placeholder = '',
size,
value,
...rest
Expand All @@ -52,7 +51,7 @@ export const Input: React.FC<InputProps> = ({
data-testid="number-input-input"
disabled={isDisabled}
id={id}
value={isFinite(value) ? value : ''}
value={value || ''}
{...rest}
/>
</StyledInputWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const Template: Story<NumberInputEProps> = (args) => <NumberInputE {...args} />

export const Default = Template.bind({})

export const SteppedFloats = Template.bind({})
SteppedFloats.args = {
step: 0.25,
}

export const Small = Template.bind({})
Small.args = {
size: COMPONENT_SIZE.SMALL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ describe('NumberInputE', () => {

userEvent.type(input, '1')
userEvent.type(input, 'a')
userEvent.type(input, '.')
userEvent.type(input, '2')
})

Expand All @@ -211,16 +210,12 @@ describe('NumberInputE', () => {
expect(onChangeSpy.mock.calls[1][1]).toEqual(1)
})

it('calls the `onChange` callback yet again with `1`', () => {
expect(onChangeSpy.mock.calls[2][1]).toEqual(1)
})

it('calls the `onChange` callback again with `12`', () => {
expect(onChangeSpy.mock.calls[3][1]).toEqual(12)
expect(onChangeSpy.mock.calls[2][1]).toEqual(12)
})

assertInputValue('12')
assertOnChangeCall(12, 4)
assertOnChangeCall(12, 3)
})

describe('and the user types a value', () => {
Expand Down Expand Up @@ -410,11 +405,15 @@ describe('NumberInputE', () => {
})
})

describe('when the step is specified', () => {
describe.each([
['3', '0'],
['0.1', '0.0'],
['0.25', '0.00'],
])('when a step of %s is specified', (step, zero) => {
beforeEach(() => {
const props = {
...defaultProps,
step: 3,
step: Number(step),
}

onChangeSpy = jest.spyOn(props, 'onChange')
Expand All @@ -428,15 +427,15 @@ describe('NumberInputE', () => {
increase.click()
})

assertInputValue('3')
assertInputValue(step)

describe('and the decrease button is clicked', () => {
beforeEach(() => {
const decrease = wrapper.getByTestId('number-input-decrease')
decrease.click()
})

assertInputValue('0')
assertInputValue(zero)
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ export type NumberInputEProps =
| NumberInputWithPrefixProps
| NumberInputWithSuffixProps

function formatValue(displayValue: number, prefix: string, suffix: string) {
function formatValue(
displayValue: string,
prefix: string,
suffix: string
): string {
if (isNil(displayValue)) {
return 'Not set'
}
Expand All @@ -132,15 +136,15 @@ function formatValue(displayValue: number, prefix: string, suffix: string) {
return displayValue
}

function getNewValue(event: React.FormEvent<HTMLInputElement>): number {
function getNewValue(event: React.FormEvent<HTMLInputElement>): string {
const { value } = event.currentTarget
const sanitizedValue = value.replace(/\D/g, '')
const sanitizedValue = value.replace(/[^.0-9]/g, '')

if (sanitizedValue === '') {
return null
}

return parseInt(sanitizedValue, 10)
return sanitizedValue
}

function isWithinRange(max: number, min: number, newValue: number) {
Expand Down Expand Up @@ -172,13 +176,17 @@ export const NumberInputE: React.FC<NumberInputEProps> = ({
value,
...rest
}) => {
const { committedValue, setCommittedValue } = useValue(value)
const { committedValue, setCommittedValue } = useValue(
value ? String(value) : null
)
const { hasFocus, onLocalFocus, onLocalBlur } = useFocus(onBlur)

function canCommit(newValue: number) {
function canCommit(newValue: string) {
const parsedValue = parseFloat(newValue)

return (
(isFinite(newValue) && isWithinRange(max, min, newValue)) ||
newValue === null
(isFinite(parsedValue) && isWithinRange(max, min, parsedValue)) ||
parsedValue === null
)
}

Expand All @@ -193,7 +201,7 @@ export const NumberInputE: React.FC<NumberInputEProps> = ({
role="spinbutton"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={committedValue || 0}
aria-valuenow={Number(committedValue) || 0}
aria-valuetext={String(formatValue(committedValue, prefix, suffix))}
>
<StyledOuterWrapper
Expand Down Expand Up @@ -227,15 +235,15 @@ export const NumberInputE: React.FC<NumberInputEProps> = ({
onChange={(event) => {
const newValue = getNewValue(event)

if (canCommit(newValue)) {
if (!newValue || canCommit(newValue)) {
setCommittedValue(newValue)
onChange(event, newValue)
onChange(event, isNil(newValue) ? null : Number(newValue))
}
}}
onBlur={(event) => {
const newValue = getNewValue(event)

if (canCommit(newValue)) {
if (!newValue || canCommit(newValue)) {
setCommittedValue(newValue)
onLocalBlur(event)
}
Expand Down Expand Up @@ -264,7 +272,7 @@ export const NumberInputE: React.FC<NumberInputEProps> = ({
onClick={(e, newValue) => {
if (canCommit(newValue)) {
setCommittedValue(newValue)
onChange(e, newValue)
onChange(e, Number(newValue))
}
}}
size={size}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'

export function useValue(value: number) {
export function useValue(value: string) {
const [committedValue, setCommittedValue] = useState(value)

useEffect(() => {
Expand Down
9 changes: 9 additions & 0 deletions packages/react-component-library/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ function sleep(ms: number): Promise<undefined> {
return new Promise((resolve) => setTimeout(resolve, ms))
}

function countDecimals(value: number): number {
if (Math.floor(value) === value) {
return 0
}

return value.toString().split('.')[1].length || 0
}

export {
getInitials,
getId,
Expand All @@ -72,4 +80,5 @@ export {
warnIfOverwriting,
withKey,
sleep,
countDecimals,
}

0 comments on commit faea396

Please sign in to comment.