Skip to content

Commit 8fcc61f

Browse files
committed
Ability to show textfield spinner on hover and focus
1 parent 1eb0b06 commit 8fcc61f

File tree

5 files changed

+141
-2
lines changed

5 files changed

+141
-2
lines changed

.changeset/gold-deers-tan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris': minor
3+
---
4+
5+
Added `stepperShownOnInteraction` prop to `TextField` to support hiding the stepper arrows for inputs of type "number" by default and revealing them on hover and focus

polaris-react/src/components/TextField/TextField.stories.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,3 +587,20 @@ export function WithInlineSuggestion() {
587587
</div>
588588
);
589589
}
590+
591+
export function WithStepperShownOnIneraction() {
592+
const [value, setValue] = useState('1');
593+
594+
const handleChange = useCallback((newValue) => setValue(newValue), []);
595+
596+
return (
597+
<TextField
598+
label="Quantity"
599+
type="number"
600+
value={value}
601+
onChange={handleChange}
602+
autoComplete="off"
603+
stepperShownOnInteraction
604+
/>
605+
);
606+
}

polaris-react/src/components/TextField/TextField.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import React, {
77
} from 'react';
88
import {CircleCancelMinor} from '@shopify/polaris-icons';
99

10+
import {useToggle} from '../../utilities/use-toggle';
1011
import {classNames, variationName} from '../../utilities/css';
1112
import {useI18n} from '../../utilities/i18n';
1213
import {useUniqueId} from '../../utilities/unique-id';
@@ -118,6 +119,8 @@ interface NonMutuallyExclusiveProps {
118119
role?: string;
119120
/** Limit increment value for numeric and date-time inputs */
120121
step?: number;
122+
/** Hide stepper by default, reveal on hover and focus */
123+
stepperShownOnInteraction?: boolean;
121124
/** Enable automatic completion by the browser. Set to "off" when you do not want the browser to fill in info */
122125
autoComplete: string;
123126
/** Mimics the behavior of the native HTML attribute, limiting the maximum value */
@@ -201,6 +204,7 @@ export function TextField({
201204
id: idProp,
202205
role,
203206
step,
207+
stepperShownOnInteraction,
204208
autoComplete,
205209
max,
206210
maxLength,
@@ -230,6 +234,11 @@ export function TextField({
230234
const [height, setHeight] = useState<number | null>(null);
231235
const [focus, setFocus] = useState(Boolean(focused));
232236
const isAfterInitial = useIsAfterInitialMount();
237+
const {
238+
value: mouseEnter,
239+
setTrue: handleMouseEnter,
240+
setFalse: handleMouseLeave,
241+
} = useToggle(false);
233242

234243
const id = useUniqueId('TextField', idProp);
235244

@@ -267,6 +276,7 @@ export function TextField({
267276
const normalizedStep = step != null ? step : 1;
268277
const normalizedMax = max != null ? max : Infinity;
269278
const normalizedMin = min != null ? min : -Infinity;
279+
const spinnerHidden = stepperShownOnInteraction && !focus && !mouseEnter;
270280

271281
const className = classNames(
272282
styles.TextField,
@@ -400,7 +410,11 @@ export function TextField({
400410
);
401411

402412
const spinnerMarkup =
403-
type === 'number' && step !== 0 && !disabled && !readOnly ? (
413+
type === 'number' &&
414+
step !== 0 &&
415+
!disabled &&
416+
!readOnly &&
417+
!spinnerHidden ? (
404418
<Spinner
405419
onClick={handleClickChild}
406420
onChange={handleNumberChange}
@@ -554,7 +568,16 @@ export function TextField({
554568
requiredIndicator={requiredIndicator}
555569
>
556570
<Connected left={connectedLeft} right={connectedRight}>
557-
<div className={className} onClick={handleClick}>
571+
<div
572+
className={className}
573+
onClick={handleClick}
574+
onMouseEnter={
575+
stepperShownOnInteraction ? handleMouseEnter : undefined
576+
}
577+
onMouseLeave={
578+
stepperShownOnInteraction ? handleMouseLeave : undefined
579+
}
580+
>
558581
{prefixMarkup}
559582
{inputMarkup}
560583
{suffixMarkup}

polaris-react/src/components/TextField/tests/TextField.test.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,99 @@ describe('<TextField />', () => {
11161116
expect(spy).not.toHaveBeenCalled();
11171117
});
11181118
});
1119+
1120+
describe('stepperShownOnInteraction', () => {
1121+
it('does not render spinner by default when input is not interacted with', () => {
1122+
const element = mountWithApp(
1123+
<TextField
1124+
id="MyTextField"
1125+
label="TextField"
1126+
type="number"
1127+
value="3"
1128+
autoComplete="off"
1129+
stepperShownOnInteraction
1130+
/>,
1131+
);
1132+
1133+
expect(element).not.toContainReactComponent(Spinner);
1134+
});
1135+
1136+
it('renders spinner when input is focused', () => {
1137+
const element = mountWithApp(
1138+
<TextField
1139+
id="MyTextField"
1140+
label="TextField"
1141+
type="number"
1142+
value="3"
1143+
autoComplete="off"
1144+
stepperShownOnInteraction
1145+
/>,
1146+
);
1147+
1148+
element!.find('input')!.trigger('onFocus');
1149+
1150+
expect(element).toContainReactComponent(Spinner);
1151+
});
1152+
1153+
it('does not render spinner when input is blurred', () => {
1154+
const element = mountWithApp(
1155+
<TextField
1156+
id="MyTextField"
1157+
label="TextField"
1158+
type="number"
1159+
value="3"
1160+
autoComplete="off"
1161+
stepperShownOnInteraction
1162+
/>,
1163+
);
1164+
1165+
element!.find('input')!.trigger('onFocus');
1166+
element!.find('input')!.trigger('onBlur');
1167+
1168+
expect(element).not.toContainReactComponent(Spinner);
1169+
});
1170+
1171+
it('renders spinner when `mouseenter` event is triggered', () => {
1172+
const element = mountWithApp(
1173+
<TextField
1174+
id="MyTextField"
1175+
label="TextField"
1176+
type="number"
1177+
value="3"
1178+
autoComplete="off"
1179+
stepperShownOnInteraction
1180+
/>,
1181+
);
1182+
1183+
element
1184+
.find(Connected)!
1185+
.triggerKeypath('children.props.onMouseEnter', {});
1186+
1187+
expect(element).toContainReactComponent(Spinner);
1188+
});
1189+
1190+
it('does not render spinner when `mouseleave` event is triggered', () => {
1191+
const element = mountWithApp(
1192+
<TextField
1193+
id="MyTextField"
1194+
label="TextField"
1195+
type="number"
1196+
value="3"
1197+
autoComplete="off"
1198+
stepperShownOnInteraction
1199+
/>,
1200+
);
1201+
1202+
element
1203+
.find(Connected)!
1204+
.triggerKeypath('children.props.onMouseEnter', {});
1205+
element
1206+
.find(Connected)!
1207+
.triggerKeypath('children.props.onMouseLeave', {});
1208+
1209+
expect(element).not.toContainReactComponent(Spinner);
1210+
});
1211+
});
11191212
});
11201213
});
11211214

polaris.shopify.com/content/components/selection-and-input/text-field.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ Text fields have standard keyboard support.
215215

216216
- Merchants who rely on the keyboard expect to move focus to each text field using the <kbd>tab</kbd> key (or <kbd>shift</kbd> + <kbd>tab</kbd> when tabbing backwards)
217217
- If the `type` is set to `number`, then merchants can use the up and down arrow keys to adjust the value typed into the field
218+
- Using the `stepperShownOnInteraction` prop if the `type` is set to `number` allows to hide the stepper arrows by default and show them when the field is interacted with to mimic the default browser behaviour
218219
- Using the `disabled` prop will prevent the text field from receive keyboard focus or inputs
219220
- The `readOnly` prop allows focus on the text field but prevents input or editing
220221
- The `inputMode` prop can be used to bring up a relevant keyboard for merchants on mobile; it’s passed down to the input as an [`inputmode` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode)

0 commit comments

Comments
 (0)