From 8d3124efe903a1956b8b828e51f799bdff73232d Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Fri, 21 Apr 2023 18:11:12 +0000 Subject: [PATCH] [TextField] Add integer number type --- .changeset/heavy-bats-jam.md | 5 + .../TextField/TextField.stories.tsx | 20 +- .../src/components/TextField/TextField.tsx | 23 +- .../TextField/tests/TextField.test.tsx | 347 +++++++++++++++++- 4 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 .changeset/heavy-bats-jam.md diff --git a/.changeset/heavy-bats-jam.md b/.changeset/heavy-bats-jam.md new file mode 100644 index 00000000000..26274af4415 --- /dev/null +++ b/.changeset/heavy-bats-jam.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +Added `integer` option for the `type` prop of TextField diff --git a/polaris-react/src/components/TextField/TextField.stories.tsx b/polaris-react/src/components/TextField/TextField.stories.tsx index 40d40a8f41a..a389572d991 100644 --- a/polaris-react/src/components/TextField/TextField.stories.tsx +++ b/polaris-react/src/components/TextField/TextField.stories.tsx @@ -33,8 +33,8 @@ export function Default() { } export function Number() { - const [value, setValue] = useState('1'); - const [value1, setValue1] = useState('1'); + const [value, setValue] = useState('1.0'); + const [value1, setValue1] = useState('1.0'); const handleChange = useCallback((newValue) => setValue(newValue), []); const handleChange1 = useCallback((newValue) => setValue1(newValue), []); @@ -59,6 +59,22 @@ export function Number() { ); } +export function Integer() { + const [value, setValue] = useState('1'); + + const handleChange = useCallback((newValue) => setValue(newValue), []); + + return ( + + ); +} + export function Email() { const [value, setValue] = useState('bernadette.lapresse@jadedpixel.com'); diff --git a/polaris-react/src/components/TextField/TextField.tsx b/polaris-react/src/components/TextField/TextField.tsx index 617932fdf91..62de5517819 100644 --- a/polaris-react/src/components/TextField/TextField.tsx +++ b/polaris-react/src/components/TextField/TextField.tsx @@ -28,6 +28,7 @@ type Type = | 'text' | 'email' | 'number' + | 'integer' | 'password' | 'search' | 'tel' @@ -293,6 +294,7 @@ export function TextField({ ); const inputType = type === 'currency' ? 'text' : type; + const isNumericType = type === 'number' || type === 'integer'; const prefixMarkup = prefix ? (
@@ -373,7 +375,8 @@ export function TextField({ // Making sure the new value has the same length of decimal places as the // step / value has. - const decimalPlaces = Math.max(dpl(numericValue), dpl(stepAmount)); + const decimalPlaces = + type === 'integer' ? 0 : Math.max(dpl(numericValue), dpl(stepAmount)); const newValue = Math.min( Number(normalizedMax), @@ -393,6 +396,7 @@ export function TextField({ onChange, onSpinnerChange, normalizedStep, + type, value, ], ); @@ -426,7 +430,7 @@ export function TextField({ ); const spinnerMarkup = - type === 'number' && step !== 0 && !disabled && !readOnly ? ( + isNumericType && step !== 0 && !disabled && !readOnly ? ( ', () => { expect(spy).toHaveBeenCalledWith('4', 'MyTextField'); }); - it('adds a decrement button that increases the value', () => { + it('adds a decrement button that decreases the value', () => { const spy = jest.fn(); const element = mountWithApp( ', () => { }); }); }); + + describe('integer', () => { + it('adds an increment button that increases the value', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + element! + .find('div', { + role: 'button', + })! + .trigger('onClick'); + expect(spy).toHaveBeenCalledWith('4', 'MyTextField'); + }); + + it('adds a decrement button that decreases the value', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + + element + .findAll('div', { + role: 'button', + })[1]! + .trigger('onClick'); + expect(spy).toHaveBeenCalledWith('2', 'MyTextField'); + }); + + it('does not call the onChange if the value is not a integer', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + + element! + .find('div', { + role: 'button', + })! + .trigger('onClick'); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('handles incrementing from no value', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + element + .findAll('div', { + role: 'button', + })[0]! + .trigger('onClick'); + expect(spy).toHaveBeenCalledWith('1', 'MyTextField'); + }); + + it('passes the step prop to the input', () => { + const element = mountWithApp( + , + ); + + expect(element).toContainReactComponent('input', { + step: 6, + }); + }); + + it('uses the step prop when incrementing', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + element! + .find('div', { + role: 'button', + })! + .trigger('onClick'); + expect(spy).toHaveBeenCalledWith('3', 'MyTextField'); + }); + + it('rounds to the nearest whole number when the step prop is a decimal', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + element! + .find('div', { + role: 'button', + })! + .trigger('onClick'); + expect(spy).toHaveBeenCalledWith('4', 'MyTextField'); + }); + + it('respects a min value', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + + element + .findAll('div', { + role: 'button', + })[1]! + .trigger('onClick'); + expect(spy).toHaveBeenLastCalledWith('2', 'MyTextField'); + + element + .findAll('div', { + role: 'button', + })[0]! + .trigger('onClick'); + expect(spy).toHaveBeenLastCalledWith('3', 'MyTextField'); + }); + + it('respects a max value', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + + element + .findAll('div', { + role: 'button', + })[0]! + .trigger('onClick'); + expect(spy).toHaveBeenLastCalledWith('2', 'MyTextField'); + + element + .findAll('div', { + role: 'button', + })[1]! + .trigger('onClick'); + expect(spy).toHaveBeenLastCalledWith('1', 'MyTextField'); + }); + + it('brings an invalid value up to the min', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + + element + .findAll('div', { + role: 'button', + })[0]! + .trigger('onClick'); + expect(spy).toHaveBeenLastCalledWith('2', 'MyTextField'); + + element + .findAll('div', { + role: 'button', + })[1]! + .trigger('onClick'); + expect(spy).toHaveBeenLastCalledWith('2', 'MyTextField'); + }); + + it('brings an invalid value down to the max', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + + element + .findAll('div', { + role: 'button', + })[0]! + .trigger('onClick'); + expect(spy).toHaveBeenLastCalledWith('2', 'MyTextField'); + + element + .findAll('div', { + role: 'button', + })[1]! + .trigger('onClick'); + expect(spy).toHaveBeenLastCalledWith('2', 'MyTextField'); + }); + + it('removes increment and decrement buttons when disabled', () => { + const element = mountWithApp( + , + ); + expect(element).not.toContainReactComponent('[role="button"]'); + }); + + it('removes increment and decrement buttons when readOnly', () => { + const element = mountWithApp( + , + ); + expect(element).not.toContainReactComponent(Spinner); + }); + + it('removes spinner buttons when type is integer and step is 0', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + expect(element).not.toContainReactComponent(Spinner); + }); + + it('decrements on mouse down', () => { + jest.useFakeTimers(); + const spy = jest.fn(); + const element = mountWithApp( + , + ); + element + .findAll('div', {role: 'button'})[1] + .trigger('onMouseDown', {button: 0}); + + jest.runOnlyPendingTimers(); + expect(spy).toHaveBeenCalledWith('2', 'MyTextField'); + }); + + it('stops decrementing on mouse up', () => { + jest.useFakeTimers(); + const spy = jest.fn(); + const element = mountWithApp( + , + ); + + const buttonDiv = element.findAll('div', {role: 'button'})[1]; + + buttonDiv.trigger('onMouseDown', {button: 0}); + buttonDiv.trigger('onMouseUp'); + + jest.runOnlyPendingTimers(); + expect(spy).not.toHaveBeenCalled(); + }); + }); }); describe('multiline', () => {