Skip to content

Commit 2119bbb

Browse files
authored
TS Strict NumberField (#5552)
* TS Strict NumberField
1 parent c3a4d63 commit 2119bbb

File tree

7 files changed

+49
-43
lines changed

7 files changed

+49
-43
lines changed

packages/@react-aria/numberfield/src/useNumberField.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
153153
// We choose between numeric and decimal based on whether we allow negative and fractional numbers,
154154
// and based on testing on various devices to determine what keys are available in each inputMode.
155155
let hasDecimals = intlOptions.maximumFractionDigits > 0;
156-
let hasNegative = isNaN(state.minValue) || state.minValue < 0;
156+
let hasNegative = (state.minValue === undefined || isNaN(state.minValue)) || state.minValue < 0;
157157
let inputMode: TextInputDOMProps['inputMode'] = 'numeric';
158158
if (isIPhone()) {
159159
// iPhone doesn't have a minus sign in either numeric or decimal.
@@ -205,8 +205,8 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
205205
value: inputValue,
206206
defaultValue: undefined, // defaultValue already used to populate state.inputValue, unneeded here
207207
autoComplete: 'off',
208-
'aria-label': props['aria-label'] || null,
209-
'aria-labelledby': props['aria-labelledby'] || null,
208+
'aria-label': props['aria-label'] || undefined,
209+
'aria-labelledby': props['aria-labelledby'] || undefined,
210210
id: inputId,
211211
type: 'text', // Can't use type="number" because then we can't have things like $ in the field.
212212
inputMode,
@@ -222,7 +222,7 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
222222

223223
useFormReset(inputRef, state.numberValue, state.setNumberValue);
224224

225-
let inputProps = mergeProps(
225+
let inputProps: InputHTMLAttributes<HTMLInputElement> = mergeProps(
226226
spinButtonProps,
227227
focusProps,
228228
textFieldProps,
@@ -255,7 +255,7 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
255255
// On touch, or with a screen reader, focus the button so that the software
256256
// keyboard does not appear and the screen reader cursor is not moved off the button.
257257
if (e.pointerType === 'mouse') {
258-
inputRef.current.focus();
258+
inputRef.current?.focus();
259259
} else {
260260
e.target.focus();
261261
}
@@ -272,7 +272,7 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
272272
// the aria-label string rather than using aria-labelledby gives more flexibility to translators to change
273273
// the order or add additional words around the label if needed.
274274
let fieldLabel = props['aria-label'] || (typeof props.label === 'string' ? props.label : '');
275-
let ariaLabelledby: string;
275+
let ariaLabelledby: string | undefined;
276276
if (!fieldLabel) {
277277
ariaLabelledby = props.label != null ? labelProps.id : props['aria-labelledby'];
278278
}

packages/@react-aria/numberfield/test/useNumberField.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,21 @@ describe('useNumberField hook', () => {
6464
onBeforeInput,
6565
onInput
6666
});
67-
inputProps.onCopy({} as any);
67+
inputProps.onCopy?.({} as any);
6868
expect(onCopy).toHaveBeenCalled();
69-
inputProps.onCut({} as any);
69+
inputProps.onCut?.({} as any);
7070
expect(onCut).toHaveBeenCalled();
71-
inputProps.onPaste({} as any);
71+
inputProps.onPaste?.({} as any);
7272
expect(onPaste).toHaveBeenCalled();
73-
inputProps.onCompositionStart({} as any);
73+
inputProps.onCompositionStart?.({} as any);
7474
expect(onCompositionStart).toHaveBeenCalled();
75-
inputProps.onCompositionEnd({} as any);
75+
inputProps.onCompositionEnd?.({} as any);
7676
expect(onCompositionEnd).toHaveBeenCalled();
77-
inputProps.onCompositionUpdate({} as any);
77+
inputProps.onCompositionUpdate?.({} as any);
7878
expect(onCompositionUpdate).toHaveBeenCalled();
79-
inputProps.onSelect({} as any);
79+
inputProps.onSelect?.({} as any);
8080
expect(onSelect).toHaveBeenCalled();
81-
inputProps.onBeforeInput({
81+
inputProps.onBeforeInput?.({
8282
preventDefault: jest.fn(),
8383
target: {
8484
value: '',
@@ -88,7 +88,7 @@ describe('useNumberField hook', () => {
8888
}
8989
} as any);
9090
expect(onBeforeInput).toHaveBeenCalled();
91-
inputProps.onInput({} as any);
91+
inputProps.onInput?.({} as any);
9292
expect(onInput).toHaveBeenCalled();
9393
});
9494
});

packages/@react-spectrum/numberfield/src/NumberField.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {FocusableRef} from '@react-types/shared';
1717
import {FocusRing} from '@react-aria/focus';
1818
import {mergeProps} from '@react-aria/utils';
1919
import {NumberFieldState, useNumberFieldState} from '@react-stately/numberfield';
20-
import React, {HTMLAttributes, InputHTMLAttributes, RefObject, useRef} from 'react';
20+
import React, {HTMLAttributes, InputHTMLAttributes, Ref, RefObject, useRef} from 'react';
2121
import {SpectrumNumberFieldProps} from '@react-types/numberfield';
2222
import {StepButton} from './StepButton';
2323
import stepperStyle from '@adobe/spectrum-css-temp/components/stepper/vars.css';
@@ -43,7 +43,7 @@ function NumberField(props: SpectrumNumberFieldProps, ref: FocusableRef<HTMLElem
4343

4444
let {locale} = useLocale();
4545
let state = useNumberFieldState({...props, locale});
46-
let inputRef = useRef<HTMLInputElement>();
46+
let inputRef = useRef<HTMLInputElement>(null);
4747
let domRef = useFocusableRef<HTMLElement>(ref, inputRef);
4848
let {
4949
groupProps,
@@ -62,21 +62,21 @@ function NumberField(props: SpectrumNumberFieldProps, ref: FocusableRef<HTMLElem
6262

6363
let {isHovered, hoverProps} = useHover({isDisabled});
6464

65-
let validationState = props.validationState || (isInvalid ? 'invalid' : null);
65+
let validationState = props.validationState || (isInvalid ? 'invalid' : undefined);
6666
let className =
6767
classNames(
6868
stepperStyle,
6969
'spectrum-Stepper',
70+
// because FocusRing won't pass along the className from Field, we have to handle that ourselves
71+
!props.label && style.className ? style.className : '',
7072
{
7173
'spectrum-Stepper--isQuiet': isQuiet,
7274
'is-disabled': isDisabled,
7375
'spectrum-Stepper--readonly': isReadOnly,
7476
'is-invalid': validationState === 'invalid' && !isDisabled,
7577
'spectrum-Stepper--showStepper': showStepper,
7678
'spectrum-Stepper--isMobile': isMobile,
77-
'is-hovered': isHovered,
78-
// because FocusRing won't pass along the className from Field, we have to handle that ourselves
79-
[style.className]: !props.label && style.className
79+
'is-hovered': isHovered
8080
}
8181
);
8282

@@ -124,7 +124,7 @@ interface NumberFieldInputProps extends SpectrumNumberFieldProps {
124124
state: NumberFieldState
125125
}
126126

127-
const NumberFieldInput = React.forwardRef(function NumberFieldInput(props: NumberFieldInputProps, ref: RefObject<HTMLElement>) {
127+
const NumberFieldInput = React.forwardRef(function NumberFieldInput(props: NumberFieldInputProps, ref: Ref<HTMLDivElement>) {
128128
let {
129129
groupProps,
130130
inputProps,
@@ -152,7 +152,7 @@ const NumberFieldInput = React.forwardRef(function NumberFieldInput(props: Numbe
152152
autoFocus={autoFocus}>
153153
<div
154154
{...groupProps}
155-
ref={ref as RefObject<HTMLDivElement>}
155+
ref={ref}
156156
style={style}
157157
className={className}>
158158
<TextFieldBase

packages/@react-spectrum/numberfield/src/StepButton.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,32 @@ import Add from '@spectrum-icons/workflow/Add';
1414
import {AriaButtonProps} from '@react-types/button';
1515
import ChevronDownSmall from '@spectrum-icons/ui/ChevronDownSmall';
1616
import ChevronUpSmall from '@spectrum-icons/ui/ChevronUpSmall';
17-
import {classNames} from '@react-spectrum/utils';
17+
import {classNames, useFocusableRef} from '@react-spectrum/utils';
18+
import {FocusableRef} from '@react-types/shared';
1819
import {FocusRing} from '@react-aria/focus';
1920
import {mergeProps} from '@react-aria/utils';
20-
import React, {RefObject} from 'react';
21+
import React, {ReactElement} from 'react';
2122
import Remove from '@spectrum-icons/workflow/Remove';
2223
import stepperStyle from '@adobe/spectrum-css-temp/components/stepper/vars.css';
2324
import {useButton} from '@react-aria/button';
2425
import {useHover} from '@react-aria/interactions';
2526
import {useProvider, useProviderProps} from '@react-spectrum/provider';
2627

2728
interface StepButtonProps extends AriaButtonProps {
28-
isQuiet: boolean,
29+
isQuiet?: boolean,
2930
direction: 'up' | 'down'
3031
}
3132

32-
function StepButton(props: StepButtonProps, ref: RefObject<HTMLDivElement>) {
33+
function StepButton(props: StepButtonProps, ref: FocusableRef<HTMLDivElement>) {
3334
props = useProviderProps(props);
3435
let {scale} = useProvider();
3536
let {direction, isDisabled, isQuiet} = props;
37+
let domRef = useFocusableRef(ref);
3638
/**
3739
* Must use div for now because Safari pointer event bugs on disabled form elements.
3840
* Link https://bugs.webkit.org/show_bug.cgi?id=219188.
3941
*/
40-
let {buttonProps, isPressed} = useButton({...props, elementType: 'div'}, ref);
42+
let {buttonProps, isPressed} = useButton({...props, elementType: 'div'}, domRef);
4143
let {hoverProps, isHovered} = useHover(props);
4244
return (
4345
<FocusRing focusRingClass={classNames(stepperStyle, 'focus-ring')}>
@@ -57,7 +59,7 @@ function StepButton(props: StepButtonProps, ref: RefObject<HTMLDivElement>) {
5759
)
5860
}
5961
{...mergeProps(hoverProps, buttonProps)}
60-
ref={ref}>
62+
ref={domRef}>
6163
{direction === 'up' && scale === 'large' &&
6264
<Add UNSAFE_className={classNames(stepperStyle, 'spectrum-Stepper-button-icon', 'spectrum-Stepper-stepUpIcon')} size="S" />
6365
}
@@ -78,5 +80,5 @@ function StepButton(props: StepButtonProps, ref: RefObject<HTMLDivElement>) {
7880
/**
7981
* Buttons for NumberField.
8082
*/
81-
let _StepButton = React.forwardRef(StepButton);
83+
let _StepButton = React.forwardRef(StepButton) as (props: StepButtonProps & {ref?: FocusableRef<HTMLDivElement>}) => ReactElement;
8284
export {_StepButton as StepButton};

packages/@react-spectrum/numberfield/stories/NumberField.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ function NumberFieldControlledStateReset() {
478478
onChange={(value) => setControlledValue(value)} />
479479
<Button
480480
variant={'primary'}
481-
onPress={() => setControlledValue(null)}>
481+
onPress={() => setControlledValue(NaN)}>
482482
Reset
483483
</Button>
484484
</>

packages/@react-stately/numberfield/src/useNumberFieldState.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export interface NumberFieldState extends FormValidationState {
2828
*/
2929
numberValue: number,
3030
/** The minimum value of the number field. */
31-
minValue: number,
31+
minValue?: number,
3232
/** The maximum value of the number field. */
33-
maxValue: number,
33+
maxValue?: number,
3434
/** Whether the current value can be incremented according to the maximum value and step. */
3535
canIncrement: boolean,
3636
/** Whether the current value can be decremented according to the minimum value and step. */
@@ -83,7 +83,7 @@ export function useNumberFieldState(
8383
step,
8484
formatOptions,
8585
value,
86-
defaultValue,
86+
defaultValue = NaN,
8787
onChange,
8888
locale,
8989
isDisabled,
@@ -94,16 +94,16 @@ export function useNumberFieldState(
9494
value = NaN;
9595
}
9696

97-
if (!isNaN(value)) {
98-
if (!isNaN(step)) {
97+
if (value !== undefined && !isNaN(value)) {
98+
if (step !== undefined && !isNaN(step)) {
9999
value = snapValueToStep(value, minValue, maxValue, step);
100100
} else {
101101
value = clamp(value, minValue, maxValue);
102102
}
103103
}
104104

105105
if (!isNaN(defaultValue)) {
106-
if (!isNaN(step)) {
106+
if (step !== undefined && !isNaN(step)) {
107107
defaultValue = snapValueToStep(defaultValue, minValue, maxValue, step);
108108
} else {
109109
defaultValue = clamp(defaultValue, minValue, maxValue);
@@ -124,8 +124,8 @@ export function useNumberFieldState(
124124
value: numberValue
125125
});
126126

127-
let clampStep = !isNaN(step) ? step : 1;
128-
if (intlOptions.style === 'percent' && isNaN(step)) {
127+
let clampStep = (step !== undefined && !isNaN(step)) ? step : 1;
128+
if (intlOptions.style === 'percent' && (step === undefined || isNaN(step))) {
129129
clampStep = 0.01;
130130
}
131131

@@ -159,7 +159,7 @@ export function useNumberFieldState(
159159

160160
// Clamp to min and max, round to the nearest step, and round to specified number of digits
161161
let clampedValue: number;
162-
if (isNaN(step)) {
162+
if (step === undefined || isNaN(step)) {
163163
clampedValue = clamp(parsedValue, minValue, maxValue);
164164
} else {
165165
clampedValue = snapValueToStep(parsedValue, minValue, maxValue, step);
@@ -172,7 +172,7 @@ export function useNumberFieldState(
172172
setInputValue(format(value === undefined ? clampedValue : numberValue));
173173
};
174174

175-
let safeNextStep = (operation: '+' | '-', minMax: number) => {
175+
let safeNextStep = (operation: '+' | '-', minMax: number = 0) => {
176176
let prev = parsedValue;
177177

178178
if (isNaN(prev)) {
@@ -242,7 +242,7 @@ export function useNumberFieldState(
242242
!isReadOnly &&
243243
(
244244
isNaN(parsedValue) ||
245-
isNaN(maxValue) ||
245+
(maxValue === undefined || isNaN(maxValue)) ||
246246
snapValueToStep(parsedValue, minValue, maxValue, clampStep) > parsedValue ||
247247
handleDecimalOperation('+', parsedValue, clampStep) <= maxValue
248248
)
@@ -253,7 +253,7 @@ export function useNumberFieldState(
253253
!isReadOnly &&
254254
(
255255
isNaN(parsedValue) ||
256-
isNaN(minValue) ||
256+
(minValue === undefined || isNaN(minValue)) ||
257257
snapValueToStep(parsedValue, minValue, maxValue, clampStep) < parsedValue ||
258258
handleDecimalOperation('-', parsedValue, clampStep) >= minValue
259259
)

tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"./packages/@react-aria/k",
5050
"./packages/@react-aria/l",
5151
"./packages/@react-aria/meter",
52+
"./packages/@react-aria/numberfield",
5253
"./packages/@react-aria/progress",
5354
"./packages/@react-aria/q",
5455
"./packages/@react-aria/radio",
@@ -79,6 +80,7 @@
7980
"./packages/@react-spectrum/label",
8081
"./packages/@react-spectrum/link",
8182
"./packages/@react-spectrum/meter",
83+
"./packages/@react-spectrum/numberfield",
8284
"./packages/@react-spectrum/progress",
8385
"./packages/@react-spectrum/radio",
8486
"./packages/@react-spectrum/searchfield",
@@ -96,6 +98,7 @@
9698
"./packages/@react-spectrum/view",
9799
"./packages/@react-spectrum/well",
98100
"./packages/@react-stately/checkbox",
101+
"./packages/@react-stately/numberfield",
99102
"./packages/@react-stately/overlays",
100103
"./packages/@react-stately/pagination",
101104
"./packages/@react-stately/searchfield",
@@ -112,6 +115,7 @@
112115
"./packages/@react-types/image",
113116
"./packages/@react-types/l",
114117
"./packages/@react-types/meter",
118+
"./packages/@react-types/numberfield",
115119
"./packages/@react-types/progress",
116120
"./packages/@react-types/searchfield",
117121
"./packages/@react-types/shared",

0 commit comments

Comments
 (0)