Skip to content

Commit 2fe9b33

Browse files
authored
API audit for number field (#1537)
1 parent cbaffb3 commit 2fe9b33

File tree

5 files changed

+113
-115
lines changed

5 files changed

+113
-115
lines changed

packages/@react-aria/color/src/useHexColorField.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {useTextField} from '@react-aria/textfield';
2323

2424
interface HexColorFieldAria {
2525
labelProps: LabelHTMLAttributes<HTMLLabelElement>,
26-
inputFieldProps: HTMLAttributes<HTMLInputElement>
26+
inputProps: HTMLAttributes<HTMLInputElement>
2727
}
2828

2929
export function useHexColorField(
@@ -87,7 +87,7 @@ export function useHexColorField(
8787

8888
return {
8989
labelProps,
90-
inputFieldProps: mergeProps(inputProps, spinButtonProps, {
90+
inputProps: mergeProps(inputProps, spinButtonProps, {
9191
role: 'textbox',
9292
'aria-valuemax': null,
9393
'aria-valuemin': null,

packages/@react-aria/color/test/useHexColorField.test.js

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,68 +31,68 @@ describe('useHexColorField', function () {
3131
};
3232

3333
it('handles defaults', function () {
34-
let {inputFieldProps} = renderHexColorFieldHook({});
35-
expect(inputFieldProps.type).toBe('text');
36-
expect(inputFieldProps.autoComplete).toBe('off');
37-
expect(inputFieldProps.autoCorrect).toBe('off');
38-
expect(inputFieldProps.id).toBeTruthy();
39-
expect(inputFieldProps.role).toBe('textbox');
40-
expect(inputFieldProps['aria-valuenow']).toBeNull();
41-
expect(inputFieldProps['aria-valuetext']).toBeNull();
42-
expect(inputFieldProps['aria-valuemin']).toBeNull();
43-
expect(inputFieldProps['aria-valuemax']).toBeNull();
44-
expect(inputFieldProps['aria-required']).toBeNull();
45-
expect(inputFieldProps['aria-disabled']).toBeNull();
46-
expect(inputFieldProps['aria-readonly']).toBeNull();
47-
expect(inputFieldProps['aria-invalid']).toBeUndefined();
48-
expect(inputFieldProps.disabled).toBe(false);
49-
expect(inputFieldProps.readOnly).toBe(false);
34+
let {inputProps} = renderHexColorFieldHook({});
35+
expect(inputProps.type).toBe('text');
36+
expect(inputProps.autoComplete).toBe('off');
37+
expect(inputProps.autoCorrect).toBe('off');
38+
expect(inputProps.id).toBeTruthy();
39+
expect(inputProps.role).toBe('textbox');
40+
expect(inputProps['aria-valuenow']).toBeNull();
41+
expect(inputProps['aria-valuetext']).toBeNull();
42+
expect(inputProps['aria-valuemin']).toBeNull();
43+
expect(inputProps['aria-valuemax']).toBeNull();
44+
expect(inputProps['aria-required']).toBeNull();
45+
expect(inputProps['aria-disabled']).toBeNull();
46+
expect(inputProps['aria-readonly']).toBeNull();
47+
expect(inputProps['aria-invalid']).toBeUndefined();
48+
expect(inputProps.disabled).toBe(false);
49+
expect(inputProps.readOnly).toBe(false);
5050
});
5151

5252
it('should return props for colorValue provided', function () {
5353
let colorValue = parseColor('#ff88a0');
54-
let {inputFieldProps} = renderHexColorFieldHook({}, {colorValue, inputValue: colorValue.toString('hex')});
55-
expect(inputFieldProps['aria-valuenow']).toBeNull();
56-
expect(inputFieldProps['aria-valuetext']).toBeNull();
57-
expect(inputFieldProps['value']).toBe('#FF88A0');
54+
let {inputProps} = renderHexColorFieldHook({}, {colorValue, inputValue: colorValue.toString('hex')});
55+
expect(inputProps['aria-valuenow']).toBeNull();
56+
expect(inputProps['aria-valuetext']).toBeNull();
57+
expect(inputProps['value']).toBe('#FF88A0');
5858
});
5959

6060
it('should return props for label', function () {
61-
let {labelProps, inputFieldProps} = renderHexColorFieldHook({
61+
let {labelProps, inputProps} = renderHexColorFieldHook({
6262
'aria-label': undefined,
6363
label: 'Secondary Color'
6464
});
6565
expect(labelProps.id).toBeTruthy();
66-
expect(labelProps.htmlFor).toBe(inputFieldProps.id);
67-
expect(inputFieldProps.id).toBeTruthy();
68-
expect(inputFieldProps['aria-labelledby']).toBe(labelProps.id);
69-
expect(inputFieldProps['aria-label']).toBeUndefined(); // because label prop is provided instead of aria-label
70-
expect(typeof inputFieldProps.onChange).toBe('function');
71-
expect(typeof inputFieldProps.onBlur).toBe('function');
72-
expect(typeof inputFieldProps.onFocus).toBe('function');
73-
expect(typeof inputFieldProps.onKeyDown).toBe('function');
74-
expect(typeof inputFieldProps.onWheel).toBe('function');
66+
expect(labelProps.htmlFor).toBe(inputProps.id);
67+
expect(inputProps.id).toBeTruthy();
68+
expect(inputProps['aria-labelledby']).toBe(labelProps.id);
69+
expect(inputProps['aria-label']).toBeUndefined(); // because label prop is provided instead of aria-label
70+
expect(typeof inputProps.onChange).toBe('function');
71+
expect(typeof inputProps.onBlur).toBe('function');
72+
expect(typeof inputProps.onFocus).toBe('function');
73+
expect(typeof inputProps.onKeyDown).toBe('function');
74+
expect(typeof inputProps.onWheel).toBe('function');
7575
});
7676

7777
it('should return prop for invalid', function () {
78-
let {inputFieldProps} = renderHexColorFieldHook({validationState: 'invalid'});
79-
expect(inputFieldProps['aria-invalid']).toBe(true);
78+
let {inputProps} = renderHexColorFieldHook({validationState: 'invalid'});
79+
expect(inputProps['aria-invalid']).toBe(true);
8080
});
8181

8282
it('should return prop for required', function () {
83-
let {inputFieldProps} = renderHexColorFieldHook({isRequired: true});
84-
expect(inputFieldProps['aria-required']).toBe(true);
83+
let {inputProps} = renderHexColorFieldHook({isRequired: true});
84+
expect(inputProps['aria-required']).toBe(true);
8585
});
8686

8787
it('should return prop for readonly', function () {
88-
let {inputFieldProps} = renderHexColorFieldHook({isReadOnly: true});
89-
expect(inputFieldProps['aria-readonly']).toBe(true);
90-
expect(inputFieldProps.readOnly).toBe(true);
88+
let {inputProps} = renderHexColorFieldHook({isReadOnly: true});
89+
expect(inputProps['aria-readonly']).toBe(true);
90+
expect(inputProps.readOnly).toBe(true);
9191
});
9292

9393
it('should return prop for disabled', function () {
94-
let {inputFieldProps} = renderHexColorFieldHook({isDisabled: true});
95-
expect(inputFieldProps['aria-disabled']).toBe(true);
96-
expect(inputFieldProps.disabled).toBe(true);
94+
let {inputProps} = renderHexColorFieldHook({isDisabled: true});
95+
expect(inputProps['aria-disabled']).toBe(true);
96+
expect(inputProps.disabled).toBe(true);
9797
});
9898
});

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

Lines changed: 61 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,25 @@ import {
2626
import intlMessages from '../intl/*.json';
2727
import {isAndroid, isIOS, isIPhone, mergeProps, useId} from '@react-aria/utils';
2828
import {NumberFieldState} from '@react-stately/numberfield';
29-
import {SpinButtonProps, useSpinButton} from '@react-aria/spinbutton';
3029
import {TextInputDOMProps} from '@react-types/shared';
3130
import {useFocus} from '@react-aria/interactions';
3231
import {
3332
useMessageFormatter,
3433
useNumberFormatter
3534
} from '@react-aria/i18n';
35+
import {useSpinButton} from '@react-aria/spinbutton';
3636
import {useTextField} from '@react-aria/textfield';
3737

38-
interface NumberFieldProps extends AriaNumberFieldProps, SpinButtonProps {
39-
inputRef?: RefObject<HTMLInputElement>,
40-
decrementAriaLabel?: string,
41-
incrementAriaLabel?: string
42-
}
43-
4438
interface NumberFieldAria {
39+
/** Props for the label element. */
4540
labelProps: LabelHTMLAttributes<HTMLLabelElement>,
46-
inputFieldProps: InputHTMLAttributes<HTMLInputElement>,
47-
numberFieldProps: HTMLAttributes<HTMLDivElement>,
41+
/** Props for the group wrapper around the input and stepper buttons. */
42+
groupProps: HTMLAttributes<HTMLElement>,
43+
/** Props for the input element. */
44+
inputProps: InputHTMLAttributes<HTMLInputElement>,
45+
/** Props for the increment button, to be passed to [useButton](useButton.html). */
4846
incrementButtonProps: AriaButtonProps,
47+
/** Props for the decrement button, to be passed to [useButton](useButton.html). */
4948
decrementButtonProps: AriaButtonProps
5049
}
5150

@@ -56,7 +55,7 @@ function supportsNativeBeforeInputEvent() {
5655
typeof InputEvent.prototype.getTargetRanges === 'function';
5756
}
5857

59-
export function useNumberField(props: NumberFieldProps, state: NumberFieldState): NumberFieldAria {
58+
export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldState, inputRef: RefObject<HTMLInputElement>): NumberFieldAria {
6059
let {
6160
decrementAriaLabel,
6261
incrementAriaLabel,
@@ -68,8 +67,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
6867
autoFocus,
6968
validationState,
7069
label,
71-
formatOptions,
72-
inputRef
70+
formatOptions
7371
} = props;
7472

7573
let {
@@ -83,7 +81,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
8381

8482
const formatMessage = useMessageFormatter(intlMessages);
8583

86-
const inputId = useId();
84+
let inputId = useId();
8785

8886
let {focusProps} = useFocus({
8987
onBlur: () => {
@@ -92,7 +90,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
9290
}
9391
});
9492

95-
const {
93+
let {
9694
spinButtonProps,
9795
incrementButtonProps: incButtonProps,
9896
decrementButtonProps: decButtonProps
@@ -132,7 +130,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
132130
incrementAriaLabel = incrementAriaLabel || formatMessage('Increment');
133131
decrementAriaLabel = decrementAriaLabel || formatMessage('Decrement');
134132

135-
const incrementButtonProps: AriaButtonProps = mergeProps(incButtonProps, {
133+
let incrementButtonProps: AriaButtonProps = mergeProps(incButtonProps, {
136134
'aria-label': incrementAriaLabel,
137135
'aria-controls': inputId,
138136
excludeFromTabOrder: true,
@@ -141,7 +139,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
141139
onPressStart: onButtonPressStart
142140
});
143141

144-
const decrementButtonProps: AriaButtonProps = mergeProps(decButtonProps, {
142+
let decrementButtonProps: AriaButtonProps = mergeProps(decButtonProps, {
145143
'aria-label': decrementAriaLabel,
146144
'aria-controls': inputId,
147145
excludeFromTabOrder: true,
@@ -285,54 +283,53 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
285283
};
286284

287285
let compositionStartState = useRef(null);
288-
let {labelProps, inputProps} = useTextField(
289-
{
290-
label,
291-
autoFocus,
292-
isDisabled,
293-
isReadOnly,
294-
isRequired,
295-
validationState,
296-
value: state.inputValue,
297-
autoComplete: 'off',
298-
'aria-label': props['aria-label'] || null,
299-
'aria-labelledby': props['aria-labelledby'] || null,
300-
id: inputId,
301-
type: 'text', // Can't use type="number" because then we can't have things like $ in the field.
302-
inputMode,
303-
onChange,
304-
onBeforeInput,
305-
onCompositionStart() {
306-
// Chrome does not implement Input Events Level 2, which specifies the insertFromComposition
307-
// and deleteByComposition inputType values for the beforeinput event. These are meant to occur
308-
// at the end of a composition (e.g. Pinyin IME, Android auto correct, etc.), and crucially, are
309-
// cancelable. The insertCompositionText and deleteCompositionText input types are not cancelable,
310-
// nor would we want to cancel them because the input from the user is incomplete at that point.
311-
// In Safari, insertFromComposition/deleteFromComposition will fire, however, allowing us to cancel
312-
// the final composition result if it is invalid. As a fallback for Chrome and Firefox, which either
313-
// don't support Input Events Level 2, or beforeinput at all, we store the state of the input when
314-
// the compositionstart event fires, and undo the changes in compositionend (below) if it is invalid.
315-
// Unfortunately, this messes up the undo/redo stack, but until insertFromComposition/deleteByComposition
316-
// are implemented, there is no other way to prevent composed input.
317-
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1022204
318-
let {value, selectionStart, selectionEnd} = inputRef.current;
319-
compositionStartState.current = {value, selectionStart, selectionEnd};
320-
},
321-
onCompositionEnd() {
322-
if (!state.validate(inputRef.current.value)) {
323-
// Restore the input value in the DOM immediately so we can synchronously update the selection position.
324-
// But also update the value in React state as well so it is correct for future updates.
325-
let {value, selectionStart, selectionEnd} = compositionStartState.current;
326-
inputRef.current.value = value;
327-
inputRef.current.setSelectionRange(selectionStart, selectionEnd);
328-
state.setInputValue(value);
329-
}
286+
let {labelProps, inputProps: textFieldProps} = useTextField({
287+
label,
288+
autoFocus,
289+
isDisabled,
290+
isReadOnly,
291+
isRequired,
292+
validationState,
293+
value: state.inputValue,
294+
autoComplete: 'off',
295+
'aria-label': props['aria-label'] || null,
296+
'aria-labelledby': props['aria-labelledby'] || null,
297+
id: inputId,
298+
type: 'text', // Can't use type="number" because then we can't have things like $ in the field.
299+
inputMode,
300+
onChange,
301+
onBeforeInput,
302+
onCompositionStart() {
303+
// Chrome does not implement Input Events Level 2, which specifies the insertFromComposition
304+
// and deleteByComposition inputType values for the beforeinput event. These are meant to occur
305+
// at the end of a composition (e.g. Pinyin IME, Android auto correct, etc.), and crucially, are
306+
// cancelable. The insertCompositionText and deleteCompositionText input types are not cancelable,
307+
// nor would we want to cancel them because the input from the user is incomplete at that point.
308+
// In Safari, insertFromComposition/deleteFromComposition will fire, however, allowing us to cancel
309+
// the final composition result if it is invalid. As a fallback for Chrome and Firefox, which either
310+
// don't support Input Events Level 2, or beforeinput at all, we store the state of the input when
311+
// the compositionstart event fires, and undo the changes in compositionend (below) if it is invalid.
312+
// Unfortunately, this messes up the undo/redo stack, but until insertFromComposition/deleteByComposition
313+
// are implemented, there is no other way to prevent composed input.
314+
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1022204
315+
let {value, selectionStart, selectionEnd} = inputRef.current;
316+
compositionStartState.current = {value, selectionStart, selectionEnd};
317+
},
318+
onCompositionEnd() {
319+
if (!state.validate(inputRef.current.value)) {
320+
// Restore the input value in the DOM immediately so we can synchronously update the selection position.
321+
// But also update the value in React state as well so it is correct for future updates.
322+
let {value, selectionStart, selectionEnd} = compositionStartState.current;
323+
inputRef.current.value = value;
324+
inputRef.current.setSelectionRange(selectionStart, selectionEnd);
325+
state.setInputValue(value);
330326
}
331-
}, inputRef);
327+
}
328+
}, inputRef);
332329

333-
const inputFieldProps = mergeProps(
330+
let inputProps = mergeProps(
334331
spinButtonProps,
335-
inputProps,
332+
textFieldProps,
336333
focusProps,
337334
{
338335
// override the spinbutton role, we can't focus a spin button with VO
@@ -347,14 +344,15 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
347344
spellCheck: 'false'
348345
}
349346
);
347+
350348
return {
351-
numberFieldProps: {
349+
groupProps: {
352350
role: 'group',
353351
'aria-disabled': isDisabled,
354352
'aria-invalid': validationState === 'invalid' ? 'true' : undefined
355353
},
356354
labelProps,
357-
inputFieldProps,
355+
inputProps,
358356
incrementButtonProps,
359357
decrementButtonProps
360358
};

packages/@react-spectrum/color/src/HexColorField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function HexColorField(props: SpectrumHexColorFieldProps, ref: RefObject<TextFie
3131
let inputRef = useRef<HTMLInputElement & HTMLTextAreaElement>();
3232
let {
3333
labelProps,
34-
inputFieldProps
34+
inputProps
3535
} = useHexColorField(otherProps, state, inputRef);
3636

3737
return (
@@ -40,7 +40,7 @@ function HexColorField(props: SpectrumHexColorFieldProps, ref: RefObject<TextFie
4040
ref={ref}
4141
inputRef={inputRef}
4242
labelProps={labelProps}
43-
inputProps={inputFieldProps} />
43+
inputProps={inputProps} />
4444
);
4545
}
4646

0 commit comments

Comments
 (0)