@vertigis/react-ui 11.29.0 → 11.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FormLabelColorField/FormLabelColorField.d.ts +19 -0
- package/FormLabelColorField/FormLabelColorField.js +23 -0
- package/FormLabelColorField/index.d.ts +2 -0
- package/FormLabelColorField/index.js +2 -0
- package/FormLabelNumberField/FormLabelNumberField.d.ts +32 -0
- package/FormLabelNumberField/FormLabelNumberField.js +75 -0
- package/FormLabelNumberField/index.d.ts +2 -0
- package/FormLabelNumberField/index.js +2 -0
- package/FormLabelSliderField/FormLabelSliderField.d.ts +43 -0
- package/FormLabelSliderField/FormLabelSliderField.js +88 -0
- package/FormLabelSliderField/index.d.ts +2 -0
- package/FormLabelSliderField/index.js +2 -0
- package/NumberFormatContext/NumberFormatContext.d.ts +20 -0
- package/NumberFormatContext/NumberFormatContext.js +15 -0
- package/NumberFormatContext/index.d.ts +2 -0
- package/NumberFormatContext/index.js +2 -0
- package/NumberInput/NumberInput.d.ts +57 -0
- package/NumberInput/NumberInput.js +143 -0
- package/NumberInput/index.d.ts +2 -0
- package/NumberInput/index.js +2 -0
- package/SymbolInput/SymbolInput.d.ts +77 -0
- package/SymbolInput/SymbolInput.js +457 -0
- package/SymbolInput/SymbolJson.d.ts +134 -0
- package/SymbolInput/SymbolJson.js +2 -0
- package/SymbolInput/index.d.ts +2 -0
- package/SymbolInput/index.js +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import type { TextFieldProps } from "@mui/material/TextField";
|
|
3
|
+
import { type ColorInputProps } from "../ColorInput/ColorInput.js";
|
|
4
|
+
import type { FormControlProps } from "../FormControl/index.js";
|
|
5
|
+
export type FormLabelColorFieldProps = Omit<TextFieldProps, "variant" | "defaultValue" | "onFocus" | "onBlur" | "onChange" | "value" | "select" | "multiline" | "minRows" | "maxRows" | "SelectProps" | "rows"> & Pick<FormControlProps, "inlineHelpContent" | "inlineHelpUrl"> & {
|
|
6
|
+
defaultValue?: ColorInputProps["defaultValue"];
|
|
7
|
+
disabled?: ColorInputProps["disabled"];
|
|
8
|
+
onBlur?: ColorInputProps["onBlur"];
|
|
9
|
+
onChange?: ColorInputProps["onChange"];
|
|
10
|
+
onFocus?: ColorInputProps["onFocus"];
|
|
11
|
+
value: ColorInputProps["value"];
|
|
12
|
+
ColorInputProps?: ColorInputProps;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Like TextField, but uses a FormLabel instead of an InputLabel. Also allows
|
|
16
|
+
* for inline help.
|
|
17
|
+
*/
|
|
18
|
+
declare const FormLabelColorField: import("react").ForwardRefExoticComponent<Pick<FormLabelColorFieldProps, "error" | "id" | "label" | "slot" | "style" | "title" | "type" | "results" | "size" | "role" | "resource" | "key" | "value" | "className" | "classes" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "lang" | "nonce" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "about" | "datatype" | "inlist" | "prefix" | "property" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "children" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "disabled" | "sx" | "margin" | "fullWidth" | "autoFocus" | "name" | "autoComplete" | "inputProps" | "inputRef" | "required" | "hiddenLabel" | "focused" | "InputProps" | "FormHelperTextProps" | "helperText" | "InputLabelProps" | "inlineHelpContent" | "inlineHelpUrl" | "ColorInputProps"> & import("react").RefAttributes<HTMLDivElement>>;
|
|
19
|
+
export default FormLabelColorField;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import ColorInput, {} from "../ColorInput/ColorInput.js";
|
|
4
|
+
import FormControl from "../FormControl/index.js";
|
|
5
|
+
import FormHelperText from "../FormHelperText/index.js";
|
|
6
|
+
import FormLabel from "../FormLabel/index.js";
|
|
7
|
+
import { useId } from "../utils/react.js";
|
|
8
|
+
/**
|
|
9
|
+
* Like TextField, but uses a FormLabel instead of an InputLabel. Also allows
|
|
10
|
+
* for inline help.
|
|
11
|
+
*/
|
|
12
|
+
const FormLabelColorField = forwardRef(function FormLabelColorField(props, ref) {
|
|
13
|
+
const { defaultValue, FormHelperTextProps, fullWidth = false, helperText, id: idProp, label, onBlur, onChange, onFocus, placeholder, type, value, disabled, ColorInputProps, ...other } = props;
|
|
14
|
+
const id = useId(idProp);
|
|
15
|
+
const helperTextId = helperText && id ? `${id}-helper-text` : undefined;
|
|
16
|
+
const inputLabelId = label && id ? `${id}-label` : undefined;
|
|
17
|
+
return (_jsxs(FormControl, { fullWidth: fullWidth, ref: ref, disabled: disabled, ...other, children: [label && (_jsx(FormLabel, { htmlFor: id, id: inputLabelId, children: label })), helperText && (_jsx(FormHelperText, { id: helperTextId, ...FormHelperTextProps, children: helperText })), _jsx(ColorInput, { id: id, placeholder: placeholder, onBlur: onBlur, onFocus: onFocus, defaultValue: defaultValue, "aria-describedby": helperTextId, "aria-labelledby": inputLabelId, onChange: onChange, value: value, disabled: disabled, ...{
|
|
18
|
+
...ColorInputProps,
|
|
19
|
+
// Workaround typings issue.
|
|
20
|
+
ref: ColorInputProps?.ref,
|
|
21
|
+
} })] }));
|
|
22
|
+
});
|
|
23
|
+
export default FormLabelColorField;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TextFieldProps } from "@mui/material/TextField";
|
|
2
|
+
import { type FC } from "react";
|
|
3
|
+
import type { FormControlProps } from "../FormControl/index.js";
|
|
4
|
+
import { type NumberInputProps } from "../NumberInput/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Properties for FormLabelNumberField.
|
|
7
|
+
*/
|
|
8
|
+
export type FormLabelNumberFieldProps = Omit<TextFieldProps, "aria-label" | "value" | "onChange" | "InputProps" | "select" | "nativeSelect" | "multiline" | "minRows" | "maxRows" | "SelectProps" | "rows" | "type" | "title" | "onError"> & Pick<FormControlProps, "inlineHelpContent" | "inlineHelpUrl"> & NumberInputProps & {
|
|
9
|
+
InputProps?: Omit<NumberInputProps, "ref">;
|
|
10
|
+
NumberInputComponent?: FC<NumberInputProps>;
|
|
11
|
+
/**
|
|
12
|
+
* A label to display as helpText when the number input value is out of
|
|
13
|
+
* range, relative to the min and max properties.
|
|
14
|
+
*/
|
|
15
|
+
numberOutOfRangeErrorText?: string;
|
|
16
|
+
/**
|
|
17
|
+
* A label to display as helpText when the number input value is invalid
|
|
18
|
+
* (ie. NaN).
|
|
19
|
+
*/
|
|
20
|
+
invalidNumberErrorText?: string;
|
|
21
|
+
/**
|
|
22
|
+
* A label to display as helpText when the number input value has more
|
|
23
|
+
* than the configured maxDecimalPlaces.
|
|
24
|
+
*/
|
|
25
|
+
maxDecimalPlacesErrorText?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Like TextField, but uses a FormLabel instead of an InputLabel, and uses
|
|
29
|
+
* NumberInput instead of a normal input.
|
|
30
|
+
*/
|
|
31
|
+
declare const FormLabelNumberField: import("react").ForwardRefExoticComponent<Pick<FormLabelNumberFieldProps, "error" | "id" | "label" | "slot" | "style" | "title" | "results" | "size" | "role" | "resource" | "key" | "value" | "components" | "className" | "classes" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "lang" | "nonce" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "about" | "datatype" | "inlist" | "prefix" | "property" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "children" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "disabled" | "sx" | "margin" | "fullWidth" | "variant" | "autoFocus" | "name" | "autoComplete" | "componentsProps" | "disableInjectingGlobalStyles" | "endAdornment" | "inputComponent" | "inputProps" | "inputRef" | "multiline" | "readOnly" | "required" | "renderSuffix" | "rows" | "maxRows" | "minRows" | "slotProps" | "slots" | "startAdornment" | "hiddenLabel" | "disableUnderline" | "focused" | "InputProps" | "FormHelperTextProps" | "helperText" | "InputLabelProps" | "max" | "min" | "inlineHelpContent" | "inlineHelpUrl" | "onErrorEnd" | "allowUndefined" | "maxDecimalPlaces" | "correctOnBlur" | "NumberInputComponent" | "numberOutOfRangeErrorText" | "invalidNumberErrorText" | "maxDecimalPlacesErrorText"> & import("react").RefAttributes<HTMLDivElement>>;
|
|
32
|
+
export default FormLabelNumberField;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useContext, useState } from "react";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import FormControl from "../FormControl/index.js";
|
|
5
|
+
import FormHelperText from "../FormHelperText/index.js";
|
|
6
|
+
import FormLabel from "../FormLabel/index.js";
|
|
7
|
+
import { NumberFormatContext } from "../NumberFormatContext/NumberFormatContext.js";
|
|
8
|
+
import { DEFAULT_DECIMAL_PLACES, } from "../NumberInput/index.js";
|
|
9
|
+
import NumberInput from "../NumberInput/index.js";
|
|
10
|
+
import { useId } from "../utils/react.js";
|
|
11
|
+
/**
|
|
12
|
+
* Like TextField, but uses a FormLabel instead of an InputLabel, and uses
|
|
13
|
+
* NumberInput instead of a normal input.
|
|
14
|
+
*/
|
|
15
|
+
const FormLabelNumberField = forwardRef(function FormLabelNumberField(props, ref) {
|
|
16
|
+
const { autoComplete, autoFocus = false, children, className, defaultValue, FormHelperTextProps, fullWidth = false, helperText, id: idProp, InputLabelProps, inputProps, InputProps, inputRef, label, name, NumberInputComponent, onBlur, onChange, onFocus, placeholder, value, min, max, numberOutOfRangeErrorText, invalidNumberErrorText, maxDecimalPlacesErrorText, error, onError, onErrorEnd, allowUndefined, maxDecimalPlaces = DEFAULT_DECIMAL_PLACES, correctOnBlur = true, ...other } = props;
|
|
17
|
+
const id = useId(idProp);
|
|
18
|
+
const helperTextId = helperText && id ? `${id}-helper-text` : undefined;
|
|
19
|
+
const inputLabelId = label && id ? `${id}-label` : undefined;
|
|
20
|
+
const FormNumberInput = NumberInputComponent ?? NumberInput;
|
|
21
|
+
const { parseNumber } = useContext(NumberFormatContext);
|
|
22
|
+
const [numberError, setNumberError] = useState();
|
|
23
|
+
const handleError = useCallback((numericValue, textValue, errorType) => {
|
|
24
|
+
if (errorType === "out-of-range") {
|
|
25
|
+
setNumberError(numberOutOfRangeErrorText ??
|
|
26
|
+
getNumberErrorText(parseNumber(textValue), min, max));
|
|
27
|
+
}
|
|
28
|
+
else if (errorType === "NaN") {
|
|
29
|
+
setNumberError(invalidNumberErrorText ?? "Value must be a valid number.");
|
|
30
|
+
}
|
|
31
|
+
else if (errorType === "max-decimals") {
|
|
32
|
+
setNumberError(maxDecimalPlacesErrorText ??
|
|
33
|
+
`Value must have at most ${maxDecimalPlaces} decimal places.`);
|
|
34
|
+
}
|
|
35
|
+
onError?.(numericValue, textValue, errorType);
|
|
36
|
+
}, [
|
|
37
|
+
onError,
|
|
38
|
+
max,
|
|
39
|
+
min,
|
|
40
|
+
parseNumber,
|
|
41
|
+
invalidNumberErrorText,
|
|
42
|
+
numberOutOfRangeErrorText,
|
|
43
|
+
maxDecimalPlaces,
|
|
44
|
+
maxDecimalPlacesErrorText,
|
|
45
|
+
]);
|
|
46
|
+
const handleErrorEnd = useCallback((numericValue, textValue) => {
|
|
47
|
+
setNumberError(undefined);
|
|
48
|
+
onErrorEnd?.(numericValue, textValue);
|
|
49
|
+
}, [onErrorEnd]);
|
|
50
|
+
return (_jsxs(FormControl, { fullWidth: fullWidth, ref: ref, error: error || !!numberError, ...other, children: [label && (_jsx(FormLabel, { htmlFor: id, id: inputLabelId, children: label })), (helperText || numberError) && (_jsx(FormHelperText, { id: helperTextId, ...FormHelperTextProps, children: numberError ?? helperText })), _jsx(FormNumberInput, { "aria-describedby": helperTextId, autoComplete: autoComplete,
|
|
51
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
52
|
+
autoFocus: autoFocus, defaultValue: defaultValue, fullWidth: fullWidth, name: name, value: value, id: id, inputRef: inputRef, onBlur: onBlur, onChange: onChange, onFocus: onFocus, placeholder: placeholder, inputProps: inputProps, error: error, onError: handleError, onErrorEnd: handleErrorEnd, min: min, max: max, allowUndefined: allowUndefined, maxDecimalPlaces: maxDecimalPlaces, correctOnBlur: correctOnBlur, ...InputProps })] }));
|
|
53
|
+
});
|
|
54
|
+
function getNumberErrorText(val, min, max) {
|
|
55
|
+
const hasMax = typeof max === "number" && !isNaN(max);
|
|
56
|
+
const hasMin = typeof min === "number" && !isNaN(min);
|
|
57
|
+
const isBelow = hasMin && val < min;
|
|
58
|
+
const isAbove = hasMax && val > max;
|
|
59
|
+
if (isNaN(val) || (!isAbove && !isBelow)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
else if (hasMin && hasMax) {
|
|
63
|
+
return `Value must be between ${min} and ${max}.`;
|
|
64
|
+
}
|
|
65
|
+
else if (hasMin) {
|
|
66
|
+
return `Value must be greater than ${min}.`;
|
|
67
|
+
}
|
|
68
|
+
else if (hasMax) {
|
|
69
|
+
return `Value must be less than ${max}.`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export default FormLabelNumberField;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type SliderProps } from "@mui/material";
|
|
2
|
+
import type { TextFieldProps } from "@mui/material/TextField";
|
|
3
|
+
import { type FC } from "react";
|
|
4
|
+
import type { FormControlProps } from "../FormControl/index.js";
|
|
5
|
+
import { type ErrorType, type NumberInputProps } from "../NumberInput/index.js";
|
|
6
|
+
export type FormLabelSliderFieldProps = Omit<TextFieldProps, "variant" | "Value" | "onChange" | "defaultValue" | "type" | "select" | "multiline" | "minRows" | "maxRows" | "SelectProps" | "rows" | "InputProps" | "inputProps" | "onError"> & Pick<FormControlProps, "inlineHelpContent" | "inlineHelpUrl"> & {
|
|
7
|
+
value?: number;
|
|
8
|
+
onChange?: (value: number) => void;
|
|
9
|
+
SliderProps?: SliderProps;
|
|
10
|
+
NumberInputComponent?: FC<NumberInputProps>;
|
|
11
|
+
InputProps?: Omit<NumberInputProps, "allowUndefined">;
|
|
12
|
+
inputProps?: NumberInputProps["inputProps"];
|
|
13
|
+
defaultValue?: number;
|
|
14
|
+
/**
|
|
15
|
+
* A label to display as helpText when the number input value is out of
|
|
16
|
+
* range, relative to the min and max properties.
|
|
17
|
+
*/
|
|
18
|
+
numberOutOfRangeErrorText?: string;
|
|
19
|
+
/**
|
|
20
|
+
* A label to display as helpText when the number input value is invalid
|
|
21
|
+
* (ie. NaN).
|
|
22
|
+
*/
|
|
23
|
+
invalidNumberErrorText?: string;
|
|
24
|
+
/**
|
|
25
|
+
* A label to display as helpText when the number input value has more
|
|
26
|
+
* than the configured maxDecimalPlaces.
|
|
27
|
+
*/
|
|
28
|
+
maxDecimalPlacesErrorText?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Executed when the input moves into an error state.
|
|
31
|
+
*/
|
|
32
|
+
onError?: (value: number, textValue: string, type: ErrorType) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Executed when the input moves out of an error state.
|
|
35
|
+
*/
|
|
36
|
+
onErrorEnd?: (value: number, textValue: string) => void;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Like TextField, but uses a FormLabel instead of an InputLabel. Also allows
|
|
40
|
+
* for inline help.
|
|
41
|
+
*/
|
|
42
|
+
declare const FormLabelSliderField: import("react").ForwardRefExoticComponent<Pick<FormLabelSliderFieldProps, "error" | "id" | "label" | "slot" | "style" | "title" | "results" | "size" | "role" | "resource" | "key" | "value" | "className" | "classes" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "lang" | "nonce" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "about" | "datatype" | "inlist" | "prefix" | "property" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "children" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "disabled" | "sx" | "margin" | "fullWidth" | "autoFocus" | "name" | "autoComplete" | "inputProps" | "inputRef" | "required" | "hiddenLabel" | "focused" | "InputProps" | "FormHelperTextProps" | "helperText" | "InputLabelProps" | "inlineHelpContent" | "inlineHelpUrl" | "onErrorEnd" | "NumberInputComponent" | "numberOutOfRangeErrorText" | "invalidNumberErrorText" | "maxDecimalPlacesErrorText" | "SliderProps"> & import("react").RefAttributes<HTMLDivElement>>;
|
|
43
|
+
export default FormLabelSliderField;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Slider, useTheme } from "@mui/material";
|
|
3
|
+
import { Stack } from "@mui/material";
|
|
4
|
+
import { forwardRef, useCallback, useContext, useRef, useState } from "react";
|
|
5
|
+
import FormControl from "../FormControl/index.js";
|
|
6
|
+
import FormHelperText from "../FormHelperText/index.js";
|
|
7
|
+
import FormLabel from "../FormLabel/index.js";
|
|
8
|
+
import { NumberFormatContext } from "../NumberFormatContext/NumberFormatContext.js";
|
|
9
|
+
import NumberInput, { DEFAULT_DECIMAL_PLACES, } from "../NumberInput/index.js";
|
|
10
|
+
import { useId } from "../utils/react.js";
|
|
11
|
+
/**
|
|
12
|
+
* Like TextField, but uses a FormLabel instead of an InputLabel. Also allows
|
|
13
|
+
* for inline help.
|
|
14
|
+
*/
|
|
15
|
+
const FormLabelSliderField = forwardRef(function FormLabelSliderField(props, ref) {
|
|
16
|
+
const { autoComplete, autoFocus = false, defaultValue, disabled, FormHelperTextProps, fullWidth = false, helperText, id: idProp, InputLabelProps, inputProps, InputProps, inputRef, label, name, onBlur, onChange, onFocus, onKeyDown, onMouseUp, placeholder, value, SliderProps, numberOutOfRangeErrorText, invalidNumberErrorText, onError, onErrorEnd, error, maxDecimalPlacesErrorText, NumberInputComponent, ...other } = props;
|
|
17
|
+
const inputId = `${useId(idProp)}-input`;
|
|
18
|
+
const sliderId = `${useId(idProp)}-slider`;
|
|
19
|
+
const helperTextId = helperText && inputId ? `${inputId}-helper-text` : undefined;
|
|
20
|
+
const inputLabelId = label && inputId ? `${inputId}-label` : undefined;
|
|
21
|
+
const inputReference = useRef();
|
|
22
|
+
const { typography } = useTheme();
|
|
23
|
+
const handleSliderChange = useCallback((event, value) => {
|
|
24
|
+
onChange?.(value);
|
|
25
|
+
}, [onChange]);
|
|
26
|
+
const max = SliderProps?.max ?? InputProps?.max ?? 100;
|
|
27
|
+
const min = SliderProps?.min ?? InputProps?.min ?? 0;
|
|
28
|
+
const { parseNumber } = useContext(NumberFormatContext);
|
|
29
|
+
const [numberError, setNumberError] = useState();
|
|
30
|
+
const maxDecimalPlaces = InputProps?.maxDecimalPlaces ?? DEFAULT_DECIMAL_PLACES;
|
|
31
|
+
const handleError = useCallback((numericValue, textValue, errorType) => {
|
|
32
|
+
if (errorType === "out-of-range") {
|
|
33
|
+
setNumberError(numberOutOfRangeErrorText ??
|
|
34
|
+
getNumberErrorText(parseNumber(textValue), min, max));
|
|
35
|
+
}
|
|
36
|
+
else if (errorType === "NaN") {
|
|
37
|
+
setNumberError(invalidNumberErrorText ?? "Value must be a valid number.");
|
|
38
|
+
}
|
|
39
|
+
else if (errorType === "max-decimals") {
|
|
40
|
+
setNumberError(maxDecimalPlacesErrorText ??
|
|
41
|
+
`Value must have at most ${maxDecimalPlaces} decimal places.`);
|
|
42
|
+
}
|
|
43
|
+
onError?.(numericValue, textValue, errorType);
|
|
44
|
+
}, [
|
|
45
|
+
onError,
|
|
46
|
+
max,
|
|
47
|
+
min,
|
|
48
|
+
parseNumber,
|
|
49
|
+
invalidNumberErrorText,
|
|
50
|
+
numberOutOfRangeErrorText,
|
|
51
|
+
maxDecimalPlaces,
|
|
52
|
+
maxDecimalPlacesErrorText,
|
|
53
|
+
]);
|
|
54
|
+
const handleErrorEnd = useCallback((numericValue, textValue) => {
|
|
55
|
+
setNumberError(undefined);
|
|
56
|
+
onErrorEnd?.(numericValue, textValue);
|
|
57
|
+
}, [onErrorEnd]);
|
|
58
|
+
const FormNumberInput = NumberInputComponent ?? NumberInput;
|
|
59
|
+
const InputElement = (_jsx(FormNumberInput, { "aria-labelledby": inputLabelId, "aria-describedby": helperTextId, autoComplete: autoComplete,
|
|
60
|
+
// Disabling this linting rule since the intent is to forward it
|
|
61
|
+
// if it exists rather than set it.
|
|
62
|
+
//
|
|
63
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
64
|
+
autoFocus: autoFocus, defaultValue: defaultValue, fullWidth: fullWidth, id: inputId, inputProps: inputProps, inputRef: inputRef ?? inputReference, name: name, onChange: onChange, placeholder: placeholder, value: value, error: error, onError: handleError, onErrorEnd: handleErrorEnd, ...{ min, max, ...InputProps }, sx: { maxWidth: typography.pxToRem(74), ...InputProps?.sx } }));
|
|
65
|
+
return (_jsxs(FormControl, { fullWidth: fullWidth, ref: ref, disabled: disabled, error: error || !!numberError, ...other, children: [label && (_jsx(FormLabel, { htmlFor: inputId, id: inputLabelId, children: label })), (helperText || !!numberError) && (_jsx(FormHelperText, { id: helperTextId, ...FormHelperTextProps, children: numberError ?? helperText })), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Slider, { "aria-describedby": helperTextId, "aria-labelledby": inputLabelId, defaultValue: defaultValue, name: name, id: sliderId, value: value, onChange: handleSliderChange, disabled: disabled, ...{ min, max, ...SliderProps } }), InputElement] })] }));
|
|
66
|
+
});
|
|
67
|
+
function getNumberErrorText(val, min, max) {
|
|
68
|
+
const hasMax = typeof max === "number" && !isNaN(max);
|
|
69
|
+
const hasMin = typeof min === "number" && !isNaN(min);
|
|
70
|
+
const isBelow = hasMin && val < min;
|
|
71
|
+
const isAbove = hasMax && val > max;
|
|
72
|
+
if (isNaN(val) || (!isAbove && !isBelow)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
else if (hasMin && hasMax) {
|
|
76
|
+
return `Value must be between ${min} and ${max}.`;
|
|
77
|
+
}
|
|
78
|
+
else if (hasMin) {
|
|
79
|
+
return `Value must be greater than ${min}.`;
|
|
80
|
+
}
|
|
81
|
+
else if (hasMax) {
|
|
82
|
+
return `Value must be less than ${max}.`;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export default FormLabelSliderField;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
export interface NumberFormatContextProps {
|
|
3
|
+
/**
|
|
4
|
+
* Returns a stringified number that may include additional/alternative
|
|
5
|
+
* formatting. By default, this will attempt to directly convert a number to
|
|
6
|
+
* a string.
|
|
7
|
+
*/
|
|
8
|
+
formatNumber: (n: number) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Returns a number parsed from a string that may include
|
|
11
|
+
* additional/alternative formatting. By default, this will attempt to
|
|
12
|
+
* directly convert a string to a number.
|
|
13
|
+
*/
|
|
14
|
+
parseNumber: (s: string) => number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A React context for components that gives access to number formatting
|
|
18
|
+
* utilities.
|
|
19
|
+
*/
|
|
20
|
+
export declare const NumberFormatContext: import("react").Context<NumberFormatContextProps>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
const formatNumber = (n) => {
|
|
3
|
+
return `${n}`;
|
|
4
|
+
};
|
|
5
|
+
const parseNumber = (s) => {
|
|
6
|
+
return +s;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A React context for components that gives access to number formatting
|
|
10
|
+
* utilities.
|
|
11
|
+
*/
|
|
12
|
+
export const NumberFormatContext = createContext({
|
|
13
|
+
formatNumber,
|
|
14
|
+
parseNumber,
|
|
15
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { type InputProps } from "@mui/material/Input";
|
|
3
|
+
export declare const DEFAULT_DECIMAL_PLACES = 16;
|
|
4
|
+
export type ErrorType = "NaN" | "out-of-range" | "max-decimals" | "unknown";
|
|
5
|
+
/**
|
|
6
|
+
* Properties for the `NumberInput` component.
|
|
7
|
+
*/
|
|
8
|
+
export interface NumberInputProps extends Omit<InputProps, "type" | "onChange" | "value" | "onError"> {
|
|
9
|
+
/**
|
|
10
|
+
* Sets the value to show in the input for controlled components. If not
|
|
11
|
+
* specified, the input will be uncontrolled instead.
|
|
12
|
+
*/
|
|
13
|
+
value?: number;
|
|
14
|
+
/**
|
|
15
|
+
* A callback to fire when selected values change.
|
|
16
|
+
*/
|
|
17
|
+
onChange?: (value: number | undefined) => void;
|
|
18
|
+
/**
|
|
19
|
+
* The maximum value possible for the number input.
|
|
20
|
+
*/
|
|
21
|
+
max?: number;
|
|
22
|
+
/**
|
|
23
|
+
* The minimum value possible for the number input.
|
|
24
|
+
*/
|
|
25
|
+
min?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Executed when the input moves into an error state.
|
|
28
|
+
*/
|
|
29
|
+
onError?: (value: number, textValue: string, type: ErrorType) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Executed when the input moves out of an error state.
|
|
32
|
+
*/
|
|
33
|
+
onErrorEnd?: (value: number, textValue: string) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Whether to consider an empty field ("") as valid. When this property is
|
|
36
|
+
* true and the field is set to an empty value, the onChange callback will
|
|
37
|
+
* be executed with an undefined argument value.
|
|
38
|
+
*/
|
|
39
|
+
allowUndefined?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The maximum number of decimal places. Must be a number between 0 and 16.
|
|
42
|
+
* Defaults to 16.
|
|
43
|
+
*/
|
|
44
|
+
maxDecimalPlaces?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Whether or not the input should automatically correct invalid values on
|
|
47
|
+
* blur to the last valid value. Defaults to true.
|
|
48
|
+
*/
|
|
49
|
+
correctOnBlur?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A number input component that leverages the NumberFormatContext for
|
|
53
|
+
* formatting and parsing. Default formatting and parsing can be overridden by
|
|
54
|
+
* wrapping this component in a NumberFormatContext.Provider.
|
|
55
|
+
*/
|
|
56
|
+
declare const NumberInput: import("react").ForwardRefExoticComponent<Pick<NumberInputProps, "error" | "id" | "slot" | "style" | "title" | "results" | "size" | "role" | "resource" | "value" | "components" | "className" | "classes" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "lang" | "nonce" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "about" | "datatype" | "inlist" | "prefix" | "property" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "disabled" | "sx" | "margin" | "fullWidth" | "autoFocus" | "name" | "autoComplete" | "componentsProps" | "disableInjectingGlobalStyles" | "endAdornment" | "inputComponent" | "inputProps" | "inputRef" | "multiline" | "readOnly" | "required" | "renderSuffix" | "rows" | "maxRows" | "minRows" | "slotProps" | "slots" | "startAdornment" | "disableUnderline" | "max" | "min" | "onErrorEnd" | "allowUndefined" | "maxDecimalPlaces" | "correctOnBlur"> & import("react").RefAttributes<unknown>>;
|
|
57
|
+
export default NumberInput;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Input, {} from "@mui/material/Input";
|
|
3
|
+
import { forwardRef, useCallback, useContext, useEffect, useState } from "react";
|
|
4
|
+
import { NumberFormatContext } from "../NumberFormatContext/index.js";
|
|
5
|
+
import { usePrevious } from "../utils/react.js";
|
|
6
|
+
export const DEFAULT_DECIMAL_PLACES = 16;
|
|
7
|
+
/**
|
|
8
|
+
* A number input component that leverages the NumberFormatContext for
|
|
9
|
+
* formatting and parsing. Default formatting and parsing can be overridden by
|
|
10
|
+
* wrapping this component in a NumberFormatContext.Provider.
|
|
11
|
+
*/
|
|
12
|
+
const NumberInput = forwardRef(function NumberInput({ onChange, onBlur, value, defaultValue, max, min, error, onError, onErrorEnd, allowUndefined, correctOnBlur = true, maxDecimalPlaces = DEFAULT_DECIMAL_PLACES, ...props }, ref) {
|
|
13
|
+
const { formatNumber, parseNumber } = useContext(NumberFormatContext);
|
|
14
|
+
const [textValue, setTextValue] = useState(value === undefined ? "" : formatNumber(value));
|
|
15
|
+
const [numericValue, setNumericValue] = useState(value === undefined ? NaN : value);
|
|
16
|
+
const [lastValid, setLastValid] = useState(numericValue);
|
|
17
|
+
const isValidNaN = useCallback((val) => allowUndefined && !val?.trim(), [allowUndefined]);
|
|
18
|
+
const isOutOfRange = useCallback((val) => (typeof min === "number" && val < min) || (typeof max === "number" && val > max), [min, max]);
|
|
19
|
+
const isTooPrecise = useCallback((val) => getDecimalPlaces(val) > maxDecimalPlaces, [maxDecimalPlaces]);
|
|
20
|
+
const invalidError = isValidNaN(textValue)
|
|
21
|
+
? false
|
|
22
|
+
: // If the user begins to type a negative number, do not present an invalid
|
|
23
|
+
// error.
|
|
24
|
+
textValue?.trim() !== "-" && parseNumber(textValue) !== numericValue;
|
|
25
|
+
const previousError = usePrevious(error || invalidError);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!isNaN(numericValue) && !isOutOfRange(numericValue) && !isTooPrecise(numericValue)) {
|
|
28
|
+
setLastValid(numericValue);
|
|
29
|
+
}
|
|
30
|
+
}, [numericValue, isOutOfRange, isTooPrecise]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const e = error || invalidError;
|
|
33
|
+
if (e !== previousError) {
|
|
34
|
+
if (invalidError) {
|
|
35
|
+
const inputVal = parseNumber(textValue);
|
|
36
|
+
let type;
|
|
37
|
+
if (isNaN(numericValue)) {
|
|
38
|
+
type = "NaN";
|
|
39
|
+
}
|
|
40
|
+
else if (isOutOfRange(inputVal)) {
|
|
41
|
+
type = "out-of-range";
|
|
42
|
+
}
|
|
43
|
+
else if (isTooPrecise(inputVal)) {
|
|
44
|
+
type = "max-decimals";
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
type = "unknown";
|
|
48
|
+
}
|
|
49
|
+
onError?.(numericValue, textValue, type);
|
|
50
|
+
}
|
|
51
|
+
else if (error) {
|
|
52
|
+
onError?.(numericValue, textValue, "unknown");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
onErrorEnd?.(numericValue, textValue);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, [
|
|
59
|
+
error,
|
|
60
|
+
previousError,
|
|
61
|
+
numericValue,
|
|
62
|
+
textValue,
|
|
63
|
+
onError,
|
|
64
|
+
onErrorEnd,
|
|
65
|
+
invalidError,
|
|
66
|
+
isOutOfRange,
|
|
67
|
+
isTooPrecise,
|
|
68
|
+
parseNumber,
|
|
69
|
+
]);
|
|
70
|
+
const reformat = useCallback(() => {
|
|
71
|
+
if ((isNaN(numericValue) && !isValidNaN(textValue)) || isOutOfRange(numericValue)) {
|
|
72
|
+
setNumericValue(lastValid);
|
|
73
|
+
setTextValue(formatNumber(lastValid));
|
|
74
|
+
onChange?.(lastValid);
|
|
75
|
+
}
|
|
76
|
+
else if (!isNaN(numericValue)) {
|
|
77
|
+
setTextValue(formatNumber(numericValue));
|
|
78
|
+
}
|
|
79
|
+
}, [formatNumber, isOutOfRange, isValidNaN, lastValid, numericValue, onChange, textValue]);
|
|
80
|
+
const correctValue = useCallback((val) => {
|
|
81
|
+
let newValue = val;
|
|
82
|
+
if (!isNaN(newValue) && typeof newValue === "number") {
|
|
83
|
+
if (typeof max === "number") {
|
|
84
|
+
newValue = Math.min(newValue, max);
|
|
85
|
+
}
|
|
86
|
+
if (typeof min === "number") {
|
|
87
|
+
newValue = Math.max(newValue, min);
|
|
88
|
+
}
|
|
89
|
+
const decimalPlaces = getDecimalPlaces(newValue);
|
|
90
|
+
const maxDecimals = Math.min(Math.max(0, maxDecimalPlaces), DEFAULT_DECIMAL_PLACES);
|
|
91
|
+
if (decimalPlaces > maxDecimals) {
|
|
92
|
+
newValue = +newValue.toFixed(maxDecimals);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return newValue;
|
|
96
|
+
}, [max, min, maxDecimalPlaces]);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (value !== undefined) {
|
|
99
|
+
// For controlled components, only update the text from the value if
|
|
100
|
+
// it's a valid number, and represents a number different from the
|
|
101
|
+
// one in the text box. Otherwise leave it alone so that it's not
|
|
102
|
+
// constantly reformatting as the user is typing.
|
|
103
|
+
if (!isNaN(value) && value !== numericValue) {
|
|
104
|
+
setTextValue(formatNumber(value));
|
|
105
|
+
}
|
|
106
|
+
setNumericValue(correctValue(value));
|
|
107
|
+
}
|
|
108
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- It's intentional to omit numericValue. We only want this to run when the incoming value changes, not when numericValue changes in handleChange().
|
|
109
|
+
}, [value, formatNumber, correctValue]);
|
|
110
|
+
const handleChange = useCallback(event => {
|
|
111
|
+
const value = event.currentTarget.value;
|
|
112
|
+
setTextValue(value);
|
|
113
|
+
if (isValidNaN(value)) {
|
|
114
|
+
setNumericValue(NaN);
|
|
115
|
+
onChange?.(undefined);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const newValue = correctValue(parseNumber(value));
|
|
119
|
+
setNumericValue(newValue);
|
|
120
|
+
onChange?.(newValue);
|
|
121
|
+
}
|
|
122
|
+
}, [correctValue, onChange, parseNumber, isValidNaN]);
|
|
123
|
+
const handleBlur = useCallback((event) => {
|
|
124
|
+
if (correctOnBlur) {
|
|
125
|
+
reformat();
|
|
126
|
+
}
|
|
127
|
+
onBlur?.(event);
|
|
128
|
+
}, [correctOnBlur, reformat, onBlur]);
|
|
129
|
+
return (_jsx(Input, { ref: ref, type: "text", value: textValue, onChange: handleChange, onBlur: handleBlur, defaultValue: typeof defaultValue === "number" ? formatNumber(defaultValue) : undefined, "data-test": "NumberInput-container", error: error || invalidError, ...props }));
|
|
130
|
+
});
|
|
131
|
+
export default NumberInput;
|
|
132
|
+
function getDecimalPlaces(value) {
|
|
133
|
+
const text = value.toString();
|
|
134
|
+
if (text.includes("e-")) {
|
|
135
|
+
const [, exponent] = text.split("e-");
|
|
136
|
+
return parseInt(exponent, 10);
|
|
137
|
+
}
|
|
138
|
+
if (Math.floor(value) !== value) {
|
|
139
|
+
const [, decimals] = text.split(".");
|
|
140
|
+
return decimals.length ?? 0;
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|