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', () => {