@qoretechnologies/reqraft 0.10.60-pr.131.gfabf11e → 0.10.60-pr.131.gfcd7fef
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/dist/components/form/fields/number/Number.d.ts +0 -7
- package/dist/components/form/fields/number/Number.d.ts.map +1 -1
- package/dist/components/form/fields/number/Number.js +17 -36
- package/dist/components/form/fields/number/Number.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +1 -1
- package/src/components/form/fields/number/Number.stories.tsx +9 -36
- package/src/components/form/fields/number/Number.tsx +32 -39
|
@@ -18,13 +18,6 @@ export interface INumberFormFieldProps extends Omit<IReqoreInputProps, 'value' |
|
|
|
18
18
|
min_value?: number;
|
|
19
19
|
max_value?: number;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* What a typed text is: the number it writes, or - when it writes none - the text as typed. A field of a
|
|
23
|
-
* number type is typed freely (David's review of qorus#646): a template (`$local:x`) or an expression
|
|
24
|
-
* (`@qty * 2`) is taken up by the field around it, and anything else is kept as typed for the form to flag
|
|
25
|
-
* as not a number. `parseInt` cut `12abc` to 12 without a word.
|
|
26
|
-
*/
|
|
27
|
-
export declare const typedNumber: (text: string, type: 'int' | 'float') => number | string;
|
|
28
21
|
export declare const NumberFormField: ({ onChange, autoFocus, type, value, templates, min_value, max_value, ...rest }: INumberFormFieldProps) => import("react/jsx-runtime").JSX.Element;
|
|
29
22
|
export default NumberFormField;
|
|
30
23
|
//# sourceMappingURL=Number.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Number.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/fields/number/Number.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gDAAgD,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mDAAmD,CAAC;AAQzF,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IACnG,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAC5C,mFAAmF;IACnF,SAAS,CAAC,EAAE,oBAAoB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"Number.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/fields/number/Number.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gDAAgD,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mDAAmD,CAAC;AAQzF,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IACnG,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAC5C,mFAAmF;IACnF,SAAS,CAAC,EAAE,oBAAoB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,eAAe,mFASzB,qBAAqB,4CAgHvB,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -22,7 +22,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
22
22
|
return t;
|
|
23
23
|
};
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.NumberFormField =
|
|
25
|
+
exports.NumberFormField = void 0;
|
|
26
26
|
var jsx_runtime_1 = require("react/jsx-runtime");
|
|
27
27
|
var reqore_1 = require("@qoretechnologies/reqore");
|
|
28
28
|
var react_1 = require("react");
|
|
@@ -31,27 +31,9 @@ var emittedValues_1 = require("../emittedValues");
|
|
|
31
31
|
// Const-only usage at render time — the cycle (Number → TemplateField → Number)
|
|
32
32
|
// is safe the same way Field → AutoFormField → Field is.
|
|
33
33
|
var TemplateField_1 = require("../template/TemplateField");
|
|
34
|
-
/** A whole number as typed: an optional sign and digits. */
|
|
35
|
-
var WHOLE_NUMBER = /^[-+]?\d+$/;
|
|
36
|
-
/** A decimal number as typed: an optional sign, digits with a point, and an exponent. */
|
|
37
|
-
var DECIMAL_NUMBER = /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/;
|
|
38
|
-
/**
|
|
39
|
-
* What a typed text is: the number it writes, or - when it writes none - the text as typed. A field of a
|
|
40
|
-
* number type is typed freely (David's review of qorus#646): a template (`$local:x`) or an expression
|
|
41
|
-
* (`@qty * 2`) is taken up by the field around it, and anything else is kept as typed for the form to flag
|
|
42
|
-
* as not a number. `parseInt` cut `12abc` to 12 without a word.
|
|
43
|
-
*/
|
|
44
|
-
var typedNumber = function (text, type) {
|
|
45
|
-
var trimmed = text.trim();
|
|
46
|
-
return (type === 'int' ? WHOLE_NUMBER : DECIMAL_NUMBER).test(trimmed) ? Number(trimmed) : text;
|
|
47
|
-
};
|
|
48
|
-
exports.typedNumber = typedNumber;
|
|
49
34
|
var NumberFormField = function (_a) {
|
|
50
35
|
var onChange = _a.onChange, autoFocus = _a.autoFocus, _b = _a.type, type = _b === void 0 ? 'int' : _b, value = _a.value, templates = _a.templates, min_value = _a.min_value, max_value = _a.max_value, rest = __rest(_a, ["onChange", "autoFocus", "type", "value", "templates", "min_value", "max_value"]);
|
|
51
|
-
|
|
52
|
-
// none. Showing the number instead dropped a decimal point mid-typing: `10.` is 10, and `10.9` became `109`.
|
|
53
|
-
var _c = (0, react_1.useState)(value === undefined || value === null ? '' : String(value)), text = _c[0], setText = _c[1];
|
|
54
|
-
var localValue = text === '' ? '' : (0, exports.typedNumber)(text, type);
|
|
36
|
+
var _c = (0, react_1.useState)(value !== null && value !== void 0 ? value : ''), localValue = _c[0], setLocalValue = _c[1];
|
|
55
37
|
// The parent's echo of an emit still in flight is not a change: copying it
|
|
56
38
|
// over the local value lost what was typed since. See `EmittedValues`.
|
|
57
39
|
var emitted = (0, emittedValues_1.useEmittedValues)();
|
|
@@ -60,7 +42,7 @@ var NumberFormField = function (_a) {
|
|
|
60
42
|
return;
|
|
61
43
|
}
|
|
62
44
|
if (value !== localValue) {
|
|
63
|
-
|
|
45
|
+
setLocalValue(value !== null && value !== void 0 ? value : '');
|
|
64
46
|
}
|
|
65
47
|
}, [value]);
|
|
66
48
|
(0, react_use_1.useDebounce)(function () {
|
|
@@ -70,13 +52,18 @@ var NumberFormField = function (_a) {
|
|
|
70
52
|
}
|
|
71
53
|
}, 100, [localValue, onChange]);
|
|
72
54
|
var handleChange = (0, react_1.useCallback)(function (rawValue) {
|
|
73
|
-
|
|
74
|
-
|
|
55
|
+
if (rawValue === '' || rawValue === '-' || rawValue === '-.') {
|
|
56
|
+
setLocalValue(rawValue);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
var parsed = type === 'int' ? parseInt(rawValue, 10) : parseFloat(rawValue);
|
|
60
|
+
setLocalValue(isNaN(parsed) ? rawValue : parsed);
|
|
61
|
+
}, [type]);
|
|
75
62
|
var handleInputChange = (0, react_1.useCallback)(function (event) {
|
|
76
63
|
handleChange(event.target.value);
|
|
77
64
|
}, [handleChange]);
|
|
78
65
|
var handleResetClick = (0, react_1.useCallback)(function () {
|
|
79
|
-
|
|
66
|
+
setLocalValue(0);
|
|
80
67
|
emitted.record(0);
|
|
81
68
|
onChange === null || onChange === void 0 ? void 0 : onChange(0);
|
|
82
69
|
}, [onChange]);
|
|
@@ -88,22 +75,16 @@ var NumberFormField = function (_a) {
|
|
|
88
75
|
}
|
|
89
76
|
: undefined;
|
|
90
77
|
}, [autoFocus]);
|
|
91
|
-
var handleItemSelect = (0, react_1.useCallback)(function (item) { return
|
|
92
|
-
// The schema's bounds
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
void max_value;
|
|
78
|
+
var handleItemSelect = (0, react_1.useCallback)(function (item) { return setLocalValue(item.value); }, []);
|
|
79
|
+
// The schema's bounds under the names the input uses. Omitted entirely when the schema
|
|
80
|
+
// declares none, so an undeclared bound stays undeclared rather than becoming `min={0}`.
|
|
81
|
+
var bounds = (0, react_1.useMemo)(function () { return (__assign(__assign({}, (typeof min_value === 'number' ? { min: min_value } : {})), (typeof max_value === 'number' ? { max: max_value } : {}))); }, [min_value, max_value]);
|
|
96
82
|
// IDE `NumberField` parity: with templates the input is wrapped in a
|
|
97
83
|
// focus-opened dropdown of the template values.
|
|
98
84
|
if (templates === null || templates === void 0 ? void 0 : templates.items) {
|
|
99
|
-
return ((0, jsx_runtime_1.jsx)(reqore_1.ReqoreDropdown, __assign({}, rest, { component: reqore_1.ReqoreInput, fluid: true, icon: 'MoneyDollarCircleLine', items: templates === null || templates === void 0 ? void 0 : templates.items, filterable: true, value:
|
|
100
|
-
// typed freely: a number input drops what is not part of a number, so a template or an expression
|
|
101
|
-
// could not be typed; the keyboard is still a numeric one
|
|
102
|
-
type: 'text', inputMode: type === 'int' ? 'numeric' : 'decimal', onClearClick: handleResetClick, focusRules: focusRules }, TemplateField_1.TemplatesListProps)));
|
|
85
|
+
return ((0, jsx_runtime_1.jsx)(reqore_1.ReqoreDropdown, __assign({}, rest, { component: reqore_1.ReqoreInput, fluid: true, icon: 'MoneyDollarCircleLine', items: templates === null || templates === void 0 ? void 0 : templates.items, filterable: true, value: localValue, onItemSelect: handleItemSelect, onChange: handleInputChange, type: 'number', step: type === 'int' ? 1 : 0.1 }, bounds, { onClearClick: handleResetClick, focusRules: focusRules }, TemplateField_1.TemplatesListProps)));
|
|
103
86
|
}
|
|
104
|
-
return ((0, jsx_runtime_1.jsx)(reqore_1.ReqoreInput, __assign({ fluid: true, icon: 'MoneyDollarCircleLine', value:
|
|
105
|
-
// typed freely, on a numeric keyboard (see above)
|
|
106
|
-
type: 'text', inputMode: type === 'int' ? 'numeric' : 'decimal', onClearClick: handleResetClick, focusRules: focusRules }, rest)));
|
|
87
|
+
return ((0, jsx_runtime_1.jsx)(reqore_1.ReqoreInput, __assign({ fluid: true, icon: 'MoneyDollarCircleLine', value: localValue, onChange: handleInputChange, type: 'number', step: type === 'int' ? 1 : 0.1, onClearClick: handleResetClick, focusRules: focusRules }, rest, bounds)));
|
|
107
88
|
};
|
|
108
89
|
exports.NumberFormField = NumberFormField;
|
|
109
90
|
exports.default = exports.NumberFormField;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Number.js","sourceRoot":"","sources":["../../../../../src/components/form/fields/number/Number.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,mDAAuE;AAGvE,+BAA+E;AAC/E,uCAAwC;AACxC,kDAAoD;AACpD,gFAAgF;AAChF,yDAAyD;AACzD,2DAA+D;
|
|
1
|
+
{"version":3,"file":"Number.js","sourceRoot":"","sources":["../../../../../src/components/form/fields/number/Number.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qoretechnologies/reqraft",
|
|
3
|
-
"version": "0.10.60-pr.131.
|
|
3
|
+
"version": "0.10.60-pr.131.gfcd7fef",
|
|
4
4
|
"description": "ReQraft is a collection of React components and hooks that are used across Qore Technologies' products made using the ReQore component library from Qore.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -575,7 +575,7 @@ export const Timeout: Story = {
|
|
|
575
575
|
|
|
576
576
|
const input = await canvas.findByDisplayValue('45');
|
|
577
577
|
await expect(input).toBeInTheDocument();
|
|
578
|
-
await expect(input).toHaveAttribute('
|
|
578
|
+
await expect(input).toHaveAttribute('type', 'number');
|
|
579
579
|
await expect(canvas.getByText('seconds')).toBeInTheDocument();
|
|
580
580
|
await expect(canvas.queryByText('Unknown type!')).not.toBeInTheDocument();
|
|
581
581
|
},
|
|
@@ -51,14 +51,12 @@ export const Integer: Story = {
|
|
|
51
51
|
const input = canvas.getByLabelText('Number');
|
|
52
52
|
|
|
53
53
|
await expect(input).toBeInTheDocument();
|
|
54
|
-
await expect(input).toHaveValue(
|
|
55
|
-
|
|
56
|
-
await expect(input).toHaveAttribute('type', 'text');
|
|
57
|
-
await expect(input).toHaveAttribute('inputmode', 'numeric');
|
|
54
|
+
await expect(input).toHaveValue(42);
|
|
55
|
+
await expect(input).toHaveAttribute('type', 'number');
|
|
58
56
|
|
|
59
57
|
await userEvent.clear(input);
|
|
60
58
|
await userEvent.type(input, '10');
|
|
61
|
-
await expect(input).toHaveValue(
|
|
59
|
+
await expect(input).toHaveValue(10);
|
|
62
60
|
await waitFor(() => expect(args.onChange).toHaveBeenLastCalledWith(10), { timeout: 500 });
|
|
63
61
|
},
|
|
64
62
|
};
|
|
@@ -72,7 +70,7 @@ export const Float: Story = {
|
|
|
72
70
|
docs: {
|
|
73
71
|
description: {
|
|
74
72
|
story:
|
|
75
|
-
'Renders the Number field in float mode with 3.14 pre-populated — the input
|
|
73
|
+
'Renders the Number field in float mode with 3.14 pre-populated — the input uses a 0.1 step and accepts decimals.',
|
|
76
74
|
},
|
|
77
75
|
},
|
|
78
76
|
},
|
|
@@ -80,12 +78,12 @@ export const Float: Story = {
|
|
|
80
78
|
const canvas = within(canvasElement);
|
|
81
79
|
const input = canvas.getByLabelText('Number');
|
|
82
80
|
|
|
83
|
-
await expect(input).toHaveValue(
|
|
84
|
-
await expect(input).toHaveAttribute('
|
|
81
|
+
await expect(input).toHaveValue(3.14);
|
|
82
|
+
await expect(input).toHaveAttribute('step', '0.1');
|
|
85
83
|
|
|
86
84
|
await userEvent.clear(input);
|
|
87
85
|
await userEvent.type(input, '10.9');
|
|
88
|
-
await expect(input).toHaveValue(
|
|
86
|
+
await expect(input).toHaveValue(10.9);
|
|
89
87
|
await waitFor(() => expect(args.onChange).toHaveBeenLastCalledWith(10.9), { timeout: 500 });
|
|
90
88
|
},
|
|
91
89
|
};
|
|
@@ -106,7 +104,7 @@ export const Empty: Story = {
|
|
|
106
104
|
const input = canvas.getByLabelText('Number');
|
|
107
105
|
|
|
108
106
|
await expect(input).toBeInTheDocument();
|
|
109
|
-
await expect(input).toHaveValue(
|
|
107
|
+
await expect(input).toHaveValue(null);
|
|
110
108
|
},
|
|
111
109
|
};
|
|
112
110
|
|
|
@@ -129,31 +127,6 @@ export const Disabled: Story = {
|
|
|
129
127
|
const input = canvas.getByLabelText('Number');
|
|
130
128
|
|
|
131
129
|
await expect(input).toBeDisabled();
|
|
132
|
-
await expect(input).toHaveValue(
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export const TypedFreely: Story = {
|
|
137
|
-
args: {
|
|
138
|
-
value: 7,
|
|
139
|
-
type: 'int',
|
|
140
|
-
},
|
|
141
|
-
parameters: {
|
|
142
|
-
docs: {
|
|
143
|
-
description: {
|
|
144
|
-
story:
|
|
145
|
-
'Renders the Number field in integer mode after typing what is not a number: 12abc stays as typed (the form flags it, it is not cut to 12), and a template ($local:quantity) or an expression (@qty * 2) is passed on as typed for the field around it to take up.',
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
async play({ args, canvasElement }) {
|
|
150
|
-
const canvas = within(canvasElement);
|
|
151
|
-
const input = canvas.getByLabelText('Number');
|
|
152
|
-
for (const text of ['12abc', '@qty * 2', '$local:quantity']) {
|
|
153
|
-
await userEvent.clear(input);
|
|
154
|
-
await userEvent.type(input, text);
|
|
155
|
-
await expect(input).toHaveValue(text);
|
|
156
|
-
await waitFor(() => expect(args.onChange).toHaveBeenLastCalledWith(text), { timeout: 500 });
|
|
157
|
-
}
|
|
130
|
+
await expect(input).toHaveValue(99);
|
|
158
131
|
},
|
|
159
132
|
};
|
|
@@ -27,22 +27,6 @@ export interface INumberFormFieldProps extends Omit<IReqoreInputProps, 'value' |
|
|
|
27
27
|
max_value?: number;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** A whole number as typed: an optional sign and digits. */
|
|
31
|
-
const WHOLE_NUMBER = /^[-+]?\d+$/;
|
|
32
|
-
/** A decimal number as typed: an optional sign, digits with a point, and an exponent. */
|
|
33
|
-
const DECIMAL_NUMBER = /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* What a typed text is: the number it writes, or - when it writes none - the text as typed. A field of a
|
|
37
|
-
* number type is typed freely (David's review of qorus#646): a template (`$local:x`) or an expression
|
|
38
|
-
* (`@qty * 2`) is taken up by the field around it, and anything else is kept as typed for the form to flag
|
|
39
|
-
* as not a number. `parseInt` cut `12abc` to 12 without a word.
|
|
40
|
-
*/
|
|
41
|
-
export const typedNumber = (text: string, type: 'int' | 'float'): number | string => {
|
|
42
|
-
const trimmed = text.trim();
|
|
43
|
-
return (type === 'int' ? WHOLE_NUMBER : DECIMAL_NUMBER).test(trimmed) ? Number(trimmed) : text;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
30
|
export const NumberFormField = ({
|
|
47
31
|
onChange,
|
|
48
32
|
autoFocus,
|
|
@@ -53,10 +37,7 @@ export const NumberFormField = ({
|
|
|
53
37
|
max_value,
|
|
54
38
|
...rest
|
|
55
39
|
}: INumberFormFieldProps) => {
|
|
56
|
-
|
|
57
|
-
// none. Showing the number instead dropped a decimal point mid-typing: `10.` is 10, and `10.9` became `109`.
|
|
58
|
-
const [text, setText] = useState<string>(value === undefined || value === null ? '' : String(value));
|
|
59
|
-
const localValue = text === '' ? '' : typedNumber(text, type);
|
|
40
|
+
const [localValue, setLocalValue] = useState<number | string>(value ?? '');
|
|
60
41
|
// The parent's echo of an emit still in flight is not a change: copying it
|
|
61
42
|
// over the local value lost what was typed since. See `EmittedValues`.
|
|
62
43
|
const emitted = useEmittedValues<number | string | undefined>();
|
|
@@ -66,7 +47,7 @@ export const NumberFormField = ({
|
|
|
66
47
|
return;
|
|
67
48
|
}
|
|
68
49
|
if (value !== localValue) {
|
|
69
|
-
|
|
50
|
+
setLocalValue(value ?? '');
|
|
70
51
|
}
|
|
71
52
|
}, [value]);
|
|
72
53
|
|
|
@@ -81,9 +62,17 @@ export const NumberFormField = ({
|
|
|
81
62
|
[localValue, onChange]
|
|
82
63
|
);
|
|
83
64
|
|
|
84
|
-
const handleChange = useCallback(
|
|
85
|
-
|
|
86
|
-
|
|
65
|
+
const handleChange = useCallback(
|
|
66
|
+
(rawValue: number | string): void => {
|
|
67
|
+
if (rawValue === '' || rawValue === '-' || rawValue === '-.') {
|
|
68
|
+
setLocalValue(rawValue);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const parsed = type === 'int' ? parseInt(rawValue as string, 10) : parseFloat(rawValue as string);
|
|
72
|
+
setLocalValue(isNaN(parsed) ? rawValue : parsed);
|
|
73
|
+
},
|
|
74
|
+
[type]
|
|
75
|
+
);
|
|
87
76
|
|
|
88
77
|
const handleInputChange = useCallback(
|
|
89
78
|
(event: ChangeEvent<HTMLInputElement>): void => {
|
|
@@ -93,7 +82,7 @@ export const NumberFormField = ({
|
|
|
93
82
|
);
|
|
94
83
|
|
|
95
84
|
const handleResetClick = useCallback((): void => {
|
|
96
|
-
|
|
85
|
+
setLocalValue(0);
|
|
97
86
|
emitted.record(0);
|
|
98
87
|
onChange?.(0);
|
|
99
88
|
}, [onChange]);
|
|
@@ -109,12 +98,17 @@ export const NumberFormField = ({
|
|
|
109
98
|
[autoFocus]
|
|
110
99
|
);
|
|
111
100
|
|
|
112
|
-
const handleItemSelect = useCallback((item) =>
|
|
101
|
+
const handleItemSelect = useCallback((item) => setLocalValue(item.value), []);
|
|
113
102
|
|
|
114
|
-
// The schema's bounds
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
103
|
+
// The schema's bounds under the names the input uses. Omitted entirely when the schema
|
|
104
|
+
// declares none, so an undeclared bound stays undeclared rather than becoming `min={0}`.
|
|
105
|
+
const bounds = useMemo(
|
|
106
|
+
() => ({
|
|
107
|
+
...(typeof min_value === 'number' ? { min: min_value } : {}),
|
|
108
|
+
...(typeof max_value === 'number' ? { max: max_value } : {}),
|
|
109
|
+
}),
|
|
110
|
+
[min_value, max_value]
|
|
111
|
+
);
|
|
118
112
|
|
|
119
113
|
// IDE `NumberField` parity: with templates the input is wrapped in a
|
|
120
114
|
// focus-opened dropdown of the template values.
|
|
@@ -127,13 +121,12 @@ export const NumberFormField = ({
|
|
|
127
121
|
icon='MoneyDollarCircleLine'
|
|
128
122
|
items={templates?.items}
|
|
129
123
|
filterable
|
|
130
|
-
value={
|
|
124
|
+
value={localValue}
|
|
131
125
|
onItemSelect={handleItemSelect}
|
|
132
126
|
onChange={handleInputChange}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
inputMode={type === 'int' ? 'numeric' : 'decimal'}
|
|
127
|
+
type='number'
|
|
128
|
+
step={type === 'int' ? 1 : 0.1}
|
|
129
|
+
{...bounds}
|
|
137
130
|
onClearClick={handleResetClick}
|
|
138
131
|
focusRules={focusRules}
|
|
139
132
|
{...TemplatesListProps}
|
|
@@ -145,14 +138,14 @@ export const NumberFormField = ({
|
|
|
145
138
|
<ReqoreInput
|
|
146
139
|
fluid
|
|
147
140
|
icon='MoneyDollarCircleLine'
|
|
148
|
-
value={
|
|
141
|
+
value={localValue}
|
|
149
142
|
onChange={handleInputChange}
|
|
150
|
-
|
|
151
|
-
type
|
|
152
|
-
inputMode={type === 'int' ? 'numeric' : 'decimal'}
|
|
143
|
+
type='number'
|
|
144
|
+
step={type === 'int' ? 1 : 0.1}
|
|
153
145
|
onClearClick={handleResetClick}
|
|
154
146
|
focusRules={focusRules}
|
|
155
147
|
{...rest}
|
|
148
|
+
{...bounds}
|
|
156
149
|
/>
|
|
157
150
|
);
|
|
158
151
|
};
|