@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
371 lines (370 loc) • 12.7 kB
JavaScript
"use client";
import _extends from "@babel/runtime/helpers/esm/extends";
import React, { useContext, useMemo, useCallback, useEffect, useRef } from 'react';
import { InputMasked, Button } from "../../../../components/index.js";
import { format } from "../../../../components/number-format/NumberUtils.js";
import SharedContext from "../../../../shared/Context.js";
import FieldBlockContext from "../../FieldBlock/FieldBlockContext.js";
import classnames from 'classnames';
import FieldBlock from "../../FieldBlock/index.js";
import { useFieldProps } from "../../hooks/index.js";
import { pickSpacingProps } from "../../../../components/flex/utils.js";
import { clamp } from "../../../../shared/helpers/clamp.js";
import DataContext from "../../DataContext/Context.js";
import * as z from 'zod';
const defaultMinimum = Number.MIN_SAFE_INTEGER;
const defaultMaximum = Number.MAX_SAFE_INTEGER;
function NumberComponent(props) {
var _props$width, _props$innerRef;
const dataContext = useContext(DataContext);
const fieldBlockContext = useContext(FieldBlockContext);
const sharedContext = useContext(SharedContext);
const locale = sharedContext?.locale;
const validateContinuouslyRef = useRef(props?.validateContinuously);
const {
currency,
currencyDisplay,
percent,
mask,
step = 1,
decimalLimit = 12,
allowNegative = true,
disallowLeadingZeroes = false,
prefix: prefixProp,
suffix: suffixProp,
showStepControls
} = props;
const schema = useMemo(() => {
var _props$schema;
return ((_props$schema = props.schema) !== null && _props$schema !== void 0 ? _props$schema : p => {
const formatValidationValue = value => {
const formatOptions = {
locale
};
if (p.currency) {
formatOptions.currency = p.currency;
}
if (p.percent) {
formatOptions.percent = true;
}
if (p.decimalLimit !== undefined) {
formatOptions.decimals = p.decimalLimit;
}
return format(value, formatOptions);
};
return z.number().nullish().superRefine((val, ctx) => {
if (val === null || val === undefined) {
return;
}
if ((p.minimum === undefined || p.minimum < defaultMinimum) && val < defaultMinimum) {
ctx.addIssue({
code: 'too_small',
minimum: defaultMinimum,
type: 'number',
inclusive: true,
message: 'NumberField.errorMinimum',
messageValues: {
minimum: formatValidationValue(defaultMinimum)
},
origin: 'number',
locale
});
}
if ((p.maximum === undefined || p.maximum > defaultMaximum) && val > defaultMaximum) {
ctx.addIssue({
code: 'too_big',
maximum: defaultMaximum,
type: 'number',
inclusive: true,
message: 'NumberField.errorMaximum',
messageValues: {
maximum: formatValidationValue(defaultMaximum)
},
origin: 'number',
locale
});
}
if (p.minimum !== undefined && val < p.minimum) {
ctx.addIssue({
code: 'too_small',
minimum: p.minimum,
type: 'number',
inclusive: true,
message: 'NumberField.errorMinimum',
messageValues: {
minimum: formatValidationValue(p.minimum)
},
origin: 'number',
locale
});
}
if (p.maximum !== undefined && val > p.maximum) {
ctx.addIssue({
code: 'too_big',
maximum: p.maximum,
type: 'number',
inclusive: true,
message: 'NumberField.errorMaximum',
messageValues: {
maximum: formatValidationValue(p.maximum)
},
origin: 'number',
locale
});
}
if (p.exclusiveMinimum !== undefined && val <= p.exclusiveMinimum) {
ctx.addIssue({
code: 'too_small',
minimum: p.exclusiveMinimum,
type: 'number',
inclusive: false,
message: 'NumberField.errorExclusiveMinimum',
messageValues: {
exclusiveMinimum: formatValidationValue(p.exclusiveMinimum)
},
origin: 'number',
exclusiveMinimum: p.exclusiveMinimum,
locale
});
}
if (p.exclusiveMaximum !== undefined && val >= p.exclusiveMaximum) {
ctx.addIssue({
code: 'too_big',
maximum: p.exclusiveMaximum,
type: 'number',
inclusive: false,
message: 'NumberField.errorExclusiveMaximum',
messageValues: {
exclusiveMaximum: formatValidationValue(p.exclusiveMaximum)
},
origin: 'number',
exclusiveMaximum: p.exclusiveMaximum,
locale
});
}
if (p.multipleOf !== undefined && val % p.multipleOf !== 0) {
ctx.addIssue({
code: 'custom',
message: 'NumberField.errorMultipleOf',
messageValues: {
multipleOf: formatValidationValue(p.multipleOf)
},
origin: 'number',
multipleOf: p.multipleOf,
locale
});
}
});
}
);
}, [props.schema, props.minimum, props.maximum, props.exclusiveMinimum, props.exclusiveMaximum, props.multipleOf, props.currency, props.percent, props.decimalLimit, locale]);
const toInput = useCallback(external => {
if (external === undefined || external === null) {
return null;
}
if (typeof external !== 'number' || isNaN(external)) {
return '';
}
return external;
}, []);
const fromInput = useCallback(({
value,
numberValue
}) => {
if (value === '') {
return props.emptyValue;
}
if (allowNegative && Object.is(numberValue, -0)) {
return props.emptyValue;
}
return numberValue;
}, [props.emptyValue, allowNegative]);
const ref = useRef();
const preparedProps = {
valueType: 'number',
validateContinuously: validateContinuouslyRef.current,
...props,
schema,
toInput,
fromInput,
width: (_props$width = props.width) !== null && _props$width !== void 0 ? _props$width : fieldBlockContext?.composition ? 'stretch' : 'medium',
innerRef: (_props$innerRef = props.innerRef) !== null && _props$innerRef !== void 0 ? _props$innerRef : ref
};
const {
id,
name,
className,
innerRef,
inputClassName,
autoComplete,
placeholder,
value,
startWith = null,
minimum = defaultMinimum,
maximum = defaultMaximum,
disabled,
htmlAttributes,
hasError,
size,
width,
align,
handleFocus,
handleBlur,
handleChange,
setDisplayValue
} = useFieldProps(preparedProps);
useEffect(() => {
setDisplayValue(innerRef.current?.value);
}, [innerRef, setDisplayValue, value]);
const {
handleSubmit
} = dataContext !== null && dataContext !== void 0 ? dataContext : {};
const onKeyDownHandler = useCallback(e => {
const {
event
} = e;
if (dataContext?.props?.isolate && event.key === 'Enter') {
handleSubmit();
event.preventDefault?.();
}
if (!showStepControls) {
return;
}
let numberValue = null;
switch (event.key) {
case 'ArrowUp':
numberValue = clamp((value !== null && value !== void 0 ? value : startWith) + step, minimum, maximum);
break;
case 'ArrowDown':
numberValue = clamp((value !== null && value !== void 0 ? value : startWith) - step, minimum, maximum);
break;
}
if (numberValue !== null) {
event.persist();
event.preventDefault();
handleChange({
numberValue
});
}
}, [dataContext?.props?.isolate, handleChange, handleSubmit, maximum, minimum, showStepControls, startWith, step, value]);
const onChangeHandler = useCallback(args => {
handleChange(args);
if (typeof args?.numberValue === 'number') {
if (args.numberValue > defaultMaximum || args.numberValue < defaultMinimum) {
handleBlur();
}
}
}, [handleChange, handleBlur]);
const fieldBlockProps = {
forId: id,
className: classnames("dnb-forms-field-number dnb-input__border--tokens", className),
contentClassName: classnames('dnb-forms-field-number__contents', showStepControls && 'dnb-forms-field-number__contents--has-controls', hasError && 'dnb-input__status--error', disabled && 'dnb-input--disabled'),
width: (width === 'stretch' || fieldBlockContext?.composition) && !showStepControls ? width : undefined,
contentWidth: width !== false ? width : undefined,
...pickSpacingProps(props)
};
const increaseClickHandler = useCallback(() => {
handleChange({
numberValue: clamp((value !== null && value !== void 0 ? value : startWith) + step, minimum, maximum)
});
}, [handleChange, maximum, minimum, startWith, step, value]);
const increaseProps = showStepControls && {
'aria-hidden': true,
className: 'dnb-button--control-after',
variant: 'secondary',
icon: 'add',
size: size || 'small',
tabIndex: -1,
disabled: disabled || value >= maximum,
onClick: increaseClickHandler,
title: sharedContext?.translation.Slider.addTitle?.replace('%s', String(value + step))
};
const decreaseClickHandler = useCallback(() => {
handleChange({
numberValue: clamp((value !== null && value !== void 0 ? value : startWith) - step, minimum, maximum)
});
}, [handleChange, maximum, minimum, startWith, step, value]);
const decreaseProps = showStepControls && {
...increaseProps,
className: 'dnb-button--control-before',
icon: 'subtract',
size: size || 'small',
disabled: disabled || value <= minimum,
onClick: decreaseClickHandler,
title: sharedContext?.translation.Slider.subtractTitle?.replace('%s', String(value - step))
};
const prefix = typeof prefixProp === 'function' ? prefixProp(value) : prefixProp;
const suffix = typeof suffixProp === 'function' ? suffixProp(value) : suffixProp;
const maskProps = useMemo(() => {
const mask_options = {
prefix,
suffix,
decimalLimit,
allowNegative,
disallowLeadingZeroes
};
if (currency) {
return {
as_currency: currency,
mask_options,
currency_mask: {
currencyDisplay,
decimalLimit
}
};
}
if (percent) {
return {
as_percent: percent,
mask_options
};
}
return {
mask,
as_number: mask ? undefined : true,
number_mask: mask ? undefined : mask_options
};
}, [currency, currencyDisplay, decimalLimit, mask, percent, prefix, suffix, allowNegative, disallowLeadingZeroes]);
const ariaParams = showStepControls && {
role: 'spinbutton',
'aria-valuemin': String(minimum),
'aria-valuemax': String(maximum),
'aria-valuenow': String(value),
'aria-valuetext': String(value)
};
const inputProps = {
id,
name,
inner_ref: innerRef,
autoComplete,
className: classnames(`dnb-forms-field-number__input dnb-input--${size}`, inputClassName),
step: showStepControls ? step : undefined,
placeholder,
value,
align: showStepControls ? 'center' : align,
onKeyDown: onKeyDownHandler,
onPaste: handleBlur,
onFocus: handleFocus,
onBlur: handleBlur,
onChange: onChangeHandler,
disabled,
status: hasError ? 'error' : undefined,
stretch: Boolean(width),
...maskProps,
...htmlAttributes,
...ariaParams
};
if (showStepControls) {
return React.createElement(FieldBlock, _extends({}, fieldBlockProps, {
asFieldset: false
}), React.createElement("span", {
className: "dnb-input__border dnb-input__border--root"
}, React.createElement(Button, decreaseProps), React.createElement(InputMasked, inputProps), React.createElement(Button, increaseProps)));
}
return React.createElement(FieldBlock, _extends({}, fieldBlockProps, {
asFieldset: false
}), React.createElement(InputMasked, inputProps));
}
NumberComponent._supportsSpacingProps = true;
export default NumberComponent;
//# sourceMappingURL=Number.js.map