Skip to content

Commit

Permalink
chore(input): update input type=number (#3312)
Browse files Browse the repository at this point in the history
* feat(box): add style props for use in the number input component

* chore(date-picker, time-picker): add missing deps

* feat(utils): add useMergeRefs util

* feat(input): update number input and add number nudger buttons

* chore(codemods): changeset

* chore(input): add more number logic

* chore: fix utils changeset

* fix: uncontrolled number input decrement and increment

* test: add tests for the number input

* docs: number input docs

* chore: pr feedback

---------

Co-authored-by: Si Taggart <me@simontaggart.com>
  • Loading branch information
nkrantz and SiTaggart committed Jul 12, 2023
1 parent 61c310e commit 5b0d3f3
Show file tree
Hide file tree
Showing 20 changed files with 779 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-buttons-complain.md
@@ -0,0 +1,5 @@
---
'@twilio-paste/codemods': patch
---

[Codemods] add new export to utils package
6 changes: 6 additions & 0 deletions .changeset/mighty-cobras-do.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/utils': minor
'@twilio-paste/core': minor
---

[Utils] Add useMergeRefs util (previously removed because it was unused) for use in the input package.
7 changes: 7 additions & 0 deletions .changeset/new-snakes-draw.md
@@ -0,0 +1,7 @@
---
'@twilio-paste/date-picker': patch
'@twilio-paste/time-picker': patch
'@twilio-paste/core': patch
---

[Date Picker, Time Picker] add missing dependencies to package.json
6 changes: 6 additions & 0 deletions .changeset/serious-knives-breathe.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/box': minor
'@twilio-paste/core': minor
---

[Box] Add PseudoPropStyles **webkit_inner_spin_button and **webkit_outer_spin_button and CustomStyleProp -moz-appearance to Box Primitive Style Props for use in the Input package for hiding native components from all browsers.
6 changes: 6 additions & 0 deletions .changeset/slimy-donuts-swim.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/input': minor
'@twilio-paste/core': minor
---

[Input] Adjust type="number" Input to use native HTML element, add custom styling and functionality.
2 changes: 2 additions & 0 deletions .eslintrc.js
Expand Up @@ -75,6 +75,8 @@ module.exports = {
'__moz_focus_inner',
'__webkit_datetime_edit',
'__webkit_calendar_picker_indicator_hover',
'__webkit_inner_spin_button',
'__webkit_outer_spin_button',
// these are variant names we use as keys in style objects
'destructive_link',
'destructive_secondary',
Expand Down
12 changes: 12 additions & 0 deletions packages/paste-core/components/date-picker/package.json
Expand Up @@ -27,37 +27,49 @@
"date-fns": "2.21.3"
},
"peerDependencies": {
"@twilio-paste/anchor": "^11.0.0",
"@twilio-paste/animation-library": "^1.0.0",
"@twilio-paste/box": "^9.0.0",
"@twilio-paste/button": "^13.0.3",
"@twilio-paste/color-contrast-utils": "^4.0.0",
"@twilio-paste/customization": "^7.0.0",
"@twilio-paste/design-tokens": "^9.0.0",
"@twilio-paste/icons": "^11.2.1",
"@twilio-paste/input": "^8.0.0",
"@twilio-paste/input-box": "^9.0.0",
"@twilio-paste/spinner": "^13.0.0",
"@twilio-paste/stack": "^7.0.0",
"@twilio-paste/style-props": "^8.0.0",
"@twilio-paste/styling-library": "^2.0.0",
"@twilio-paste/theme": "^10.0.0",
"@twilio-paste/types": "^5.0.0",
"@twilio-paste/uid-library": "^1.0.0",
"@twilio-paste/utils": "^4.0.0",
"@types/react": "^16.8.6 || ^17.0.2 || ^18.0.27",
"@types/react-dom": "^16.8.6 || ^17.0.2 || ^18.0.10",
"prop-types": "^15.7.2",
"react": "^16.8.6 || ^17.0.2 || ^18.0.0",
"react-dom": "^16.8.6 || ^17.0.2 || ^18.0.0"
},
"devDependencies": {
"@twilio-paste/anchor": "^11.0.0",
"@twilio-paste/animation-library": "^1.0.0",
"@twilio-paste/box": "^9.0.0",
"@twilio-paste/button": "^13.0.3",
"@twilio-paste/color-contrast-utils": "^4.0.0",
"@twilio-paste/customization": "^7.0.0",
"@twilio-paste/design-tokens": "^9.0.2",
"@twilio-paste/icons": "^11.2.1",
"@twilio-paste/input": "^8.0.0",
"@twilio-paste/input-box": "^9.0.0",
"@twilio-paste/spinner": "^13.0.0",
"@twilio-paste/stack": "^7.0.0",
"@twilio-paste/style-props": "^8.0.0",
"@twilio-paste/styling-library": "^2.0.0",
"@twilio-paste/theme": "^10.0.0",
"@twilio-paste/types": "^5.0.0",
"@twilio-paste/uid-library": "^1.0.0",
"@twilio-paste/utils": "^4.0.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"prop-types": "^15.7.2",
Expand Down
92 changes: 87 additions & 5 deletions packages/paste-core/components/input/__tests__/input.test.tsx
@@ -1,12 +1,18 @@
import * as React from 'react';
import {render, screen} from '@testing-library/react';
import {fireEvent, render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {CustomizationProvider} from '@twilio-paste/customization';

import {Input} from '../src';
import type {InputTypes} from '../src';

const NOOP = (): void => {};

const NumberInput: React.FC = () => {
const [value, setValue] = React.useState('0');
return <Input type="number" value={value} min="-1" max="2" onChange={(e) => setValue(e.currentTarget.value)} />;
};

describe('Input inner input props', () => {
const initialProps = {
id: 'input',
Expand Down Expand Up @@ -101,6 +107,8 @@ describe('Input event handlers', () => {
expect(onFocusMock).toHaveBeenCalledTimes(1);
RenderedInput.blur();
expect(onBlurMock).toHaveBeenCalledTimes(1);
userEvent.type(RenderedInput, 'foo');
expect(onChangeMock).toHaveBeenCalledTimes(3);
});
});

Expand Down Expand Up @@ -177,6 +185,60 @@ describe('HTML attributes', () => {
});
});

describe('Number Input', () => {
it('increases when step up clicked', () => {
const {getByRole} = render(<Input type="number" defaultValue={1} />);
userEvent.click(getByRole('button', {name: 'step value up'}));
expect(getByRole('spinbutton').value).toBe('2');
});

it('decreases when step down clicked', () => {
const {getByRole} = render(<Input type="number" defaultValue={3} />);
userEvent.click(getByRole('button', {name: 'step value down'}));
expect(getByRole('spinbutton').value).toBe('2');
});
it('increases by step when step up clicked', () => {
const {getByRole} = render(<Input type="number" defaultValue={1} step={2} />);
userEvent.click(getByRole('button', {name: 'step value up'}));
expect(getByRole('spinbutton').value).toBe('3');
});

it('decreases by step when step down clicked', () => {
const {getByRole} = render(<Input type="number" defaultValue={3} step={2} />);
userEvent.click(getByRole('button', {name: 'step value down'}));
expect(getByRole('spinbutton').value).toBe('1');
});

it('does not decrement below min value', () => {
const {getByRole} = render(<Input type="number" defaultValue={1} min={1} />);
userEvent.click(getByRole('button', {name: 'step value down'}));
expect(getByRole('spinbutton').value).toBe('1');
});

it('does not increment above max value', () => {
const {getByRole} = render(<Input type="number" defaultValue={5} max={5} />);
userEvent.click(getByRole('button', {name: 'step value up'}));
expect(getByRole('spinbutton').value).toBe('5');
});

it('increase button hides when hit max', () => {
const {getByRole} = render(<NumberInput />);
const IncreaseButton = getByRole('button', {name: 'step value up'});
userEvent.click(IncreaseButton);
expect(getByRole('spinbutton').value).toBe('1');
userEvent.click(IncreaseButton);
expect(getByRole('spinbutton').value).toBe('2');
expect(IncreaseButton).not.toBeInTheDocument();
});
it('decrease button hides when hit min', () => {
const {getByRole} = render(<NumberInput />);
const DecreaseButton = getByRole('button', {name: 'step value down'});
userEvent.click(DecreaseButton);
expect(getByRole('spinbutton').value).toBe('-1');
expect(DecreaseButton).not.toBeInTheDocument();
});
});

describe('Customization', () => {
it('should add custom styles to Input', (): void => {
const {container} = render(
Expand Down Expand Up @@ -246,11 +308,17 @@ describe('Customization', () => {
},
},
},
INPUT_INCREMENT_BUTTON: {
backgroundColor: 'colorBackgroundNew',
},
INPUT_DECREMENT_BUTTON: {
backgroundColor: 'colorBackgroundError',
},
}}
>
<Input
id="input"
type="text"
type="number"
value="test"
onChange={NOOP}
variant="inverse"
Expand All @@ -260,13 +328,17 @@ describe('Customization', () => {
</CustomizationProvider>
);
const renderedInput = container.querySelector('[data-paste-element="INPUT"]');
const renderedInputElement = screen.getByRole('textbox');
const renderedInputElement = screen.getByRole('spinbutton');
const renderedInputPrefix = screen.getByText('test before');
const renderedInputSuffix = screen.getByText('test after');
const renderedIncrement = screen.getByRole('button', {name: 'step value up'});
const renderedDecrement = screen.getByRole('button', {name: 'step value down'});
expect(renderedInput).toHaveStyleRule('background-color', 'rgb(0, 20, 137)');
expect(renderedInputElement).toHaveStyleRule('background-color', 'rgb(0, 20, 137)');
expect(renderedInputPrefix).toHaveStyleRule('background-color', 'rgb(0, 20, 137)');
expect(renderedInputSuffix).toHaveStyleRule('background-color', 'rgb(0, 20, 137)');
expect(renderedIncrement).toHaveStyleRule('background-color', 'rgb(245, 240, 252)');
expect(renderedDecrement).toHaveStyleRule('background-color', 'rgb(214, 31, 31)');
});

it('should add custom styles to Input with a custom element data attribute', (): void => {
Expand All @@ -278,11 +350,17 @@ describe('Customization', () => {
FOO_ELEMENT: {backgroundColor: 'colorBackground'},
FOO_PREFIX: {backgroundColor: 'colorBackground'},
FOO_SUFFIX: {backgroundColor: 'colorBackground'},
FOO_INCREMENT_BUTTON: {
backgroundColor: 'colorBackgroundNew',
},
FOO_DECREMENT_BUTTON: {
backgroundColor: 'colorBackgroundError',
},
}}
>
<Input
id="input"
type="text"
type="number"
value="test"
onChange={NOOP}
element="FOO"
Expand All @@ -292,13 +370,17 @@ describe('Customization', () => {
</CustomizationProvider>
);
const renderedInput = container.querySelector('[data-paste-element="FOO"]');
const renderedInputElement = screen.getByRole('textbox');
const renderedInputElement = screen.getByRole('spinbutton');
const renderedInputPrefix = screen.getByText('test before');
const renderedInputSuffix = screen.getByText('test after');
const renderedIncrement = screen.getByRole('button', {name: 'step value up'});
const renderedDecrement = screen.getByRole('button', {name: 'step value down'});
expect(renderedInput).toHaveStyleRule('background-color', 'rgb(244, 244, 246)');
expect(renderedInputElement).toHaveStyleRule('background-color', 'rgb(244, 244, 246)');
expect(renderedInputPrefix).toHaveStyleRule('background-color', 'rgb(244, 244, 246)');
expect(renderedInputSuffix).toHaveStyleRule('background-color', 'rgb(244, 244, 246)');
expect(renderedIncrement).toHaveStyleRule('background-color', 'rgb(245, 240, 252)');
expect(renderedDecrement).toHaveStyleRule('background-color', 'rgb(214, 31, 31)');
});

it('should add custom styles to a Input variant with a custom element data attribute', (): void => {
Expand Down
12 changes: 12 additions & 0 deletions packages/paste-core/components/input/package.json
Expand Up @@ -24,35 +24,47 @@
"tsc": "tsc"
},
"peerDependencies": {
"@twilio-paste/anchor": "^11.0.0",
"@twilio-paste/animation-library": "^1.0.0",
"@twilio-paste/box": "^9.0.0",
"@twilio-paste/button": "^13.0.3",
"@twilio-paste/color-contrast-utils": "^4.0.0",
"@twilio-paste/customization": "^7.0.0",
"@twilio-paste/design-tokens": "^9.0.0",
"@twilio-paste/icons": "^11.2.1",
"@twilio-paste/input-box": "^9.0.0",
"@twilio-paste/spinner": "^13.0.0",
"@twilio-paste/stack": "^7.0.0",
"@twilio-paste/style-props": "^8.0.0",
"@twilio-paste/styling-library": "^2.0.0",
"@twilio-paste/theme": "^10.0.0",
"@twilio-paste/types": "^5.0.0",
"@twilio-paste/uid-library": "^1.0.0",
"@twilio-paste/utils": "^4.0.0",
"@types/react": "^16.8.6 || ^17.0.2 || ^18.0.27",
"@types/react-dom": "^16.8.6 || ^17.0.2 || ^18.0.10",
"prop-types": "^15.7.2",
"react": "^16.8.6 || ^17.0.2 || ^18.0.0",
"react-dom": "^16.8.6 || ^17.0.2 || ^18.0.0"
},
"devDependencies": {
"@twilio-paste/anchor": "^11.0.0",
"@twilio-paste/animation-library": "^1.0.0",
"@twilio-paste/box": "^9.0.0",
"@twilio-paste/button": "^13.0.3",
"@twilio-paste/color-contrast-utils": "^4.0.0",
"@twilio-paste/customization": "^7.0.0",
"@twilio-paste/design-tokens": "^9.2.0",
"@twilio-paste/icons": "^11.2.1",
"@twilio-paste/input-box": "^9.0.1",
"@twilio-paste/spinner": "^13.0.0",
"@twilio-paste/stack": "^7.0.0",
"@twilio-paste/style-props": "^8.0.0",
"@twilio-paste/styling-library": "^2.0.0",
"@twilio-paste/theme": "^10.0.0",
"@twilio-paste/types": "^5.0.0",
"@twilio-paste/uid-library": "^1.0.0",
"@twilio-paste/utils": "^4.0.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"prop-types": "^15.7.2",
Expand Down
36 changes: 36 additions & 0 deletions packages/paste-core/components/input/src/DecrementButton.tsx
@@ -0,0 +1,36 @@
import * as React from 'react';
import type {BoxProps} from '@twilio-paste/box';
import {Button} from '@twilio-paste/button';
import {ChevronDownIcon} from '@twilio-paste/icons/esm/ChevronDownIcon';

export interface DecrementButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
i18nStepDownLabel?: string;
element?: BoxProps['element'];
}

export const DecrementButton = React.forwardRef<HTMLButtonElement, DecrementButtonProps>(
({i18nStepDownLabel = 'step value down', element, ...props}, ref) => {
return (
<Button
{...props}
ref={ref}
element={`${element}_DECREMENT_BUTTON`}
variant="reset"
size="reset"
type="button"
// @ts-expect-error remove when Reset Button types extends BoxProps
borderRadius="borderRadius20"
backgroundColor="colorBackground"
marginRight="space30"
>
<ChevronDownIcon
decorative={false}
title={i18nStepDownLabel}
size="sizeIcon05"
element={`${element}_DECREMENT_ICON`}
/>
</Button>
);
}
);
DecrementButton.displayName = 'Decrement';
36 changes: 36 additions & 0 deletions packages/paste-core/components/input/src/IncrementButton.tsx
@@ -0,0 +1,36 @@
import * as React from 'react';
import type {BoxProps} from '@twilio-paste/box';
import {Button} from '@twilio-paste/button';
import {ChevronUpIcon} from '@twilio-paste/icons/esm/ChevronUpIcon';

export interface IncrementButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
i18nStepUpLabel?: string;
element?: BoxProps['element'];
}

export const IncrementButton = React.forwardRef<HTMLButtonElement, IncrementButtonProps>(
({i18nStepUpLabel = 'step value up', element, ...props}, ref) => {
return (
<Button
{...props}
ref={ref}
element={`${element}_INCREMENT_BUTTON`}
variant="reset"
size="reset"
type="button"
// @ts-expect-error remove when Reset Button types extends BoxProps
borderRadius="borderRadius20"
backgroundColor="colorBackground"
marginRight="space30"
>
<ChevronUpIcon
decorative={false}
title={i18nStepUpLabel}
size="sizeIcon05"
element={`${element}_INCREMENT_ICON`}
/>
</Button>
);
}
);
IncrementButton.displayName = 'Increment';

0 comments on commit 5b0d3f3

Please sign in to comment.