@danske/sapphire-react-lab 0.104.0 → 0.105.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/README.md +2 -1
- package/build/cjs/index.js +729 -234
- package/build/cjs/index.js.map +1 -1
- package/build/esm/Amount/src/Amount.js +76 -0
- package/build/esm/Amount/src/Amount.js.map +1 -0
- package/build/esm/Amount/src/useGroupAmount.js +35 -0
- package/build/esm/Amount/src/useGroupAmount.js.map +1 -0
- package/build/esm/Autocomplete/src/Autocomplete.js +13 -3
- package/build/esm/Autocomplete/src/Autocomplete.js.map +1 -1
- package/build/esm/Breadcrumbs/src/BreadcrumbItemLink.js +1 -1
- package/build/esm/Breadcrumbs/src/BreadcrumbItemLink.js.map +1 -1
- package/build/esm/Filtering/src/FilterDropdown.js +8 -3
- package/build/esm/Filtering/src/FilterDropdown.js.map +1 -1
- package/build/esm/Filtering/src/SearchableSelectFilter.js +2 -1
- package/build/esm/Filtering/src/SearchableSelectFilter.js.map +1 -1
- package/build/esm/NumberField/src/NumberField.js +53 -21
- package/build/esm/NumberField/src/NumberField.js.map +1 -1
- package/build/esm/NumberField/src/StepperButton.js +8 -4
- package/build/esm/NumberField/src/StepperButton.js.map +1 -1
- package/build/esm/NumberField/src/cursorHelpers.js +55 -0
- package/build/esm/NumberField/src/cursorHelpers.js.map +1 -0
- package/build/esm/NumberField/src/formatHelpers.js +81 -0
- package/build/esm/NumberField/src/formatHelpers.js.map +1 -0
- package/build/esm/NumberField/src/keyboardHelpers.js +120 -0
- package/build/esm/NumberField/src/keyboardHelpers.js.map +1 -0
- package/build/esm/NumberField/src/useNumberFieldFormatting.js +94 -0
- package/build/esm/NumberField/src/useNumberFieldFormatting.js.map +1 -0
- package/build/esm/NumberField/src/useSapphireNumberField.js +1 -1
- package/build/esm/NumberField/src/useSapphireNumberField.js.map +1 -1
- package/build/esm/TagGroup/src/Tag.js +31 -23
- package/build/esm/TagGroup/src/Tag.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/index.d.ts +72 -7
- package/package.json +36 -36
|
@@ -4,7 +4,8 @@ import { mergeProps, filterDOMProps } from '@react-aria/utils';
|
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import textFieldStyles from '@danske/sapphire-css/components/textField/textField.module.css';
|
|
6
6
|
import { useButton, Icon } from '@danske/sapphire-react';
|
|
7
|
-
import {
|
|
7
|
+
import { Add, Subtract } from '@danske/sapphire-icons/react';
|
|
8
|
+
import { useFocusRing } from '@react-aria/focus';
|
|
8
9
|
|
|
9
10
|
var __defProp = Object.defineProperty;
|
|
10
11
|
var __defProps = Object.defineProperties;
|
|
@@ -48,15 +49,18 @@ const StepperButton = (_a) => {
|
|
|
48
49
|
const ref = useRef(null);
|
|
49
50
|
const { buttonProps, isPressed } = useButton(props, ref);
|
|
50
51
|
const { hoverProps, isHovered } = useHover({ isDisabled: props.isDisabled });
|
|
51
|
-
|
|
52
|
+
const { focusProps, isFocusVisible } = useFocusRing();
|
|
53
|
+
return /* @__PURE__ */ React.createElement("button", __spreadProps(__spreadValues({}, mergeProps(buttonProps, hoverProps, focusProps, filterDOMProps(props, { global: true }))), {
|
|
54
|
+
tabIndex: 0,
|
|
52
55
|
ref,
|
|
53
56
|
className: clsx(textFieldStyles["sapphire-text-field__stepper-button"], textFieldStyles["js-hover"], {
|
|
54
57
|
[textFieldStyles["is-active"]]: isPressed,
|
|
55
|
-
[textFieldStyles["is-hover"]]: isHovered
|
|
58
|
+
[textFieldStyles["is-hover"]]: isHovered,
|
|
59
|
+
[textFieldStyles["is-focus"]]: isFocusVisible
|
|
56
60
|
})
|
|
57
61
|
}), /* @__PURE__ */ React.createElement(Icon, {
|
|
58
62
|
size: size === "lg" ? "md" : "sm"
|
|
59
|
-
}, variant === "increment" ? /* @__PURE__ */ React.createElement(
|
|
63
|
+
}, variant === "increment" ? /* @__PURE__ */ React.createElement(Add, null) : /* @__PURE__ */ React.createElement(Subtract, null)));
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
export { StepperButton };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StepperButton.js","sources":["../../../../src/NumberField/src/StepperButton.tsx"],"sourcesContent":["import React, { useRef } from 'react';\nimport { AriaButtonProps } from '@react-aria/button';\nimport { useHover } from '@react-aria/interactions';\nimport { filterDOMProps, mergeProps } from '@react-aria/utils';\nimport clsx from 'clsx';\nimport styles from '@danske/sapphire-css/components/textField/textField.module.css';\nimport { Icon, useButton } from '@danske/sapphire-react';\nimport {
|
|
1
|
+
{"version":3,"file":"StepperButton.js","sources":["../../../../src/NumberField/src/StepperButton.tsx"],"sourcesContent":["import React, { useRef } from 'react';\nimport { AriaButtonProps } from '@react-aria/button';\nimport { useHover } from '@react-aria/interactions';\nimport { filterDOMProps, mergeProps } from '@react-aria/utils';\nimport clsx from 'clsx';\nimport styles from '@danske/sapphire-css/components/textField/textField.module.css';\nimport { Icon, useButton } from '@danske/sapphire-react';\nimport { Add, Subtract } from '@danske/sapphire-icons/react';\nimport { useFocusRing } from '@react-aria/focus';\n\n/**\n * @internal\n */\nexport const StepperButton = ({\n variant,\n size,\n ...props\n}: AriaButtonProps & {\n variant: 'increment' | 'decrement';\n size: 'lg' | 'md';\n}): React.JSX.Element => {\n const ref = useRef(null);\n const { buttonProps, isPressed } = useButton(props, ref);\n const { hoverProps, isHovered } = useHover({ isDisabled: props.isDisabled });\n const { focusProps, isFocusVisible } = useFocusRing();\n\n return (\n <button\n {...mergeProps(\n buttonProps,\n hoverProps,\n focusProps,\n filterDOMProps(props, { global: true })\n )}\n tabIndex={0} // tabIndex comes as \"-1\" from the react-aria hook\n ref={ref}\n className={clsx(\n styles['sapphire-text-field__stepper-button'],\n styles['js-hover'],\n {\n [styles['is-active']]: isPressed,\n [styles['is-hover']]: isHovered,\n [styles['is-focus']]: isFocusVisible,\n }\n )}\n >\n <Icon size={size === 'lg' ? 'md' : 'sm'}>\n {variant === 'increment' ? <Add /> : <Subtract />}\n </Icon>\n </button>\n );\n};\n"],"names":["styles"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaa,MAAA,aAAA,GAAgB,CAAC,EAOL,KAAA;AAPK,EAC5B,IAAA,EAAA,GAAA,EAAA,EAAA;AAAA,IAAA,OAAA;AAAA,IACA,IAAA;AAAA,GAF4B,GAAA,EAAA,EAGzB,kBAHyB,EAGzB,EAAA;AAAA,IAFH,SAAA;AAAA,IACA,MAAA;AAAA,GAAA,CAAA,CAAA;AAMA,EAAA,MAAM,MAAM,MAAO,CAAA,IAAA,CAAA,CAAA;AACnB,EAAA,MAAM,EAAE,WAAA,EAAa,SAAc,EAAA,GAAA,SAAA,CAAU,KAAO,EAAA,GAAA,CAAA,CAAA;AACpD,EAAA,MAAM,EAAE,UAAY,EAAA,SAAA,EAAA,GAAc,QAAS,CAAA,EAAE,YAAY,KAAM,CAAA,UAAA,EAAA,CAAA,CAAA;AAC/D,EAAM,MAAA,EAAE,YAAY,cAAmB,EAAA,GAAA,YAAA,EAAA,CAAA;AAEvC,EACE,uBAAA,KAAA,CAAA,aAAA,CAAC,QAAD,EAAA,aAAA,CAAA,cAAA,CAAA,EAAA,EACM,UACF,CAAA,WAAA,EACA,UACA,EAAA,UAAA,EACA,cAAe,CAAA,KAAA,EAAO,EAAE,MAAA,EAAQ,IALpC,EAAA,CAAA,CAAA,CAAA,EAAA;AAAA,IAOE,QAAU,EAAA,CAAA;AAAA,IACV,GAAA;AAAA,IACA,SAAW,EAAA,IAAA,CACTA,eAAO,CAAA,qCAAA,CAAA,EACPA,gBAAO,UACP,CAAA,EAAA;AAAA,MAAA,CACGA,gBAAO,WAAe,CAAA,GAAA,SAAA;AAAA,MAAA,CACtBA,gBAAO,UAAc,CAAA,GAAA,SAAA;AAAA,MAAA,CACrBA,gBAAO,UAAc,CAAA,GAAA,cAAA;AAAA,KAAA,CAAA;AAAA,GAAA,CAAA,sCAIzB,IAAD,EAAA;AAAA,IAAM,IAAA,EAAM,IAAS,KAAA,IAAA,GAAO,IAAO,GAAA,IAAA;AAAA,GAAA,EAChC,YAAY,WAAc,mBAAA,KAAA,CAAA,aAAA,CAAC,GAAD,EAAA,IAAA,CAAA,uCAAW,QAAD,EAAA,IAAA,CAAA,CAAA,CAAA,CAAA;AAAA;;;;"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const isDeleteOperation = (inputType) => {
|
|
2
|
+
return inputType === "deleteContentBackward" || inputType === "deleteContentForward";
|
|
3
|
+
};
|
|
4
|
+
const calculateCursorPosition = (oldValue, newValue, oldCursor, inputType, decimalSeparator, numericValue) => {
|
|
5
|
+
if (!newValue)
|
|
6
|
+
return 0;
|
|
7
|
+
const oldDecimalPos = oldValue.indexOf(decimalSeparator);
|
|
8
|
+
const newDecimalPos = newValue.indexOf(decimalSeparator);
|
|
9
|
+
if (numericValue === 0 && isDeleteOperation(inputType)) {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
const countDigits = (str, start, end) => {
|
|
13
|
+
let count = 0;
|
|
14
|
+
for (let i = start; i < end; i++) {
|
|
15
|
+
if (/[0-9]/.test(str[i]))
|
|
16
|
+
count++;
|
|
17
|
+
}
|
|
18
|
+
return count;
|
|
19
|
+
};
|
|
20
|
+
if (isDeleteOperation(inputType) && oldDecimalPos >= 0 && oldCursor === oldDecimalPos && newDecimalPos >= 0) {
|
|
21
|
+
return newDecimalPos;
|
|
22
|
+
}
|
|
23
|
+
if (oldDecimalPos >= 0 && oldCursor > oldDecimalPos && newDecimalPos >= 0) {
|
|
24
|
+
const digitsBeforeCursor = countDigits(oldValue, oldDecimalPos + 1, oldCursor);
|
|
25
|
+
let digitCount2 = 0;
|
|
26
|
+
for (let i = newDecimalPos + 1; i < newValue.length; i++) {
|
|
27
|
+
if (/[0-9]/.test(newValue[i]) && ++digitCount2 === digitsBeforeCursor) {
|
|
28
|
+
return i + 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return newDecimalPos + 1;
|
|
32
|
+
}
|
|
33
|
+
const oldEnd = oldDecimalPos >= 0 ? oldDecimalPos : oldValue.length;
|
|
34
|
+
const newEnd = newDecimalPos >= 0 ? newDecimalPos : newValue.length;
|
|
35
|
+
const intDigitsBeforeCursor = countDigits(oldValue, 0, Math.min(oldCursor, oldEnd));
|
|
36
|
+
if (intDigitsBeforeCursor === 0) {
|
|
37
|
+
if (isDeleteOperation(inputType)) {
|
|
38
|
+
for (let i = 0; i < newValue.length; i++) {
|
|
39
|
+
if (/[0-9]/.test(newValue[i]))
|
|
40
|
+
return i + 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
let digitCount = 0;
|
|
46
|
+
for (let i = 0; i < newEnd; i++) {
|
|
47
|
+
if (/[0-9]/.test(newValue[i]) && ++digitCount === intDigitsBeforeCursor) {
|
|
48
|
+
return i + 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return newDecimalPos >= 0 ? newDecimalPos : newValue.length;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { calculateCursorPosition, isDeleteOperation };
|
|
55
|
+
//# sourceMappingURL=cursorHelpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursorHelpers.js","sources":["../../../../src/NumberField/src/cursorHelpers.ts"],"sourcesContent":["/**\n * Check if the input type is a delete operation\n */\nexport const isDeleteOperation = (inputType: string): boolean => {\n return (\n inputType === 'deleteContentBackward' ||\n inputType === 'deleteContentForward'\n );\n};\n\n/**\n * Calculate the correct cursor position after formatting is applied.\n *\n * @param oldValue - The unformatted value (what's in input before formatting)\n * @param newValue - The formatted value (what we're replacing it with)\n * @param oldCursor - The cursor position in the unformatted value\n * @param inputType - The input type from InputEvent\n * @param decimalSeparator - The decimal separator character\n * @param numericValue - The parsed numeric value (optional, used for special decimal handling)\n * @returns The new cursor position in the formatted value\n */\nexport const calculateCursorPosition = (\n oldValue: string,\n newValue: string,\n oldCursor: number,\n inputType: string,\n decimalSeparator: string,\n numericValue?: number | null\n): number => {\n // For empty values, position at start\n if (!newValue) return 0;\n\n // Find decimal positions\n const oldDecimalPos = oldValue.indexOf(decimalSeparator);\n const newDecimalPos = newValue.indexOf(decimalSeparator);\n\n // Special handling: if numeric value is 0 and user performed a delete operation\n // Position cursor at the start (before the 0) to allow easy re-typing\n if (numericValue === 0 && isDeleteOperation(inputType)) {\n return 0;\n }\n\n // Count digits before cursor in the appropriate section\n const countDigits = (str: string, start: number, end: number): number => {\n let count = 0;\n for (let i = start; i < end; i++) {\n if (/[0-9]/.test(str[i])) count++;\n }\n return count;\n };\n\n // If user was at decimal position in old value and deleted it, position cursor before decimal in new formatted value\n if (\n isDeleteOperation(inputType) &&\n oldDecimalPos >= 0 &&\n oldCursor === oldDecimalPos &&\n newDecimalPos >= 0\n ) {\n return newDecimalPos;\n }\n\n // Cursor is in decimal part\n if (oldDecimalPos >= 0 && oldCursor > oldDecimalPos && newDecimalPos >= 0) {\n const digitsBeforeCursor = countDigits(\n oldValue,\n oldDecimalPos + 1,\n oldCursor\n );\n let digitCount = 0;\n for (let i = newDecimalPos + 1; i < newValue.length; i++) {\n if (/[0-9]/.test(newValue[i]) && ++digitCount === digitsBeforeCursor) {\n return i + 1;\n }\n }\n return newDecimalPos + 1;\n }\n\n // Cursor is in integer part\n const oldEnd = oldDecimalPos >= 0 ? oldDecimalPos : oldValue.length;\n const newEnd = newDecimalPos >= 0 ? newDecimalPos : newValue.length;\n const intDigitsBeforeCursor = countDigits(\n oldValue,\n 0,\n Math.min(oldCursor, oldEnd)\n );\n\n if (intDigitsBeforeCursor === 0) {\n // Cursor was at start - special handling for delete operations\n if (isDeleteOperation(inputType)) {\n for (let i = 0; i < newValue.length; i++) {\n if (/[0-9]/.test(newValue[i])) return i + 1;\n }\n }\n return 0;\n }\n\n // Find position after same number of integer digits\n let digitCount = 0;\n for (let i = 0; i < newEnd; i++) {\n if (/[0-9]/.test(newValue[i]) && ++digitCount === intDigitsBeforeCursor) {\n return i + 1;\n }\n }\n\n return newDecimalPos >= 0 ? newDecimalPos : newValue.length;\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const getSeparators = (formatter) => {
|
|
2
|
+
var _a, _b, _c, _d, _e;
|
|
3
|
+
const parts = formatter.formatToParts(1000.1);
|
|
4
|
+
const groupSeparator = (_b = (_a = parts.find((p) => p.type === "group")) == null ? void 0 : _a.value) != null ? _b : ",";
|
|
5
|
+
let decimalSeparator = (_c = parts.find((p) => p.type === "decimal")) == null ? void 0 : _c.value;
|
|
6
|
+
if (!decimalSeparator) {
|
|
7
|
+
const locale = formatter.resolvedOptions().locale;
|
|
8
|
+
const plainFormatter = new Intl.NumberFormat(locale);
|
|
9
|
+
decimalSeparator = (_e = (_d = plainFormatter.formatToParts(1.1).find((p) => p.type === "decimal")) == null ? void 0 : _d.value) != null ? _e : ".";
|
|
10
|
+
}
|
|
11
|
+
return { groupSeparator, decimalSeparator };
|
|
12
|
+
};
|
|
13
|
+
const trimSymbol = (value, symbol) => {
|
|
14
|
+
if (!symbol)
|
|
15
|
+
return value;
|
|
16
|
+
if (value.startsWith(symbol)) {
|
|
17
|
+
return value.slice(symbol.length).trimStart();
|
|
18
|
+
} else if (value.endsWith(symbol)) {
|
|
19
|
+
return value.slice(0, -symbol.length).trimEnd();
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
const parseNumericValue = (value, decimalSeparator, formatter) => {
|
|
24
|
+
if (!value || value === "-")
|
|
25
|
+
return null;
|
|
26
|
+
const cleaned = value.replace(new RegExp(`[^0-9${decimalSeparator}\\-]`, "g"), "");
|
|
27
|
+
if (!cleaned)
|
|
28
|
+
return null;
|
|
29
|
+
const hasLeadingMinus = cleaned.startsWith("-");
|
|
30
|
+
const withoutMinus = cleaned.replace(/-/g, "");
|
|
31
|
+
const normalized = hasLeadingMinus ? "-" + withoutMinus : withoutMinus;
|
|
32
|
+
const parts = normalized.split(decimalSeparator);
|
|
33
|
+
const withStandardDecimal = parts.length > 1 ? parts[0] + "." + parts.slice(1).join("") : normalized.replace(decimalSeparator, ".");
|
|
34
|
+
let parsed = parseFloat(withStandardDecimal);
|
|
35
|
+
if (isNaN(parsed))
|
|
36
|
+
return null;
|
|
37
|
+
if ((formatter == null ? void 0 : formatter.resolvedOptions().style) === "percent") {
|
|
38
|
+
parsed /= 100;
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
};
|
|
42
|
+
const formatNumberString = (value, formatter, decimalSeparator, shouldStripSymbols, symbol) => {
|
|
43
|
+
if (!value || value === "-" || value === decimalSeparator) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
const hasTrailingDecimal = value.endsWith(decimalSeparator);
|
|
47
|
+
const numericValue = parseNumericValue(value, decimalSeparator, formatter);
|
|
48
|
+
if (numericValue === null) {
|
|
49
|
+
return value.replace(new RegExp(`[^0-9${decimalSeparator}\\-]`, "g"), "");
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const formatted = formatter.format(numericValue);
|
|
53
|
+
const result = shouldStripSymbols && symbol ? trimSymbol(formatted, symbol) : formatted;
|
|
54
|
+
return hasTrailingDecimal && !result.includes(decimalSeparator) ? result + decimalSeparator : result;
|
|
55
|
+
} catch (_) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const isSymbol = (type) => ["currency", "unit", "percentSign"].includes(type);
|
|
60
|
+
const hasSymbolAt = (parts, start) => {
|
|
61
|
+
const [first, second] = start ? [parts[0], parts[1]] : [parts[parts.length - 1], parts[parts.length - 2]];
|
|
62
|
+
return isSymbol(first == null ? void 0 : first.type) || (first == null ? void 0 : first.type) === "literal" && isSymbol(second == null ? void 0 : second.type);
|
|
63
|
+
};
|
|
64
|
+
const extractSymbol = (formatter, prefix, postfix) => {
|
|
65
|
+
var _a, _b;
|
|
66
|
+
const parts = formatter.formatToParts(1000.1);
|
|
67
|
+
const extractedPrefix = hasSymbolAt(parts, true) ? ((_a = parts.find((part) => isSymbol(part.type))) == null ? void 0 : _a.value) || null : null;
|
|
68
|
+
const extractedPostfix = hasSymbolAt(parts, false) ? ((_b = [...parts].reverse().find((part) => isSymbol(part.type))) == null ? void 0 : _b.value) || null : null;
|
|
69
|
+
return prefix !== void 0 || postfix !== void 0 ? {
|
|
70
|
+
extractedPrefix: prefix,
|
|
71
|
+
extractedPostfix: postfix,
|
|
72
|
+
symbol: extractedPrefix || extractedPostfix || void 0
|
|
73
|
+
} : {
|
|
74
|
+
extractedPrefix,
|
|
75
|
+
extractedPostfix,
|
|
76
|
+
symbol: extractedPrefix || extractedPostfix || void 0
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export { extractSymbol, formatNumberString, getSeparators, parseNumericValue, trimSymbol };
|
|
81
|
+
//# sourceMappingURL=formatHelpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatHelpers.js","sources":["../../../../src/NumberField/src/formatHelpers.ts"],"sourcesContent":["import { ReactNode } from 'react';\n\n/** Extract separators from Intl.NumberFormat */\nexport const getSeparators = (formatter: Intl.NumberFormat) => {\n const parts = formatter.formatToParts(1000.1);\n const groupSeparator = parts.find((p) => p.type === 'group')?.value ?? ',';\n let decimalSeparator = parts.find((p) => p.type === 'decimal')?.value;\n\n // Percentage formatters don't show decimals, so detect from a plain formatter\n if (!decimalSeparator) {\n const locale = formatter.resolvedOptions().locale;\n const plainFormatter = new Intl.NumberFormat(locale);\n decimalSeparator =\n plainFormatter.formatToParts(1.1).find((p) => p.type === 'decimal')\n ?.value ?? '.';\n }\n\n return { groupSeparator, decimalSeparator };\n};\n\n/** Extract only numeric parts (digits, group/decimal separators) from unformatted number */\nexport const trimSymbol = (\n value: string,\n symbol: string | undefined\n): string => {\n if (!symbol) return value;\n if (value.startsWith(symbol)) {\n return value.slice(symbol.length).trimStart();\n } else if (value.endsWith(symbol)) {\n return value.slice(0, -symbol.length).trimEnd();\n }\n return value;\n};\n\n/**\n * Parse numeric value from user input string.\n * Handles edge cases and converts percentage display values to actual values.\n */\nexport const parseNumericValue = (\n value: string,\n decimalSeparator: string,\n formatter?: Intl.NumberFormat\n): number | null => {\n if (!value || value === '-') return null;\n\n // Remove all non-numeric characters except decimal separator and minus\n const cleaned = value.replace(\n new RegExp(`[^0-9${decimalSeparator}\\\\-]`, 'g'),\n ''\n );\n if (!cleaned) return null;\n\n // Normalize minus sign (keep only if at start)\n const hasLeadingMinus = cleaned.startsWith('-');\n const withoutMinus = cleaned.replace(/-/g, '');\n const normalized = hasLeadingMinus ? '-' + withoutMinus : withoutMinus;\n\n // Normalize decimal separator (keep only first occurrence)\n const parts = normalized.split(decimalSeparator);\n const withStandardDecimal =\n parts.length > 1\n ? parts[0] + '.' + parts.slice(1).join('')\n : normalized.replace(decimalSeparator, '.');\n\n let parsed = parseFloat(withStandardDecimal);\n if (isNaN(parsed)) return null;\n\n // Convert percentage display value (25) to actual value (0.25)\n if (formatter?.resolvedOptions().style === 'percent') {\n parsed /= 100;\n }\n\n return parsed;\n};\n\n/**\n * Format user input string with locale-aware separators.\n * Optionally strips currency/unit/percent symbols for display.\n */\nexport const formatNumberString = (\n value: string,\n formatter: Intl.NumberFormat,\n decimalSeparator: string,\n shouldStripSymbols: boolean,\n symbol?: string\n): string => {\n // Allow typing special characters\n if (!value || value === '-' || value === decimalSeparator) {\n return value;\n }\n\n // Preserve trailing decimal separator for ongoing input\n const hasTrailingDecimal = value.endsWith(decimalSeparator);\n\n const numericValue = parseNumericValue(value, decimalSeparator, formatter);\n\n // Fallback: return cleaned value if parsing fails\n if (numericValue === null) {\n return value.replace(new RegExp(`[^0-9${decimalSeparator}\\\\-]`, 'g'), '');\n }\n\n try {\n const formatted = formatter.format(numericValue);\n const result =\n shouldStripSymbols && symbol ? trimSymbol(formatted, symbol) : formatted;\n\n // Re-append trailing decimal separator to allow continued typing\n return hasTrailingDecimal && !result.includes(decimalSeparator)\n ? result + decimalSeparator\n : result;\n } catch (_) {\n return value;\n }\n};\n\n/** Check if part is a symbol that should be extracted */\nconst isSymbol = (type: string) =>\n ['currency', 'unit', 'percentSign'].includes(type);\n\nexport interface SymbolExtractionInfo {\n extractedPrefix: ReactNode;\n extractedPostfix: ReactNode;\n symbol?: string;\n}\n\n/** Check if parts start or end with a symbol */\nconst hasSymbolAt = (\n parts: Intl.NumberFormatPart[],\n start: boolean\n): boolean => {\n const [first, second] = start\n ? [parts[0], parts[1]]\n : [parts[parts.length - 1], parts[parts.length - 2]];\n return (\n isSymbol(first?.type) ||\n (first?.type === 'literal' && isSymbol(second?.type))\n );\n};\n\n/**\n * Extract currency/unit/percent symbols and determine their position.\n *\n * If user has provided custom prefix/postfix, those will be used instead of extracted ones.\n */\nexport const extractSymbol = (\n formatter: Intl.NumberFormat,\n prefix?: ReactNode,\n postfix?: ReactNode\n): SymbolExtractionInfo => {\n const parts = formatter.formatToParts(1000.1);\n\n const extractedPrefix = hasSymbolAt(parts, true)\n ? parts.find((part) => isSymbol(part.type))?.value || null\n : null;\n const extractedPostfix = hasSymbolAt(parts, false)\n ? [...parts].reverse().find((part) => isSymbol(part.type))?.value || null\n : null;\n\n return prefix !== undefined || postfix !== undefined\n ? {\n extractedPrefix: prefix,\n extractedPostfix: postfix,\n symbol: extractedPrefix || extractedPostfix || undefined,\n }\n : {\n extractedPrefix,\n extractedPostfix,\n symbol: extractedPrefix || extractedPostfix || undefined,\n };\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const escapeRegExp = (str) => str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
2
|
+
const getSuffix = (value, groupSeparator, decimalSeparator) => {
|
|
3
|
+
var _a;
|
|
4
|
+
const suffixReg = new RegExp(`\\d([^${escapeRegExp(groupSeparator)}${escapeRegExp(decimalSeparator)}0-9]+)`);
|
|
5
|
+
return (_a = value.match(suffixReg)) == null ? void 0 : _a[1];
|
|
6
|
+
};
|
|
7
|
+
function handleNumberFieldKeyDown({
|
|
8
|
+
event,
|
|
9
|
+
inputRef,
|
|
10
|
+
groupSeparator,
|
|
11
|
+
decimalSeparator,
|
|
12
|
+
onOriginalKeyDown
|
|
13
|
+
}) {
|
|
14
|
+
var _a;
|
|
15
|
+
const { key } = event;
|
|
16
|
+
const input = event.currentTarget;
|
|
17
|
+
const { selectionStart, value } = input;
|
|
18
|
+
if (selectionStart === null) {
|
|
19
|
+
onOriginalKeyDown == null ? void 0 : onOriginalKeyDown(event);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (/^[0-9]$/.test(key)) {
|
|
23
|
+
const decimalPos = value.indexOf(decimalSeparator);
|
|
24
|
+
if (selectionStart === 0) {
|
|
25
|
+
const integerPart = decimalPos >= 0 ? value.slice(0, decimalPos) : value;
|
|
26
|
+
const cleanedInteger = integerPart.replace(new RegExp(escapeRegExp(groupSeparator), "g"), "");
|
|
27
|
+
if (cleanedInteger === "0") {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
const newValue = key + (decimalPos >= 0 ? value.slice(decimalPos) : "");
|
|
30
|
+
input.value = newValue;
|
|
31
|
+
input.setSelectionRange(1, 1);
|
|
32
|
+
const inputEvent = new Event("input", { bubbles: true });
|
|
33
|
+
input.dispatchEvent(inputEvent);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (key === decimalSeparator) {
|
|
39
|
+
const decimalPos = value.indexOf(decimalSeparator);
|
|
40
|
+
if (decimalPos >= 0 && selectionStart <= decimalPos) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
input.setSelectionRange(decimalPos + 1, decimalPos + 1);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (key === "Backspace" && selectionStart > 0 && value[selectionStart - 1] === groupSeparator) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
let deletePos = selectionStart - 2;
|
|
49
|
+
while (deletePos >= 0 && value[deletePos] === groupSeparator) {
|
|
50
|
+
deletePos--;
|
|
51
|
+
}
|
|
52
|
+
if (deletePos >= 0) {
|
|
53
|
+
const newValue = value.slice(0, deletePos) + value.slice(deletePos + 1);
|
|
54
|
+
input.value = newValue;
|
|
55
|
+
input.setSelectionRange(deletePos, deletePos);
|
|
56
|
+
const inputEvent = new Event("input", { bubbles: true });
|
|
57
|
+
input.dispatchEvent(inputEvent);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (key === "Backspace" && selectionStart > 0 && value[selectionStart - 1] === decimalSeparator) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
let deletePos = selectionStart - 2;
|
|
64
|
+
while (deletePos >= 0 && value[deletePos] === groupSeparator) {
|
|
65
|
+
deletePos--;
|
|
66
|
+
}
|
|
67
|
+
if (deletePos >= 0) {
|
|
68
|
+
const newValue = value.slice(0, deletePos) + value.slice(deletePos + 1);
|
|
69
|
+
input.value = newValue;
|
|
70
|
+
const decimalPos = newValue.indexOf(decimalSeparator);
|
|
71
|
+
if (decimalPos >= 0) {
|
|
72
|
+
input.setSelectionRange(decimalPos, decimalPos);
|
|
73
|
+
}
|
|
74
|
+
const inputEvent = new Event("input", { bubbles: true });
|
|
75
|
+
input.dispatchEvent(inputEvent);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (key === "Delete" && value[selectionStart] === groupSeparator) {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
let deletePos = selectionStart + 1;
|
|
82
|
+
while (deletePos < value.length && value[deletePos] === groupSeparator) {
|
|
83
|
+
deletePos++;
|
|
84
|
+
}
|
|
85
|
+
if (deletePos < value.length) {
|
|
86
|
+
const newValue = value.slice(0, deletePos) + value.slice(deletePos + 1);
|
|
87
|
+
input.value = newValue;
|
|
88
|
+
input.setSelectionRange(selectionStart, selectionStart);
|
|
89
|
+
const inputEvent = new Event("input", { bubbles: true });
|
|
90
|
+
input.dispatchEvent(inputEvent);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (key === "Delete" && value[selectionStart] === decimalSeparator) {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
let deletePos = selectionStart + 1;
|
|
97
|
+
while (deletePos < value.length && !/[0-9]/.test(value[deletePos])) {
|
|
98
|
+
deletePos++;
|
|
99
|
+
}
|
|
100
|
+
if (deletePos < value.length) {
|
|
101
|
+
const newValue = value.slice(0, deletePos) + value.slice(deletePos + 1);
|
|
102
|
+
input.value = newValue;
|
|
103
|
+
input.setSelectionRange(selectionStart, selectionStart);
|
|
104
|
+
const inputEvent = new Event("input", { bubbles: true });
|
|
105
|
+
input.dispatchEvent(inputEvent);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (key !== "ArrowUp" && key !== "ArrowDown" && value !== "-") {
|
|
110
|
+
const suffix = getSuffix(value, groupSeparator, decimalSeparator);
|
|
111
|
+
if (suffix && selectionStart > value.length - suffix.length) {
|
|
112
|
+
const suffixStartPos = value.length - suffix.length;
|
|
113
|
+
(_a = inputRef.current) == null ? void 0 : _a.setSelectionRange(suffixStartPos, suffixStartPos);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
onOriginalKeyDown == null ? void 0 : onOriginalKeyDown(event);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { handleNumberFieldKeyDown };
|
|
120
|
+
//# sourceMappingURL=keyboardHelpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboardHelpers.js","sources":["../../../../src/NumberField/src/keyboardHelpers.ts"],"sourcesContent":["import { RefObject } from 'react';\n\n/** Escape special regex characters */\nconst escapeRegExp = (str: string): string =>\n str.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&');\n\n/** Extract suffix (non-numeric trailing characters) from value */\nconst getSuffix = (\n value: string,\n groupSeparator: string,\n decimalSeparator: string\n): string | undefined => {\n const suffixReg = new RegExp(\n `\\\\d([^${escapeRegExp(groupSeparator)}${escapeRegExp(\n decimalSeparator\n )}0-9]+)`\n );\n return value.match(suffixReg)?.[1];\n};\n\ninterface HandleKeyDownParams {\n event: React.KeyboardEvent<HTMLInputElement>;\n inputRef: RefObject<HTMLInputElement>;\n groupSeparator: string;\n decimalSeparator: string;\n onOriginalKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;\n}\n\n/**\n * Handle keyboard events for number field input.\n * Manages special behaviors for separators, navigation, and deletion.\n */\nexport function handleNumberFieldKeyDown({\n event,\n inputRef,\n groupSeparator,\n decimalSeparator,\n onOriginalKeyDown,\n}: HandleKeyDownParams): void {\n const { key } = event;\n const input = event.currentTarget;\n const { selectionStart, value } = input;\n\n if (selectionStart === null) {\n onOriginalKeyDown?.(event);\n return;\n }\n\n // Handle typing a digit at the start when value is 0\n // Example: \"|0.00\" → user types \"7\" → becomes \"7.00\" (replace, not insert)\n if (/^[0-9]$/.test(key)) {\n const decimalPos = value.indexOf(decimalSeparator);\n\n // Check if cursor is at the start and the integer part is just \"0\"\n if (selectionStart === 0) {\n // Extract the integer part (everything before decimal separator)\n const integerPart = decimalPos >= 0 ? value.slice(0, decimalPos) : value;\n\n // Remove group separators to check if it's just \"0\"\n const cleanedInteger = integerPart.replace(\n new RegExp(escapeRegExp(groupSeparator), 'g'),\n ''\n );\n\n if (cleanedInteger === '0') {\n event.preventDefault();\n\n // Replace the \"0\" with the typed digit\n const newValue = key + (decimalPos >= 0 ? value.slice(decimalPos) : '');\n input.value = newValue;\n\n // Position cursor after the typed digit\n input.setSelectionRange(1, 1);\n\n // Trigger input event to reformat\n const inputEvent = new Event('input', { bubbles: true });\n input.dispatchEvent(inputEvent);\n return;\n }\n }\n }\n\n // Handle typing decimal separator to navigate to decimal part\n // Example: \"123|.00\" → user types \".\" → cursor moves to \"123.|00\"\n if (key === decimalSeparator) {\n const decimalPos = value.indexOf(decimalSeparator);\n\n // If decimal exists and cursor is before it, jump to after decimal\n if (decimalPos >= 0 && selectionStart <= decimalPos) {\n event.preventDefault();\n input.setSelectionRange(decimalPos + 1, decimalPos + 1);\n return;\n }\n }\n\n // Handle Backspace when cursor is right after a group separator\n // Example: \"1,|234\" with Backspace should delete '1', not ','\n if (\n key === 'Backspace' &&\n selectionStart > 0 &&\n value[selectionStart - 1] === groupSeparator\n ) {\n event.preventDefault();\n\n // Find the digit before the separator\n let deletePos = selectionStart - 2;\n while (deletePos >= 0 && value[deletePos] === groupSeparator) {\n deletePos--;\n }\n\n if (deletePos >= 0) {\n // Delete the digit and let formatting handle the rest\n const newValue = value.slice(0, deletePos) + value.slice(deletePos + 1);\n input.value = newValue;\n\n // Set cursor position before the separator that was after the deleted digit\n input.setSelectionRange(deletePos, deletePos);\n\n // Trigger input event to reformat\n const inputEvent = new Event('input', { bubbles: true });\n input.dispatchEvent(inputEvent);\n }\n return;\n }\n\n // Handle Backspace when cursor is right after decimal separator\n // Example: \"123.|00\" with Backspace should delete '3', not '.'\n if (\n key === 'Backspace' &&\n selectionStart > 0 &&\n value[selectionStart - 1] === decimalSeparator\n ) {\n event.preventDefault();\n\n // Find the digit before the decimal separator\n let deletePos = selectionStart - 2;\n while (deletePos >= 0 && value[deletePos] === groupSeparator) {\n deletePos--;\n }\n\n if (deletePos >= 0) {\n // Delete the digit and let formatting handle the rest\n const newValue = value.slice(0, deletePos) + value.slice(deletePos + 1);\n input.value = newValue;\n\n // Set cursor position at the decimal separator (which shifted left by 1)\n const decimalPos = newValue.indexOf(decimalSeparator);\n if (decimalPos >= 0) {\n input.setSelectionRange(decimalPos, decimalPos);\n }\n\n // Trigger input event to reformat\n const inputEvent = new Event('input', { bubbles: true });\n input.dispatchEvent(inputEvent);\n }\n return;\n }\n\n // Handle Delete when cursor is right before a group separator\n // Example: \"1|,234\" with Delete should delete '2', not ','\n if (key === 'Delete' && value[selectionStart] === groupSeparator) {\n event.preventDefault();\n\n // Find the digit after the separator\n let deletePos = selectionStart + 1;\n while (deletePos < value.length && value[deletePos] === groupSeparator) {\n deletePos++;\n }\n\n if (deletePos < value.length) {\n // Delete the digit and let formatting handle the rest\n const newValue = value.slice(0, deletePos) + value.slice(deletePos + 1);\n input.value = newValue;\n\n // Keep cursor at the same position (before the separator)\n input.setSelectionRange(selectionStart, selectionStart);\n\n // Trigger input event to reformat\n const inputEvent = new Event('input', { bubbles: true });\n input.dispatchEvent(inputEvent);\n }\n return;\n }\n\n // Handle Delete when cursor is right before decimal separator\n // Example: \"123|.00\" with Delete should delete '0' (first decimal digit), not '.'\n if (key === 'Delete' && value[selectionStart] === decimalSeparator) {\n event.preventDefault();\n\n // Find the first digit after the separator\n let deletePos = selectionStart + 1;\n while (deletePos < value.length && !/[0-9]/.test(value[deletePos])) {\n deletePos++;\n }\n\n if (deletePos < value.length) {\n // Delete the digit and let formatting handle the rest\n const newValue = value.slice(0, deletePos) + value.slice(deletePos + 1);\n input.value = newValue;\n\n // Keep cursor at the decimal separator\n input.setSelectionRange(selectionStart, selectionStart);\n\n // Trigger input event to reformat\n const inputEvent = new Event('input', { bubbles: true });\n input.dispatchEvent(inputEvent);\n }\n return;\n }\n\n // Prevent cursor from entering suffix area (skip for arrow keys and empty values)\n if (key !== 'ArrowUp' && key !== 'ArrowDown' && value !== '-') {\n const suffix = getSuffix(value, groupSeparator, decimalSeparator);\n\n if (suffix && selectionStart > value.length - suffix.length) {\n const suffixStartPos = value.length - suffix.length;\n inputRef.current?.setSelectionRange(suffixStartPos, suffixStartPos);\n }\n }\n\n onOriginalKeyDown?.(event);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useMemo, useState, useRef, useLayoutEffect, useCallback } from 'react';
|
|
2
|
+
import { getSeparators, extractSymbol, parseNumericValue, trimSymbol, formatNumberString } from './formatHelpers.js';
|
|
3
|
+
import { calculateCursorPosition } from './cursorHelpers.js';
|
|
4
|
+
import { handleNumberFieldKeyDown } from './keyboardHelpers.js';
|
|
5
|
+
|
|
6
|
+
function useNumberFieldFormatting({
|
|
7
|
+
inputRef,
|
|
8
|
+
formatter,
|
|
9
|
+
inputValue,
|
|
10
|
+
prefix,
|
|
11
|
+
postfix,
|
|
12
|
+
onOriginalKeyDown,
|
|
13
|
+
onChange,
|
|
14
|
+
autoFormat
|
|
15
|
+
}) {
|
|
16
|
+
const { groupSeparator, decimalSeparator } = useMemo(() => getSeparators(formatter), [formatter]);
|
|
17
|
+
const [formattedValue, setFormattedValue] = useState("");
|
|
18
|
+
const symbolInfo = useMemo(() => extractSymbol(formatter, prefix, postfix), [formatter, prefix, postfix]);
|
|
19
|
+
const { extractedPrefix, extractedPostfix, symbol } = symbolInfo;
|
|
20
|
+
const cursorPositionRef = useRef(null);
|
|
21
|
+
const displayValue = useMemo(() => {
|
|
22
|
+
const numericValue = parseNumericValue(inputValue, decimalSeparator, formatter);
|
|
23
|
+
if (numericValue === null)
|
|
24
|
+
return inputValue;
|
|
25
|
+
try {
|
|
26
|
+
const trimmedValue = trimSymbol(inputValue, symbol) || inputValue;
|
|
27
|
+
return autoFormat ? formattedValue || trimmedValue : trimmedValue;
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return inputValue;
|
|
30
|
+
}
|
|
31
|
+
}, [
|
|
32
|
+
inputValue,
|
|
33
|
+
symbol,
|
|
34
|
+
formatter,
|
|
35
|
+
decimalSeparator,
|
|
36
|
+
autoFormat,
|
|
37
|
+
formattedValue
|
|
38
|
+
]);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
if (cursorPositionRef.current !== null && inputRef.current) {
|
|
41
|
+
inputRef.current.setSelectionRange(cursorPositionRef.current, cursorPositionRef.current);
|
|
42
|
+
cursorPositionRef.current = null;
|
|
43
|
+
}
|
|
44
|
+
}, [displayValue, inputRef]);
|
|
45
|
+
const handleInput = useCallback((e) => {
|
|
46
|
+
if (!autoFormat)
|
|
47
|
+
return;
|
|
48
|
+
const input = e.currentTarget;
|
|
49
|
+
const { value, selectionStart } = input;
|
|
50
|
+
const inputType = e.nativeEvent.inputType || "";
|
|
51
|
+
const shouldStrip = !!symbol;
|
|
52
|
+
const maxFractionDigits = formatter.resolvedOptions().maximumFractionDigits || 0;
|
|
53
|
+
const decimalIndex = value.indexOf(decimalSeparator);
|
|
54
|
+
if (decimalIndex !== -1 && selectionStart !== null) {
|
|
55
|
+
const decimalPart = value.slice(decimalIndex + 1).replace(new RegExp(`[^0-9]`, "g"), "");
|
|
56
|
+
const prevDecimalIndex = displayValue.indexOf(decimalSeparator);
|
|
57
|
+
const prevDecimalLength = prevDecimalIndex !== -1 ? displayValue.slice(prevDecimalIndex + 1).length : 0;
|
|
58
|
+
if (decimalPart.length > maxFractionDigits && selectionStart > decimalIndex + 1 + prevDecimalLength) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
cursorPositionRef.current = decimalIndex + 1 + prevDecimalLength;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const formattedValue2 = formatNumberString(value, formatter, decimalSeparator, shouldStrip, symbol);
|
|
65
|
+
setFormattedValue(formattedValue2);
|
|
66
|
+
const numericValue = parseNumericValue(formattedValue2, decimalSeparator, formatter);
|
|
67
|
+
if (formattedValue2 !== value && formattedValue2 !== "" && selectionStart !== null) {
|
|
68
|
+
const newCursorPosition = calculateCursorPosition(value, formattedValue2, selectionStart, inputType, decimalSeparator, numericValue);
|
|
69
|
+
cursorPositionRef.current = newCursorPosition;
|
|
70
|
+
}
|
|
71
|
+
if (numericValue !== null) {
|
|
72
|
+
onChange == null ? void 0 : onChange(numericValue);
|
|
73
|
+
}
|
|
74
|
+
}, [formatter, decimalSeparator, symbol, autoFormat, displayValue, onChange]);
|
|
75
|
+
const handleKeyDown = useCallback((e) => {
|
|
76
|
+
handleNumberFieldKeyDown({
|
|
77
|
+
event: e,
|
|
78
|
+
inputRef,
|
|
79
|
+
groupSeparator,
|
|
80
|
+
decimalSeparator,
|
|
81
|
+
onOriginalKeyDown
|
|
82
|
+
});
|
|
83
|
+
}, [groupSeparator, decimalSeparator, inputRef, onOriginalKeyDown]);
|
|
84
|
+
return {
|
|
85
|
+
displayValue,
|
|
86
|
+
extractedPrefix,
|
|
87
|
+
extractedPostfix,
|
|
88
|
+
handleInput,
|
|
89
|
+
handleKeyDown
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { useNumberFieldFormatting };
|
|
94
|
+
//# sourceMappingURL=useNumberFieldFormatting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNumberFieldFormatting.js","sources":["../../../../src/NumberField/src/useNumberFieldFormatting.ts"],"sourcesContent":["import {\n useMemo,\n useCallback,\n RefObject,\n ReactNode,\n useRef,\n useLayoutEffect,\n useState,\n} from 'react';\nimport {\n getSeparators,\n parseNumericValue,\n formatNumberString,\n trimSymbol,\n extractSymbol,\n} from './formatHelpers';\nimport { calculateCursorPosition } from './cursorHelpers';\nimport { handleNumberFieldKeyDown } from './keyboardHelpers';\n\ninterface UseNumberFieldFormattingParams {\n inputRef: RefObject<HTMLInputElement>;\n formatter: Intl.NumberFormat;\n inputValue: string;\n prefix?: ReactNode;\n postfix?: ReactNode;\n onOriginalKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;\n onChange?: (value: number) => void;\n autoFormat: boolean;\n}\n\ninterface UseNumberFieldFormattingReturn {\n displayValue: string;\n extractedPrefix: ReactNode;\n extractedPostfix: ReactNode;\n handleInput: (e: React.FormEvent<HTMLInputElement>) => void;\n handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;\n}\n\n/**\n * Hook for number field formatting with locale-aware separators and cursor management.\n * Handles currency/unit extraction, number formatting, and maintains correct cursor position.\n * When autoFormat is false, returns original values without any computation.\n */\nexport function useNumberFieldFormatting({\n inputRef,\n formatter,\n inputValue,\n prefix,\n postfix,\n onOriginalKeyDown,\n onChange,\n autoFormat,\n}: UseNumberFieldFormattingParams): UseNumberFieldFormattingReturn {\n const { groupSeparator, decimalSeparator } = useMemo(\n () => getSeparators(formatter),\n [formatter]\n );\n\n const [formattedValue, setFormattedValue] = useState<string>('');\n\n // Extract and position currency/unit symbols\n const symbolInfo = useMemo(\n () => extractSymbol(formatter, prefix, postfix),\n [formatter, prefix, postfix]\n );\n\n const { extractedPrefix, extractedPostfix, symbol } = symbolInfo;\n\n // Track cursor position to restore after React updates the value\n const cursorPositionRef = useRef<number | null>(null);\n\n // Format the input value for display\n const displayValue = useMemo(() => {\n const numericValue = parseNumericValue(\n inputValue,\n decimalSeparator,\n formatter\n );\n if (numericValue === null) return inputValue;\n\n try {\n const trimmedValue = trimSymbol(inputValue, symbol) || inputValue;\n return autoFormat ? formattedValue || trimmedValue : trimmedValue;\n } catch {\n return inputValue;\n }\n }, [\n inputValue,\n symbol,\n formatter,\n decimalSeparator,\n autoFormat,\n formattedValue,\n ]);\n\n // Restore cursor position after value changes\n useLayoutEffect(() => {\n if (cursorPositionRef.current !== null && inputRef.current) {\n inputRef.current.setSelectionRange(\n cursorPositionRef.current,\n cursorPositionRef.current\n );\n cursorPositionRef.current = null;\n }\n }, [displayValue, inputRef]);\n\n // Handle input with formatting and cursor management\n const handleInput = useCallback(\n (e: React.FormEvent<HTMLInputElement>): void => {\n if (!autoFormat) return;\n\n const input = e.currentTarget;\n const { value, selectionStart } = input;\n const inputType = (e.nativeEvent as InputEvent).inputType || '';\n const shouldStrip = !!symbol;\n\n /**\n * Check if we're exceeding maximum decimal places when typing at the end.\n * For example, with max 2 decimals, having \"12,34|\" and typing \"5\" should be prevented.\n */\n //#region Prevent excess decimals\n const maxFractionDigits =\n formatter.resolvedOptions().maximumFractionDigits || 0;\n const decimalIndex = value.indexOf(decimalSeparator);\n\n if (decimalIndex !== -1 && selectionStart !== null) {\n // Count decimal digits in the new value\n const decimalPart = value\n .slice(decimalIndex + 1)\n .replace(new RegExp(`[^0-9]`, 'g'), '');\n const prevDecimalIndex = displayValue.indexOf(decimalSeparator);\n const prevDecimalLength =\n prevDecimalIndex !== -1\n ? displayValue.slice(prevDecimalIndex + 1).length\n : 0;\n\n // If we have more decimals than allowed and cursor was at the end of decimals, prevent and restore\n if (\n decimalPart.length > maxFractionDigits &&\n selectionStart > decimalIndex + 1 + prevDecimalLength\n ) {\n e.preventDefault();\n // Store cursor position to restore after React's update\n cursorPositionRef.current = decimalIndex + 1 + prevDecimalLength;\n return;\n }\n }\n //#endregion Prevent excess decimals\n\n const formattedValue = formatNumberString(\n value,\n formatter,\n decimalSeparator,\n shouldStrip,\n symbol\n );\n\n setFormattedValue(formattedValue);\n\n const numericValue = parseNumericValue(\n formattedValue,\n decimalSeparator,\n formatter\n );\n\n // Calculate and store cursor position for restoration after React's update\n if (\n formattedValue !== value &&\n formattedValue !== '' &&\n selectionStart !== null\n ) {\n const newCursorPosition = calculateCursorPosition(\n value,\n formattedValue,\n selectionStart,\n inputType,\n decimalSeparator,\n numericValue\n );\n\n cursorPositionRef.current = newCursorPosition;\n }\n\n if (numericValue !== null) {\n onChange?.(numericValue);\n }\n },\n [formatter, decimalSeparator, symbol, autoFormat, displayValue, onChange]\n );\n\n // Handle keydown for special key behaviors\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLInputElement>): void => {\n handleNumberFieldKeyDown({\n event: e,\n inputRef,\n groupSeparator,\n decimalSeparator,\n onOriginalKeyDown,\n });\n },\n [groupSeparator, decimalSeparator, inputRef, onOriginalKeyDown]\n );\n\n return {\n displayValue,\n extractedPrefix,\n extractedPostfix,\n handleInput,\n handleKeyDown,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -42,7 +42,7 @@ const useSapphireNumberField = (_a, ref) => {
|
|
|
42
42
|
const numberFieldAria = useNumberField(__spreadProps(__spreadValues({}, numberFieldProps), {
|
|
43
43
|
validationState: error === false || error === void 0 ? void 0 : "invalid"
|
|
44
44
|
}), numberFieldState, ref);
|
|
45
|
-
return numberFieldAria;
|
|
45
|
+
return __spreadProps(__spreadValues({}, numberFieldAria), { state: numberFieldState });
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
export { useSapphireNumberField };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSapphireNumberField.js","sources":["../../../../src/NumberField/src/useSapphireNumberField.ts"],"sourcesContent":["import React, { RefObject } from 'react';\nimport {\n AriaNumberFieldProps,\n NumberFieldAria,\n useNumberField,\n} from '@react-aria/numberfield';\nimport { useNumberFieldState } from '@react-stately/numberfield';\nimport { useLocale } from '@react-aria/i18n';\n\nexport interface SapphireNumberFieldProps\n // TODO: when this is moved to `core`, we can replace the picked props with `ValueBasePropsKeys`\n extends Pick<\n AriaNumberFieldProps,\n | 'label'\n | 'isDisabled'\n | 'isRequired'\n | 'value'\n | 'defaultValue'\n | 'onChange'\n | 'incrementAriaLabel'\n | 'decrementAriaLabel'\n | 'onFocus'\n | 'onBlur'\n | 'formatOptions'\n | 'step'\n | 'minValue'\n | 'maxValue'\n | 'placeholder'\n | 'isReadOnly'\n | 'autoFocus'\n | 'onFocusChange'\n | 'onKeyUp'\n | 'onKeyDown'\n > {\n /**\n * Whether the input should render as having an error and an error message.\n * This also sets the appropriate `aria` attributes on the input.\n *\n * A `boolean` sets the error state.\n * A `ReactNode` sets the error state with an additional error message.\n *\n * **An error message is strongly recommended because an error should always have an explanation about how to fix it.**\n */\n error?: boolean | React.ReactNode;\n}\n\nexport const useSapphireNumberField = (\n { error, ...numberFieldProps }: SapphireNumberFieldProps,\n ref: RefObject<HTMLInputElement>\n): NumberFieldAria => {\n const { locale } = useLocale();\n const numberFieldState = useNumberFieldState({\n ...numberFieldProps,\n locale,\n });\n const numberFieldAria = useNumberField(\n {\n ...numberFieldProps,\n validationState:\n error === false || error === undefined ? undefined : 'invalid',\n },\n numberFieldState,\n ref\n );\n\n return numberFieldAria;\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8Ca,MAAA,sBAAA,GAAyB,CACpC,EAAA,EACA,
|
|
1
|
+
{"version":3,"file":"useSapphireNumberField.js","sources":["../../../../src/NumberField/src/useSapphireNumberField.ts"],"sourcesContent":["import React, { RefObject } from 'react';\nimport {\n AriaNumberFieldProps,\n NumberFieldAria,\n useNumberField,\n} from '@react-aria/numberfield';\nimport { useNumberFieldState } from '@react-stately/numberfield';\nimport { useLocale } from '@react-aria/i18n';\n\nexport interface SapphireNumberFieldProps\n // TODO: when this is moved to `core`, we can replace the picked props with `ValueBasePropsKeys`\n extends Pick<\n AriaNumberFieldProps,\n | 'label'\n | 'isDisabled'\n | 'isRequired'\n | 'value'\n | 'defaultValue'\n | 'onChange'\n | 'incrementAriaLabel'\n | 'decrementAriaLabel'\n | 'onFocus'\n | 'onBlur'\n | 'formatOptions'\n | 'step'\n | 'minValue'\n | 'maxValue'\n | 'placeholder'\n | 'isReadOnly'\n | 'autoFocus'\n | 'onFocusChange'\n | 'onKeyUp'\n | 'onKeyDown'\n > {\n /**\n * Whether the input should render as having an error and an error message.\n * This also sets the appropriate `aria` attributes on the input.\n *\n * A `boolean` sets the error state.\n * A `ReactNode` sets the error state with an additional error message.\n *\n * **An error message is strongly recommended because an error should always have an explanation about how to fix it.**\n */\n error?: boolean | React.ReactNode;\n}\n\nexport const useSapphireNumberField = (\n { error, ...numberFieldProps }: SapphireNumberFieldProps,\n ref: RefObject<HTMLInputElement>\n): NumberFieldAria & { state: ReturnType<typeof useNumberFieldState> } => {\n const { locale } = useLocale();\n const numberFieldState = useNumberFieldState({\n ...numberFieldProps,\n locale,\n });\n const numberFieldAria = useNumberField(\n {\n ...numberFieldProps,\n validationState:\n error === false || error === undefined ? undefined : 'invalid',\n },\n numberFieldState,\n ref\n );\n\n return { ...numberFieldAria, state: numberFieldState };\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8Ca,MAAA,sBAAA,GAAyB,CACpC,EAAA,EACA,GACwE,KAAA;AAFxE,EAAA,IAAA,EAAA,GAAA,EAAA,EAAE,EAAF,KAAA,EAAA,GAAA,EAAA,EAAY,gBAAZ,GAAA,SAAA,CAAA,EAAA,EAAY,CAAV,OAAA,CAAA,CAAA,CAAA;AAGF,EAAA,MAAM,EAAE,MAAW,EAAA,GAAA,SAAA,EAAA,CAAA;AACnB,EAAM,MAAA,gBAAA,GAAmB,mBAAoB,CAAA,aAAA,CAAA,cAAA,CAAA,EAAA,EACxC,gBADwC,CAAA,EAAA;AAAA,IAE3C,MAAA;AAAA,GAAA,CAAA,CAAA,CAAA;AAEF,EAAM,MAAA,eAAA,GAAkB,cACtB,CAAA,aAAA,CAAA,cAAA,CAAA,EAAA,EACK,gBADL,CAAA,EAAA;AAAA,IAEE,eACE,EAAA,KAAA,KAAU,KAAS,IAAA,KAAA,KAAU,SAAY,KAAY,CAAA,GAAA,SAAA;AAAA,GAAA,CAAA,EAEzD,gBACA,EAAA,GAAA,CAAA,CAAA;AAGF,EAAO,OAAA,aAAA,CAAA,cAAA,CAAA,EAAA,EAAK,eAAL,CAAA,EAAA,EAAsB,KAAO,EAAA,gBAAA,EAAA,CAAA,CAAA;AAAA;;;;"}
|
|
@@ -3,8 +3,8 @@ import clsx from 'clsx';
|
|
|
3
3
|
import { useButton } from '@react-aria/button';
|
|
4
4
|
import { useHover } from '@react-aria/interactions';
|
|
5
5
|
import { useTag } from '@react-aria/tag';
|
|
6
|
-
import { mergeProps } from '@react-aria/utils';
|
|
7
|
-
import { useThemeCheck, Icon } from '@danske/sapphire-react';
|
|
6
|
+
import { mergeRefs, mergeProps } from '@react-aria/utils';
|
|
7
|
+
import { useThemeCheck, TooltipIfNeeded, Icon } from '@danske/sapphire-react';
|
|
8
8
|
import { Close, Error } from '@danske/sapphire-icons/react';
|
|
9
9
|
import styles from '@danske/sapphire-css/components/tag/tag.module.css';
|
|
10
10
|
import { FocusRing } from '@react-aria/focus';
|
|
@@ -61,28 +61,36 @@ function Tag(props) {
|
|
|
61
61
|
]);
|
|
62
62
|
const { hoverProps, isHovered } = useHover({ isDisabled });
|
|
63
63
|
const hasAction = otherProps.hasAction;
|
|
64
|
-
return /* @__PURE__ */ React.createElement(
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
ref
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
return /* @__PURE__ */ React.createElement(TooltipIfNeeded, {
|
|
65
|
+
title: item.rendered
|
|
66
|
+
}, (tooltipRef, tooltipProps) => {
|
|
67
|
+
const _a2 = tooltipProps || {}, { ref: tooltipTargetRef } = _a2, restTooltipProps = __objRest(_a2, ["ref"]);
|
|
68
|
+
return /* @__PURE__ */ React.createElement(FocusRing, {
|
|
69
|
+
focusRingClass: styles["is-focus"]
|
|
70
|
+
}, /* @__PURE__ */ React.createElement("div", __spreadValues(__spreadProps(__spreadValues({
|
|
71
|
+
ref: mergeRefs(ref, tooltipTargetRef)
|
|
72
|
+
}, mergeProps({
|
|
73
|
+
onKeyDown: (e) => {
|
|
74
|
+
if (e.key !== " ") {
|
|
75
|
+
onKeyDown == null ? void 0 : onKeyDown(e);
|
|
76
|
+
} else {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
}
|
|
74
79
|
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
}, rowProps, hoverProps, restTooltipProps)), {
|
|
81
|
+
className: clsx(styles["sapphire-tag"], styles["js-focus"], styles["js-hover"], {
|
|
82
|
+
[styles["sapphire-tag--actionable"]]: hasAction,
|
|
83
|
+
[styles["is-active"]]: hasAction && isPressed,
|
|
84
|
+
[styles["is-hover"]]: hasAction && isHovered,
|
|
85
|
+
[styles["is-disabled"]]: isDisabled
|
|
86
|
+
})
|
|
87
|
+
}), hasError ? { "aria-invalid": true } : {}), /* @__PURE__ */ React.createElement("div", __spreadProps(__spreadValues({}, gridCellProps), {
|
|
88
|
+
className: styles["sapphire-tag__content"]
|
|
89
|
+
}), hasError && !isDisabled && /* @__PURE__ */ React.createElement(IconError, null), /* @__PURE__ */ React.createElement("span", {
|
|
90
|
+
ref: tooltipRef,
|
|
91
|
+
className: styles["sapphire-tag__label"]
|
|
92
|
+
}, item.rendered), !isDisabled && props.allowsRemoving && /* @__PURE__ */ React.createElement(RemoveButton, __spreadValues({}, removeButtonProps)))));
|
|
93
|
+
});
|
|
86
94
|
}
|
|
87
95
|
function RemoveButton(removeButtonProps) {
|
|
88
96
|
const buttonRef = useRef(null);
|