From 3419cd79efae94d53e11dcec27654764259c6d08 Mon Sep 17 00:00:00 2001 From: Stefan Legg Date: Tue, 28 Mar 2023 15:52:34 +0000 Subject: [PATCH 1/4] [TextField] add onStepperChange optional prop --- .changeset/beige-eggs-join.md | 5 ++ .../src/components/TextField/TextField.tsx | 21 +++++- .../TextField/tests/TextField.test.tsx | 66 +++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 .changeset/beige-eggs-join.md diff --git a/.changeset/beige-eggs-join.md b/.changeset/beige-eggs-join.md new file mode 100644 index 00000000000..823c8bceb3e --- /dev/null +++ b/.changeset/beige-eggs-join.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +Add onStepperChange as optional prop for TextField diff --git a/polaris-react/src/components/TextField/TextField.tsx b/polaris-react/src/components/TextField/TextField.tsx index cc5371f90ba..b114f2a1d93 100644 --- a/polaris-react/src/components/TextField/TextField.tsx +++ b/polaris-react/src/components/TextField/TextField.tsx @@ -162,6 +162,8 @@ interface NonMutuallyExclusiveProps { onClearButtonClick?(id: string): void; /** Callback fired when value is changed */ onChange?(value: string, id: string): void; + /** When provided, callback fired instead of onChange when value is changed via the number step control */ + onStepperChange?(value: string, id: string): void; /** Callback fired when input is focused */ onFocus?: (event?: React.FocusEvent) => void; /** Callback fired when input is blurred */ @@ -229,6 +231,7 @@ export function TextField({ suggestion, onClearButtonClick, onChange, + onStepperChange, onFocus, onBlur, borderless, @@ -354,7 +357,7 @@ export function TextField({ const handleNumberChange = useCallback( (steps: number) => { - if (onChange == null) { + if (onChange == null && onStepperChange == null) { return; } // Returns the length of decimal places in a number @@ -374,9 +377,21 @@ export function TextField({ Math.max(numericValue + steps * normalizedStep, Number(normalizedMin)), ); - onChange(String(newValue.toFixed(decimalPlaces)), id); + if (onStepperChange != null) { + onStepperChange(String(newValue.toFixed(decimalPlaces)), id); + } else if (onChange != null) { + onChange(String(newValue.toFixed(decimalPlaces)), id); + } }, - [id, normalizedMax, normalizedMin, onChange, normalizedStep, value], + [ + id, + normalizedMax, + normalizedMin, + onChange, + onStepperChange, + normalizedStep, + value, + ], ); const handleButtonRelease = useCallback(() => { diff --git a/polaris-react/src/components/TextField/tests/TextField.test.tsx b/polaris-react/src/components/TextField/tests/TextField.test.tsx index c4d9fecde31..5493d51e32c 100644 --- a/polaris-react/src/components/TextField/tests/TextField.test.tsx +++ b/polaris-react/src/components/TextField/tests/TextField.test.tsx @@ -1073,6 +1073,72 @@ describe('', () => { expect(spy).not.toHaveBeenCalled(); }); + describe('onStepperChange()', () => { + it('is called with the new value when incrementing by step', () => { + const spy = jest.fn(); + const element = mountWithApp( + , + ); + + element.findAll('div', {role: 'button'})[0].trigger('onClick'); + expect(spy).toHaveBeenCalledWith('6', 'MyTextField'); + }); + + it('is called with the new value instead of onChange when incrementing by step', () => { + const onStepperSpy = jest.fn(); + const onChangeSpy = jest.fn(); + const element = mountWithApp( + , + ); + + element.findAll('div', {role: 'button'})[0].trigger('onClick'); + expect(onStepperSpy).toHaveBeenCalledWith('6', 'MyTextField'); + expect(onChangeSpy).not.toHaveBeenCalled(); + }); + + it('is not called when new values are typed', () => { + const onStepperSpy = jest.fn(); + const onChangeSpy = jest.fn(); + const element = mountWithApp( + , + ); + + element.find('input')!.trigger('onChange', { + currentTarget: { + value: '6', + }, + }); + expect(onStepperSpy).not.toHaveBeenCalled(); + expect(onChangeSpy).toHaveBeenCalledWith('6', 'MyTextField'); + }); + }); + describe('document events', () => { type EventCallback = (mockEventData?: {[key: string]: any}) => void; From e77013b443d8d8d4c3f23817fa189c9d2533cad8 Mon Sep 17 00:00:00 2001 From: Stefan Legg Date: Wed, 12 Apr 2023 20:20:04 +0000 Subject: [PATCH 2/4] Support Spinbutton pattern keyboard interactions --- .changeset/beige-eggs-join.md | 4 +- .../src/components/TextField/TextField.tsx | 56 +++++-- .../TextField/tests/TextField.test.tsx | 137 +++++++++++++++++- 3 files changed, 183 insertions(+), 14 deletions(-) diff --git a/.changeset/beige-eggs-join.md b/.changeset/beige-eggs-join.md index 823c8bceb3e..66486668943 100644 --- a/.changeset/beige-eggs-join.md +++ b/.changeset/beige-eggs-join.md @@ -2,4 +2,6 @@ '@shopify/polaris': minor --- -Add onStepperChange as optional prop for TextField +Add onSpinButtonClick as optional prop for TextField +Add largeStep as option prop for TextField +Add Spinner keypress interactions for Home, End, Page Up, Page Down diff --git a/polaris-react/src/components/TextField/TextField.tsx b/polaris-react/src/components/TextField/TextField.tsx index b114f2a1d93..0d885e87262 100644 --- a/polaris-react/src/components/TextField/TextField.tsx +++ b/polaris-react/src/components/TextField/TextField.tsx @@ -122,6 +122,8 @@ interface NonMutuallyExclusiveProps { role?: string; /** Limit increment value for numeric and date-time inputs */ step?: number; + /** Increment value for numeric and date-time inputs when using Page Up or Page Down */ + largeStep?: number; /** Enable automatic completion by the browser. Set to "off" when you do not want the browser to fill in info */ autoComplete: string; /** Mimics the behavior of the native HTML attribute, limiting the maximum value */ @@ -163,7 +165,7 @@ interface NonMutuallyExclusiveProps { /** Callback fired when value is changed */ onChange?(value: string, id: string): void; /** When provided, callback fired instead of onChange when value is changed via the number step control */ - onStepperChange?(value: string, id: string): void; + onSpinButtonClick?(value: string, id: string): void; /** Callback fired when input is focused */ onFocus?: (event?: React.FocusEvent) => void; /** Callback fired when input is blurred */ @@ -209,6 +211,7 @@ export function TextField({ id: idProp, role, step, + largeStep, autoComplete, max, maxLength, @@ -231,7 +234,7 @@ export function TextField({ suggestion, onClearButtonClick, onChange, - onStepperChange, + onSpinButtonClick, onFocus, onBlur, borderless, @@ -356,8 +359,8 @@ export function TextField({ ) : null; const handleNumberChange = useCallback( - (steps: number) => { - if (onChange == null && onStepperChange == null) { + (steps: number, stepAmount = normalizedStep) => { + if (onChange == null && onSpinButtonClick == null) { return; } // Returns the length of decimal places in a number @@ -370,15 +373,15 @@ 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(normalizedStep)); + const decimalPlaces = Math.max(dpl(numericValue), dpl(stepAmount)); const newValue = Math.min( Number(normalizedMax), - Math.max(numericValue + steps * normalizedStep, Number(normalizedMin)), + Math.max(numericValue + steps * stepAmount, Number(normalizedMin)), ); - if (onStepperChange != null) { - onStepperChange(String(newValue.toFixed(decimalPlaces)), id); + if (onSpinButtonClick != null) { + onSpinButtonClick(String(newValue.toFixed(decimalPlaces)), id); } else if (onChange != null) { onChange(String(newValue.toFixed(decimalPlaces)), id); } @@ -388,7 +391,7 @@ export function TextField({ normalizedMax, normalizedMin, onChange, - onStepperChange, + onSpinButtonClick, normalizedStep, value, ], @@ -546,6 +549,7 @@ export function TextField({ onBlur: handleOnBlur, onClick: handleClickChild, onKeyPress: handleKeyPress, + onKeyDown: handleKeyDown, onChange: !suggestion ? handleChange : undefined, onInput: suggestion ? handleChange : undefined, }); @@ -660,6 +664,40 @@ export function TextField({ event.preventDefault(); } + function handleKeyDown(event: React.KeyboardEvent) { + if (type !== 'number') { + return; + } + + const {key, which} = event; + if ((which === Key.Home || key === 'Home') && min !== undefined) { + if (onSpinButtonClick != null) { + onSpinButtonClick(String(min), id); + } else if (onChange != null) { + onChange(String(min), id); + } + } + + if ((which === Key.End || key === 'End') && max !== undefined) { + if (onSpinButtonClick != null) { + onSpinButtonClick(String(max), id); + } else if (onChange != null) { + onChange(String(max), id); + } + } + + if ((which === Key.PageUp || key === 'PageUp') && largeStep !== undefined) { + handleNumberChange(1, largeStep); + } + + if ( + (which === Key.PageDown || key === 'PageDown') && + largeStep !== undefined + ) { + handleNumberChange(-1, largeStep); + } + } + function handleOnBlur(event: React.FocusEvent) { setFocus(false); diff --git a/polaris-react/src/components/TextField/tests/TextField.test.tsx b/polaris-react/src/components/TextField/tests/TextField.test.tsx index 5493d51e32c..1ebaf1478fb 100644 --- a/polaris-react/src/components/TextField/tests/TextField.test.tsx +++ b/polaris-react/src/components/TextField/tests/TextField.test.tsx @@ -9,6 +9,7 @@ import {Tag} from '../../Tag'; import {Resizer, Spinner} from '../components'; import {TextField} from '../TextField'; import styles from '../TextField.scss'; +import {Key} from '../../../types'; describe('', () => { it('allows specific props to pass through properties on the input', () => { @@ -1073,7 +1074,7 @@ describe('', () => { expect(spy).not.toHaveBeenCalled(); }); - describe('onStepperChange()', () => { + describe('onSpinButtonClick()', () => { it('is called with the new value when incrementing by step', () => { const spy = jest.fn(); const element = mountWithApp( @@ -1083,7 +1084,7 @@ describe('', () => { type="number" value="2" step={4} - onStepperChange={spy} + onSpinButtonClick={spy} autoComplete="off" />, ); @@ -1102,7 +1103,7 @@ describe('', () => { type="number" value="2" step={4} - onStepperChange={onStepperSpy} + onSpinButtonClick={onStepperSpy} onChange={onChangeSpy} autoComplete="off" />, @@ -1123,7 +1124,7 @@ describe('', () => { type="number" value="2" step={4} - onStepperChange={onStepperSpy} + onSpinButtonClick={onStepperSpy} onChange={onChangeSpy} autoComplete="off" />, @@ -1139,6 +1140,134 @@ describe('', () => { }); }); + describe('keydown events', () => { + it('decrements by largeStep when provided and Page Down is pressed', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + textField.find('input')!.trigger('onKeyDown', { + key: 'PageDown', + which: Key.PageDown, + }); + expect(spy).toHaveBeenCalledWith('6', 'MyTextField'); + }); + + it('increments by largeStep when provided and Page Up is pressed', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + textField.find('input')!.trigger('onKeyDown', { + key: 'PageUp', + which: Key.PageUp, + }); + expect(spy).toHaveBeenCalledWith('14', 'MyTextField'); + }); + + it('calls onChange(min) if Home is pressed and min was provided', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + textField.find('input')!.trigger('onKeyDown', { + key: 'Home', + which: Key.Home, + }); + expect(spy).toHaveBeenCalledWith('1', 'MyTextField'); + }); + + it('calls onChange(max) if End is pressed and max was provided', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + textField.find('input')!.trigger('onKeyDown', { + key: 'End', + which: Key.End, + }); + expect(spy).toHaveBeenCalledWith('100', 'MyTextField'); + }); + + it('calls onSpinButtonClick(min) if Home is pressed and min was provided', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + textField.find('input')!.trigger('onKeyDown', { + key: 'Home', + which: Key.Home, + }); + expect(spy).toHaveBeenCalledWith('1', 'MyTextField'); + }); + + it('calls onSpinButtonClick(max) if End is pressed and max was provided', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + textField.find('input')!.trigger('onKeyDown', { + key: 'End', + which: Key.End, + }); + expect(spy).toHaveBeenCalledWith('100', 'MyTextField'); + }); + }); + describe('document events', () => { type EventCallback = (mockEventData?: {[key: string]: any}) => void; From 6a8e112fbb4df305fd4fb2463a321ee67f72517c Mon Sep 17 00:00:00 2001 From: Stefan Legg Date: Thu, 13 Apr 2023 14:08:31 +0000 Subject: [PATCH 3/4] Rename onSpinButtonClick to onSpinnerChange --- .changeset/beige-eggs-join.md | 2 +- .../src/components/TextField/TextField.tsx | 20 +++++++++---------- .../TextField/tests/TextField.test.tsx | 16 +++++++-------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.changeset/beige-eggs-join.md b/.changeset/beige-eggs-join.md index 66486668943..7e2a7679845 100644 --- a/.changeset/beige-eggs-join.md +++ b/.changeset/beige-eggs-join.md @@ -2,6 +2,6 @@ '@shopify/polaris': minor --- -Add onSpinButtonClick as optional prop for TextField +Add onSpinnerChange as optional prop for TextField Add largeStep as option prop for TextField Add Spinner keypress interactions for Home, End, Page Up, Page Down diff --git a/polaris-react/src/components/TextField/TextField.tsx b/polaris-react/src/components/TextField/TextField.tsx index 0d885e87262..7559e9ce7ac 100644 --- a/polaris-react/src/components/TextField/TextField.tsx +++ b/polaris-react/src/components/TextField/TextField.tsx @@ -165,7 +165,7 @@ interface NonMutuallyExclusiveProps { /** Callback fired when value is changed */ onChange?(value: string, id: string): void; /** When provided, callback fired instead of onChange when value is changed via the number step control */ - onSpinButtonClick?(value: string, id: string): void; + onSpinnerChange?(value: string, id: string): void; /** Callback fired when input is focused */ onFocus?: (event?: React.FocusEvent) => void; /** Callback fired when input is blurred */ @@ -234,7 +234,7 @@ export function TextField({ suggestion, onClearButtonClick, onChange, - onSpinButtonClick, + onSpinnerChange, onFocus, onBlur, borderless, @@ -360,7 +360,7 @@ export function TextField({ const handleNumberChange = useCallback( (steps: number, stepAmount = normalizedStep) => { - if (onChange == null && onSpinButtonClick == null) { + if (onChange == null && onSpinnerChange == null) { return; } // Returns the length of decimal places in a number @@ -380,8 +380,8 @@ export function TextField({ Math.max(numericValue + steps * stepAmount, Number(normalizedMin)), ); - if (onSpinButtonClick != null) { - onSpinButtonClick(String(newValue.toFixed(decimalPlaces)), id); + if (onSpinnerChange != null) { + onSpinnerChange(String(newValue.toFixed(decimalPlaces)), id); } else if (onChange != null) { onChange(String(newValue.toFixed(decimalPlaces)), id); } @@ -391,7 +391,7 @@ export function TextField({ normalizedMax, normalizedMin, onChange, - onSpinButtonClick, + onSpinnerChange, normalizedStep, value, ], @@ -671,16 +671,16 @@ export function TextField({ const {key, which} = event; if ((which === Key.Home || key === 'Home') && min !== undefined) { - if (onSpinButtonClick != null) { - onSpinButtonClick(String(min), id); + if (onSpinnerChange != null) { + onSpinnerChange(String(min), id); } else if (onChange != null) { onChange(String(min), id); } } if ((which === Key.End || key === 'End') && max !== undefined) { - if (onSpinButtonClick != null) { - onSpinButtonClick(String(max), id); + if (onSpinnerChange != null) { + onSpinnerChange(String(max), id); } else if (onChange != null) { onChange(String(max), id); } diff --git a/polaris-react/src/components/TextField/tests/TextField.test.tsx b/polaris-react/src/components/TextField/tests/TextField.test.tsx index 1ebaf1478fb..10f8267fc7c 100644 --- a/polaris-react/src/components/TextField/tests/TextField.test.tsx +++ b/polaris-react/src/components/TextField/tests/TextField.test.tsx @@ -1074,7 +1074,7 @@ describe('', () => { expect(spy).not.toHaveBeenCalled(); }); - describe('onSpinButtonClick()', () => { + describe('onSpinnerChange()', () => { it('is called with the new value when incrementing by step', () => { const spy = jest.fn(); const element = mountWithApp( @@ -1084,7 +1084,7 @@ describe('', () => { type="number" value="2" step={4} - onSpinButtonClick={spy} + onSpinnerChange={spy} autoComplete="off" />, ); @@ -1103,7 +1103,7 @@ describe('', () => { type="number" value="2" step={4} - onSpinButtonClick={onStepperSpy} + onSpinnerChange={onStepperSpy} onChange={onChangeSpy} autoComplete="off" />, @@ -1124,7 +1124,7 @@ describe('', () => { type="number" value="2" step={4} - onSpinButtonClick={onStepperSpy} + onSpinnerChange={onStepperSpy} onChange={onChangeSpy} autoComplete="off" />, @@ -1225,7 +1225,7 @@ describe('', () => { expect(spy).toHaveBeenCalledWith('100', 'MyTextField'); }); - it('calls onSpinButtonClick(min) if Home is pressed and min was provided', () => { + it('calls onSpinnerChange(min) if Home is pressed and min was provided', () => { const spy = jest.fn(); const textField = mountWithApp( ', () => { value="10" min={1} step={1} - onSpinButtonClick={spy} + onSpinnerChange={spy} autoComplete="off" />, ); @@ -1246,7 +1246,7 @@ describe('', () => { expect(spy).toHaveBeenCalledWith('1', 'MyTextField'); }); - it('calls onSpinButtonClick(max) if End is pressed and max was provided', () => { + it('calls onSpinnerChange(max) if End is pressed and max was provided', () => { const spy = jest.fn(); const textField = mountWithApp( ', () => { value="10" max={100} step={1} - onSpinButtonClick={spy} + onSpinnerChange={spy} autoComplete="off" />, ); From bc58aecc4ca3533ac5c642001fffae266c2afe2f Mon Sep 17 00:00:00 2001 From: Stefan Legg Date: Thu, 13 Apr 2023 13:35:12 -0400 Subject: [PATCH 4/4] Update changeset descriptions Co-authored-by: Chloe Rice --- .changeset/beige-eggs-join.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/beige-eggs-join.md b/.changeset/beige-eggs-join.md index 7e2a7679845..e236319631e 100644 --- a/.changeset/beige-eggs-join.md +++ b/.changeset/beige-eggs-join.md @@ -2,6 +2,6 @@ '@shopify/polaris': minor --- -Add onSpinnerChange as optional prop for TextField -Add largeStep as option prop for TextField -Add Spinner keypress interactions for Home, End, Page Up, Page Down +- Added an optional `onSpinnerChange` prop to`TextField` +- Added an optional `largeStep` prop to `TextField` +- Added `TextField` `Spinner` keypress interactions for Home, End, Page Up, Page Down