@fiscozen/input 0.1.7 → 0.1.8

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.
@@ -0,0 +1,105 @@
1
+ declare const _default: import('vue').DefineComponent<{
2
+ amount: import('vue').PropType<any>;
3
+ nullOnEmpty: {
4
+ type: import('vue').PropType<boolean>;
5
+ };
6
+ name: {
7
+ type: import('vue').PropType<string>;
8
+ };
9
+ size: {
10
+ type: import('vue').PropType<"sm" | "md" | "lg">;
11
+ };
12
+ required: {
13
+ type: import('vue').PropType<boolean>;
14
+ };
15
+ label: {
16
+ type: import('vue').PropType<string>;
17
+ required: true;
18
+ };
19
+ pattern: {
20
+ type: import('vue').PropType<string>;
21
+ };
22
+ placeholder: {
23
+ type: import('vue').PropType<string>;
24
+ };
25
+ disabled: {
26
+ type: import('vue').PropType<boolean>;
27
+ };
28
+ error: {
29
+ type: import('vue').PropType<boolean>;
30
+ };
31
+ leftIcon: {
32
+ type: import('vue').PropType<string>;
33
+ };
34
+ leftIconVariant: {
35
+ type: import('vue').PropType<import('@fiscozen/icons/src/types').IconVariant>;
36
+ };
37
+ rightIcon: {
38
+ type: import('vue').PropType<string>;
39
+ };
40
+ rightIconVariant: {
41
+ type: import('vue').PropType<import('@fiscozen/icons/src/types').IconVariant>;
42
+ };
43
+ valid: {
44
+ type: import('vue').PropType<boolean>;
45
+ };
46
+ readonly: {
47
+ type: import('vue').PropType<boolean>;
48
+ };
49
+ }, {
50
+ inputRef: import('vue').ComputedRef<HTMLInputElement | null | undefined>;
51
+ containerRef: import('vue').ComputedRef<HTMLElement | null | undefined>;
52
+ }, unknown, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
53
+ "update:amount": (...args: any[]) => void;
54
+ }, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
55
+ amount: import('vue').PropType<any>;
56
+ nullOnEmpty: {
57
+ type: import('vue').PropType<boolean>;
58
+ };
59
+ name: {
60
+ type: import('vue').PropType<string>;
61
+ };
62
+ size: {
63
+ type: import('vue').PropType<"sm" | "md" | "lg">;
64
+ };
65
+ required: {
66
+ type: import('vue').PropType<boolean>;
67
+ };
68
+ label: {
69
+ type: import('vue').PropType<string>;
70
+ required: true;
71
+ };
72
+ pattern: {
73
+ type: import('vue').PropType<string>;
74
+ };
75
+ placeholder: {
76
+ type: import('vue').PropType<string>;
77
+ };
78
+ disabled: {
79
+ type: import('vue').PropType<boolean>;
80
+ };
81
+ error: {
82
+ type: import('vue').PropType<boolean>;
83
+ };
84
+ leftIcon: {
85
+ type: import('vue').PropType<string>;
86
+ };
87
+ leftIconVariant: {
88
+ type: import('vue').PropType<import('@fiscozen/icons/src/types').IconVariant>;
89
+ };
90
+ rightIcon: {
91
+ type: import('vue').PropType<string>;
92
+ };
93
+ rightIconVariant: {
94
+ type: import('vue').PropType<import('@fiscozen/icons/src/types').IconVariant>;
95
+ };
96
+ valid: {
97
+ type: import('vue').PropType<boolean>;
98
+ };
99
+ readonly: {
100
+ type: import('vue').PropType<boolean>;
101
+ };
102
+ }>> & {
103
+ "onUpdate:amount"?: ((...args: any[]) => any) | undefined;
104
+ }, {}, {}>;
105
+ export default _default;
@@ -0,0 +1,127 @@
1
+ import { Ref } from 'vue';
2
+
3
+ declare const _default: __VLS_WithTemplateSlots<import('vue').DefineComponent<{
4
+ modelValue: import('vue').PropType<any>;
5
+ name: {
6
+ type: import('vue').PropType<string>;
7
+ };
8
+ size: {
9
+ type: import('vue').PropType<"sm" | "md" | "lg">;
10
+ default: string;
11
+ };
12
+ type: {
13
+ type: import('vue').PropType<"number" | "text" | "password" | "email" | "tel" | "url">;
14
+ default: string;
15
+ };
16
+ required: {
17
+ type: import('vue').PropType<boolean>;
18
+ };
19
+ label: {
20
+ type: import('vue').PropType<string>;
21
+ required: true;
22
+ };
23
+ pattern: {
24
+ type: import('vue').PropType<string>;
25
+ };
26
+ placeholder: {
27
+ type: import('vue').PropType<string>;
28
+ };
29
+ disabled: {
30
+ type: import('vue').PropType<boolean>;
31
+ };
32
+ error: {
33
+ type: import('vue').PropType<boolean>;
34
+ default: boolean;
35
+ };
36
+ leftIcon: {
37
+ type: import('vue').PropType<string>;
38
+ };
39
+ leftIconVariant: {
40
+ type: import('vue').PropType<import('@fiscozen/icons/src/types').IconVariant>;
41
+ };
42
+ rightIcon: {
43
+ type: import('vue').PropType<string>;
44
+ };
45
+ rightIconVariant: {
46
+ type: import('vue').PropType<import('@fiscozen/icons/src/types').IconVariant>;
47
+ };
48
+ valid: {
49
+ type: import('vue').PropType<boolean>;
50
+ };
51
+ readonly: {
52
+ type: import('vue').PropType<boolean>;
53
+ };
54
+ }, {
55
+ inputRef: Ref<HTMLInputElement | null>;
56
+ containerRef: Ref<HTMLElement | null>;
57
+ }, unknown, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
58
+ input: (...args: any[]) => void;
59
+ focus: (...args: any[]) => void;
60
+ }, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
61
+ modelValue: import('vue').PropType<any>;
62
+ name: {
63
+ type: import('vue').PropType<string>;
64
+ };
65
+ size: {
66
+ type: import('vue').PropType<"sm" | "md" | "lg">;
67
+ default: string;
68
+ };
69
+ type: {
70
+ type: import('vue').PropType<"number" | "text" | "password" | "email" | "tel" | "url">;
71
+ default: string;
72
+ };
73
+ required: {
74
+ type: import('vue').PropType<boolean>;
75
+ };
76
+ label: {
77
+ type: import('vue').PropType<string>;
78
+ required: true;
79
+ };
80
+ pattern: {
81
+ type: import('vue').PropType<string>;
82
+ };
83
+ placeholder: {
84
+ type: import('vue').PropType<string>;
85
+ };
86
+ disabled: {
87
+ type: import('vue').PropType<boolean>;
88
+ };
89
+ error: {
90
+ type: import('vue').PropType<boolean>;
91
+ default: boolean;
92
+ };
93
+ leftIcon: {
94
+ type: import('vue').PropType<string>;
95
+ };
96
+ leftIconVariant: {
97
+ type: import('vue').PropType<import('@fiscozen/icons/src/types').IconVariant>;
98
+ };
99
+ rightIcon: {
100
+ type: import('vue').PropType<string>;
101
+ };
102
+ rightIconVariant: {
103
+ type: import('vue').PropType<import('@fiscozen/icons/src/types').IconVariant>;
104
+ };
105
+ valid: {
106
+ type: import('vue').PropType<boolean>;
107
+ };
108
+ readonly: {
109
+ type: import('vue').PropType<boolean>;
110
+ };
111
+ }>> & {
112
+ onFocus?: ((...args: any[]) => any) | undefined;
113
+ onInput?: ((...args: any[]) => any) | undefined;
114
+ }, {
115
+ size: "sm" | "md" | "lg";
116
+ type: "number" | "text" | "password" | "email" | "tel" | "url";
117
+ error: boolean;
118
+ }, {}>, {
119
+ errorMessage?(_: {}): any;
120
+ helpText?(_: {}): any;
121
+ }>;
122
+ export default _default;
123
+ type __VLS_WithTemplateSlots<T, S> = T & {
124
+ new (): {
125
+ $slots: S;
126
+ };
127
+ };
@@ -0,0 +1,3 @@
1
+ export { default as FzInput } from './FzInput.vue';
2
+ export { default as FzCurrencyInput } from './FzCurrencyInput.vue';
3
+ export type * from './types';
@@ -0,0 +1,71 @@
1
+ import { IconVariant } from '@fiscozen/icons';
2
+
3
+ type FzInputProps = {
4
+ /**
5
+ * The label displayed on top of the input
6
+ */
7
+ label: string;
8
+ /**
9
+ * The size of the input
10
+ */
11
+ size?: "sm" | "md" | "lg";
12
+ /**
13
+ * The placeholder displayed in the input
14
+ */
15
+ placeholder?: string;
16
+ /**
17
+ * If set to true, the input is required
18
+ */
19
+ required?: boolean;
20
+ /**
21
+ * If set to true, the input is disabled
22
+ */
23
+ disabled?: boolean;
24
+ /**
25
+ * If set to true, the input is in error state
26
+ */
27
+ error?: boolean;
28
+ /**
29
+ * Left icon name
30
+ */
31
+ leftIcon?: string;
32
+ /**
33
+ * Left icon variant
34
+ */
35
+ leftIconVariant?: IconVariant;
36
+ /**
37
+ * Right icon name
38
+ */
39
+ rightIcon?: string;
40
+ /**
41
+ * Right icon variant
42
+ */
43
+ rightIconVariant?: IconVariant;
44
+ /**
45
+ * The input type
46
+ */
47
+ type?: "text" | "password" | "email" | "number" | "tel" | "url";
48
+ /**
49
+ * If set to true, the input is valid
50
+ */
51
+ valid?: boolean;
52
+ /**
53
+ * Pattern to validate the input
54
+ */
55
+ pattern?: string;
56
+ /**
57
+ * Defines the textarea key in a form
58
+ */
59
+ name?: string;
60
+ /**
61
+ * native readonly input value
62
+ */
63
+ readonly?: boolean;
64
+ };
65
+ interface FzCurrencyInputProps extends Omit<FzInputProps, "type" | "modelValue"> {
66
+ /**
67
+ * Is set to true, an empty string will be casted to null
68
+ */
69
+ nullOnEmpty?: boolean;
70
+ }
71
+ export { FzInputProps, FzCurrencyInputProps };
@@ -0,0 +1,12 @@
1
+ import { Ref } from 'vue';
2
+ import { FzInputProps } from './types';
3
+
4
+ export default function useInputStyle(props: FzInputProps, container: Ref<HTMLElement | null>): {
5
+ staticContainerClass: string;
6
+ computedContainerClass: import('vue').ComputedRef<string[]>;
7
+ computedLabelClass: import('vue').ComputedRef<string[]>;
8
+ staticInputClass: string;
9
+ computedHelpClass: import('vue').ComputedRef<string[]>;
10
+ computedErrorClass: import('vue').ComputedRef<string[]>;
11
+ containerWidth: import('vue').ComputedRef<string>;
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiscozen/input",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Design System Input component",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -9,8 +9,8 @@
9
9
  "peerDependencies": {
10
10
  "tailwindcss": "^3.4.1",
11
11
  "vue": "^3.4.13",
12
- "@fiscozen/icons": "^0.1.9",
13
- "@fiscozen/composables": "^0.1.24"
12
+ "@fiscozen/composables": "^0.1.26",
13
+ "@fiscozen/icons": "^0.1.10"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@rushstack/eslint-patch": "^1.3.3",
@@ -1,26 +1,118 @@
1
1
  <template>
2
- <FzInput ref="fzInputRef" v-bind="props" type="text"></FzInput>
2
+ <FzInput
3
+ ref="fzInputRef"
4
+ v-bind="props"
5
+ :modelValue="fzInputModel"
6
+ type="text"
7
+ @paste="onPaste"
8
+ ></FzInput>
3
9
  </template>
4
10
 
5
11
  <script setup lang="ts">
6
- import { computed, onMounted, ref } from "vue";
12
+ import { computed, nextTick, onMounted, ref, watch } from "vue";
7
13
  import FzInput from "./FzInput.vue";
8
14
  import { FzCurrencyInputProps } from "./types";
9
15
  import { useCurrency } from "@fiscozen/composables";
10
16
 
11
17
  const fzInputRef = ref<InstanceType<typeof FzInput>>();
18
+ const fzInputModel = ref();
12
19
  const containerRef = computed(() => fzInputRef.value?.containerRef);
13
20
  const inputRef = computed(() => fzInputRef.value?.inputRef);
14
- const props = defineProps<FzCurrencyInputProps>();
15
- const { inputRef: currencyInputRef } = useCurrency();
21
+ const props = withDefaults(defineProps<FzCurrencyInputProps>(), {
22
+ minimumFractionDigits: 2,
23
+ maximumFractionDigits: 2,
24
+ });
25
+ const {
26
+ inputRef: currencyInputRef,
27
+ setValue,
28
+ emitAmount,
29
+ parse,
30
+ format,
31
+ } = useCurrency({
32
+ minimumFractionDigits: props.minimumFractionDigits,
33
+ maximumFractionDigits: props.maximumFractionDigits,
34
+ });
16
35
 
17
36
  defineEmits(["update:amount"]);
18
37
 
38
+ const onPaste = (e: ClipboardEvent) => {
39
+ e.preventDefault();
40
+
41
+ if (props.readonly) {
42
+ return;
43
+ }
44
+
45
+ let rawPastedText;
46
+ if (e.clipboardData && e.clipboardData.getData) {
47
+ rawPastedText = e.clipboardData.getData("text/plain");
48
+ } else {
49
+ throw "invalid paste value";
50
+ }
51
+
52
+ // Fix for firefox paste handling on `contenteditable` elements where `e.target` is the text node, not the element
53
+ let eventTarget;
54
+ if ((!e.target as any)?.tagName) {
55
+ eventTarget = (e as any).explicitOriginalTarget;
56
+ } else {
57
+ eventTarget = e.target;
58
+ }
59
+
60
+ let isNegative = rawPastedText.slice(0, 1) === "-";
61
+ const separatorRegex = /[,.]/g;
62
+ const separators: string[] = [...rawPastedText.matchAll(separatorRegex)].map(
63
+ (regexRes) => regexRes[0],
64
+ );
65
+
66
+ const uniqueSeparators = new Set(separators);
67
+ let decimalSeparator = ".";
68
+ let thousandSeparator = "";
69
+ let unknownSeparator;
70
+
71
+ // case 1: there are 2 different separators pasted, therefore we can assume the rightmost is the decimal separator
72
+ if (uniqueSeparators.size > 1) {
73
+ decimalSeparator = separators[separators.length - 1];
74
+ thousandSeparator = separators[0];
75
+ }
76
+
77
+ // case 2: there are multiple instances of the same separator, therefore it must be the thousand separator
78
+ if (uniqueSeparators.size === 1) {
79
+ if (separators.length > 1) {
80
+ thousandSeparator = separators[0];
81
+ }
82
+
83
+ // case 3: there is only one instance of a separator with < 3 digits afterwards (must be decimal separator)
84
+ unknownSeparator = separators[0];
85
+ const splitted = rawPastedText.split(unknownSeparator);
86
+
87
+ if (splitted[1].length !== 3) {
88
+ decimalSeparator = unknownSeparator;
89
+ }
90
+ }
91
+
92
+ // case 3: there is only one instance of a separator with 3 digits afterwards. Here we cannot make assumptions
93
+ // we will format based on settings
94
+ //@ts-ignore
95
+ let safeText = rawPastedText.replaceAll(thousandSeparator, "").trim();
96
+ safeText = safeText.replaceAll(decimalSeparator, ".").trim();
97
+
98
+ const safeNum = parse(safeText);
99
+ safeText = format(safeNum);
100
+ setValue(safeText);
101
+ emitAmount(safeNum);
102
+ };
103
+
19
104
  onMounted(() => {
20
105
  currencyInputRef.value = inputRef.value;
106
+ nextTick(() => {
107
+ fzInputModel.value = inputRef.value?.value;
108
+ });
21
109
  });
22
110
  const model = defineModel("amount");
23
111
 
112
+ watch(model, (newVal) => {
113
+ fzInputModel.value = newVal as string;
114
+ });
115
+
24
116
  defineExpose({
25
117
  inputRef,
26
118
  containerRef,
package/src/FzInput.vue CHANGED
@@ -27,6 +27,7 @@
27
27
  :pattern="pattern"
28
28
  :name
29
29
  @focus="(e) => $emit('focus', e)"
30
+ @paste="(e) => $emit('paste', e)"
30
31
  />
31
32
  <FzIcon
32
33
  v-if="valid"
@@ -92,7 +93,7 @@ const {
92
93
  containerWidth,
93
94
  } = useInputStyle(props, containerRef);
94
95
 
95
- const emit = defineEmits(["input", "focus"]);
96
+ const emit = defineEmits(["input", "focus", "paste"]);
96
97
  defineExpose({
97
98
  inputRef,
98
99
  containerRef,
@@ -37,4 +37,93 @@ describe.concurrent("FzCurrencyInput", () => {
37
37
  await new Promise((resolve) => window.setTimeout(resolve, 100));
38
38
  expect(inputElement.element.value).toBe("12,30");
39
39
  });
40
+
41
+ it("should allow to set value at 0", async () => {
42
+ const wrapper = mount(FzCurrencyInput, {
43
+ props: {
44
+ label: "Label",
45
+ amount: 10,
46
+ "onUpdate:amount": (e) => wrapper.setProps({ amount: e }),
47
+ },
48
+ });
49
+
50
+ const inputElement = wrapper.find("input");
51
+ await inputElement.trigger("blur");
52
+ await new Promise((resolve) => window.setTimeout(resolve, 100));
53
+ expect(inputElement.element.value).toBe("10,00");
54
+ wrapper.setProps({ amount: 0 });
55
+ await new Promise((resolve) => window.setTimeout(resolve, 100));
56
+ expect(inputElement.element.value).toBe("0,00");
57
+ });
58
+
59
+ it("should handle pasted values using the best possible euristic to parse and render it correctly", async () => {
60
+ const wrapper = mount(FzCurrencyInput, {
61
+ props: {
62
+ label: "Label",
63
+ "onUpdate:amount": (e) => wrapper.setProps({ amount: e }),
64
+ },
65
+ });
66
+
67
+ const inputElement = wrapper.find("input");
68
+ // we need to mock the paste event
69
+ await inputElement.trigger("paste", {
70
+ clipboardData: {
71
+ getData() {
72
+ return "1.233.222,43";
73
+ },
74
+ },
75
+ });
76
+ await new Promise((resolve) => window.setTimeout(resolve, 100));
77
+ expect(inputElement.element.value).toBe("1233222,43");
78
+
79
+ await inputElement.trigger("paste", {
80
+ clipboardData: {
81
+ getData() {
82
+ return "1.23";
83
+ },
84
+ },
85
+ });
86
+ await new Promise((resolve) => window.setTimeout(resolve, 100));
87
+ expect(inputElement.element.value).toBe("1,23");
88
+
89
+ await inputElement.trigger("paste", {
90
+ clipboardData: {
91
+ getData() {
92
+ return "1,23";
93
+ },
94
+ },
95
+ });
96
+ await new Promise((resolve) => window.setTimeout(resolve, 100));
97
+ expect(inputElement.element.value).toBe("1,23");
98
+
99
+ await inputElement.trigger("paste", {
100
+ clipboardData: {
101
+ getData() {
102
+ return "1.232.111";
103
+ },
104
+ },
105
+ });
106
+ await new Promise((resolve) => window.setTimeout(resolve, 100));
107
+ expect(inputElement.element.value).toBe("1232111,00");
108
+
109
+ await inputElement.trigger("paste", {
110
+ clipboardData: {
111
+ getData() {
112
+ return "1.232";
113
+ },
114
+ },
115
+ });
116
+ await new Promise((resolve) => window.setTimeout(resolve, 100));
117
+ expect(inputElement.element.value).toBe("1,23");
118
+
119
+ await inputElement.trigger("paste", {
120
+ clipboardData: {
121
+ getData() {
122
+ return "1.232555";
123
+ },
124
+ },
125
+ });
126
+ await new Promise((resolve) => window.setTimeout(resolve, 100));
127
+ expect(inputElement.element.value).toBe("1,23");
128
+ });
40
129
  });
package/src/types.ts CHANGED
@@ -70,6 +70,18 @@ interface FzCurrencyInputProps
70
70
  * Is set to true, an empty string will be casted to null
71
71
  */
72
72
  nullOnEmpty?: boolean;
73
+ /**
74
+ * Minimum number of decimal places allowed, set null to allow arbitrary decimal values length
75
+ * note that limits from Intl.NumberFormat still apply
76
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#digit_options
77
+ */
78
+ minimumFractionDigits?: number;
79
+ /**
80
+ * Maximum number of decimal places allowed, set null to allow arbitrary decimal values length
81
+ * note that limits from Intl.NumberFormat still apply
82
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#digit_options
83
+ */
84
+ maximumFractionDigits?: number | null;
73
85
  }
74
86
 
75
87
  export { FzInputProps, FzCurrencyInputProps };