@jobber/components 6.52.0 → 6.53.0

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.
@@ -1,16 +1,256 @@
1
- export { I as InputPhoneNumber } from '../InputPhoneNumber-es.js';
2
- import '../tslib.es6-es.js';
3
- import 'react';
4
- import 'react-hook-form';
5
- import '../FormField-es.js';
6
- import 'framer-motion';
7
- import '@jobber/design';
8
- import 'classnames';
1
+ import React__default, { useState, useMemo, useCallback, useEffect, cloneElement, forwardRef, useId } from 'react';
2
+ import { _ as __rest } from '../tslib.es6-es.js';
3
+ import classnames from 'classnames';
4
+ import { j as useFormFieldWrapperStyles, b as useAtlantisFormFieldName, f as FormFieldWrapper, l as FormFieldPostFix, k as FormField } from '../FormField-es.js';
5
+ import { useFormContext, useForm } from 'react-hook-form';
9
6
  import '../Button-es.js';
10
- import 'react-router-dom';
7
+ import '@jobber/design';
8
+ import 'framer-motion';
11
9
  import '../Icon-es.js';
12
- import '../Typography-es.js';
13
10
  import '../Text-es.js';
11
+ import '../Typography-es.js';
14
12
  import '../useFormFieldFocus-es.js';
15
13
  import '../InputValidation-es.js';
16
14
  import '../Spinner-es.js';
15
+ import 'react-router-dom';
16
+
17
+ var styles = {"mask":"_78-Lxj78xPg-","hiddenValue":"GoiXVXaU1Qs-","emptyValue":"oOrjwubmsVA-","spinning":"T3VvmDmzs-4-"};
18
+
19
+ function useInputMask({ value = "", pattern, delimiter = "*", strict = true, onChange, }) {
20
+ const [isMasking, setIsMasking] = useState(!value);
21
+ const patternInfo = useMemo(() => {
22
+ const patternChars = pattern.split("");
23
+ const specialChars = patternChars.filter(char => char !== delimiter);
24
+ const maxCleanChars = patternChars.filter(char => char === delimiter).length;
25
+ return {
26
+ patternChars,
27
+ specialChars,
28
+ maxCleanChars,
29
+ };
30
+ }, [pattern, delimiter]);
31
+ const inputValue = useMemo(() => {
32
+ return value
33
+ .split("")
34
+ .filter(char => !patternInfo.specialChars.includes(char))
35
+ .join("");
36
+ }, [value, patternInfo.specialChars]);
37
+ const formatValue = useCallback((unformattedValue) => {
38
+ const { patternChars, specialChars, maxCleanChars } = patternInfo;
39
+ const cleanValueChars = unformattedValue
40
+ .split("")
41
+ .filter(char => !specialChars.includes(char) && !Number.isNaN(Number(char)));
42
+ const isOverCharLimit = cleanValueChars.length > maxCleanChars;
43
+ if (!strict && isOverCharLimit) {
44
+ return cleanValueChars.join("");
45
+ }
46
+ else {
47
+ const formattedValue = patternChars.reduce(getMaskedValue([...cleanValueChars], specialChars), "");
48
+ return formattedValue;
49
+ }
50
+ }, [patternInfo, strict]);
51
+ const maskedOnChange = useCallback((newValue, event) => {
52
+ const formatted = formatValue(newValue);
53
+ onChange === null || onChange === void 0 ? void 0 : onChange(formatted, event);
54
+ }, [formatValue, onChange]);
55
+ const formattedValue = useMemo(() => formatValue(value), [formatValue, value]);
56
+ useEffect(() => {
57
+ const shouldMask = strict || inputValue.length < patternInfo.maxCleanChars;
58
+ setIsMasking(shouldMask);
59
+ }, [inputValue, patternInfo.maxCleanChars, strict]);
60
+ const placeholderMask = useMemo(() => pattern
61
+ .replace(new RegExp(`\\${delimiter}`, "g"), "_")
62
+ .slice(value.length), [pattern, delimiter, value]);
63
+ return {
64
+ formattedValue,
65
+ placeholderMask,
66
+ isMasking,
67
+ maskedOnChange,
68
+ inputValue,
69
+ };
70
+ }
71
+ function getMaskedValue(cleanVal, specialChars) {
72
+ return (result, nextCharacter) => {
73
+ if (!cleanVal.length)
74
+ return result;
75
+ if (specialChars.includes(nextCharacter))
76
+ return result + nextCharacter;
77
+ const nextValue = cleanVal.shift();
78
+ return result + (nextValue !== undefined ? nextValue : "");
79
+ };
80
+ }
81
+
82
+ function MaskElement({ isMasking, formattedValue, placeholderMask, }) {
83
+ if (!isMasking) {
84
+ return null;
85
+ }
86
+ return (React__default.createElement("div", { className: styles.mask, "aria-hidden": "true" },
87
+ React__default.createElement("span", { className: styles.hiddenValue }, formattedValue),
88
+ React__default.createElement("span", null, placeholderMask)));
89
+ }
90
+ function InputMask({ children, delimiter = "*", pattern, strict = true, }) {
91
+ const { value: inputValue, onChange } = children.props;
92
+ const { placeholderMask, isMasking, formattedValue, maskedOnChange } = useInputMask({
93
+ value: String(inputValue || ""),
94
+ pattern,
95
+ delimiter,
96
+ strict,
97
+ onChange: onChange,
98
+ });
99
+ const inputMask = (React__default.createElement(MaskElement, { isMasking: isMasking, formattedValue: formattedValue, placeholderMask: placeholderMask }));
100
+ return cloneElement(children, {
101
+ onChange: maskedOnChange,
102
+ children: isMasking && inputMask,
103
+ });
104
+ }
105
+
106
+ const DEFAULT_PATTERN = "(***) ***-****";
107
+
108
+ /**
109
+ * Combines the actions on the InputPhoneNumber such as onChange, onEnter, onFocus, onBlur, and onClear to forward information to the consumers of the InputPhoneNumber.
110
+ * Do not repeat this pattern. We are doing this as a proof of concept relating to the refactoring of Form inputs to see what can be removed.
111
+ */
112
+ function useInputPhoneActions({ onChange, inputRef, onFocus, onBlur, onKeyDown, onEnter, }) {
113
+ function handleClear() {
114
+ var _a;
115
+ handleBlur();
116
+ onChange && onChange("");
117
+ (_a = inputRef === null || inputRef === void 0 ? void 0 : inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
118
+ }
119
+ function handleChange(event) {
120
+ const newValue = event.currentTarget.value;
121
+ onChange === null || onChange === void 0 ? void 0 : onChange(newValue, event);
122
+ }
123
+ function handleFocus(event) {
124
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
125
+ }
126
+ function handleKeyDown(event) {
127
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
128
+ if (!onEnter)
129
+ return;
130
+ if (event.key !== "Enter")
131
+ return;
132
+ if (event.shiftKey || event.ctrlKey)
133
+ return;
134
+ event.preventDefault();
135
+ onEnter === null || onEnter === void 0 ? void 0 : onEnter(event);
136
+ }
137
+ function handleBlur(event) {
138
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
139
+ }
140
+ return {
141
+ handleClear,
142
+ handleChange,
143
+ handleFocus,
144
+ handleBlur,
145
+ handleKeyDown,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Provides the props for the html fields rendered by the html input.
151
+ * DO not repeat this pattern. We are doing this as a proof of concept relating to the refactoring of Form inputs to see what can be removed.
152
+ */
153
+ function useInputPhoneFormField(_a) {
154
+ var { id, name, description, inline, handleChange, handleBlur, handleFocus, handleKeyDown, error, disabled, autofocus, value, readonly } = _a, rest = __rest(_a, ["id", "name", "description", "inline", "handleChange", "handleBlur", "handleFocus", "handleKeyDown", "error", "disabled", "autofocus", "value", "readonly"]);
155
+ const descriptionIdentifier = `descriptionUUID--${id}`;
156
+ const fieldProps = Object.assign(Object.assign(Object.assign(Object.assign({}, rest), { id,
157
+ name,
158
+ disabled, autoFocus: autofocus, onChange: handleChange, onBlur: handleBlur, onFocus: handleFocus, onKeyDown: handleKeyDown, value, invalid: error || rest.invalid ? "true" : undefined }), (description &&
159
+ !inline && { "aria-describedby": descriptionIdentifier })), { readOnly: readonly });
160
+ return { fieldProps, descriptionIdentifier };
161
+ }
162
+
163
+ const InputPhoneNumberRebuilt = forwardRef(function InputPhoneNumberInternal(_a, ref) {
164
+ var _b, _c, _d, _e;
165
+ var { pattern = DEFAULT_PATTERN } = _a, props = __rest(_a, ["pattern"]);
166
+ const inputPhoneNumberRef = (_b = ref) !== null && _b !== void 0 ? _b : React__default.useRef(null);
167
+ const wrapperRef = React__default.useRef(null);
168
+ const { inputStyle } = useFormFieldWrapperStyles(Object.assign(Object.assign({}, props), { type: "tel" }));
169
+ const generatedId = useId();
170
+ const id = props.id || generatedId;
171
+ const { name } = useAtlantisFormFieldName({
172
+ nameProp: props.name,
173
+ id: id,
174
+ });
175
+ const { formattedValue, isMasking, placeholderMask, inputValue, maskedOnChange, } = useInputMask({
176
+ value: props.value,
177
+ pattern,
178
+ strict: false,
179
+ onChange: props.onChange,
180
+ });
181
+ const { handleChange, handleBlur, handleFocus, handleClear, handleKeyDown, } = useInputPhoneActions({
182
+ onChange: maskedOnChange,
183
+ onBlur: props.onBlur,
184
+ onFocus: props.onFocus,
185
+ onEnter: props.onEnter,
186
+ inputRef: inputPhoneNumberRef,
187
+ });
188
+ const { fieldProps, descriptionIdentifier } = useInputPhoneFormField({
189
+ id,
190
+ name,
191
+ handleChange,
192
+ handleBlur,
193
+ handleFocus,
194
+ handleKeyDown,
195
+ autofocus: props.autoFocus,
196
+ disabled: props.disabled,
197
+ readonly: props.readonly,
198
+ invalid: props.invalid,
199
+ error: props.error,
200
+ description: props.description,
201
+ inline: props.inline,
202
+ });
203
+ return (React__default.createElement(FormFieldWrapper, { disabled: props.disabled, size: props.size, inline: props.inline, wrapperRef: wrapperRef, error: (_c = props.error) !== null && _c !== void 0 ? _c : "", invalid: Boolean(props.error || props.invalid), identifier: id, descriptionIdentifier: descriptionIdentifier, description: props.description, clearable: (_d = props.clearable) !== null && _d !== void 0 ? _d : "never", onClear: handleClear, type: "tel", placeholder: props.placeholder, value: formattedValue, prefix: props.prefix, suffix: props.suffix, readonly: props.readonly, loading: props.loading },
204
+ React__default.createElement("input", Object.assign({ type: "tel" }, fieldProps, { ref: inputPhoneNumberRef, className: classnames(inputStyle, {
205
+ [styles.emptyValue]: inputValue.length === 0 && pattern[0] === "(",
206
+ }), value: formattedValue })),
207
+ React__default.createElement(MaskElement, { isMasking: isMasking, formattedValue: formattedValue, placeholderMask: placeholderMask }),
208
+ React__default.createElement(FormFieldPostFix, { variation: "spinner", visible: (_e = props.loading) !== null && _e !== void 0 ? _e : false })));
209
+ });
210
+
211
+ function InputPhoneNumber$1(_a) {
212
+ var { required } = _a, props = __rest(_a, ["required"]);
213
+ const { placeholder, validations, pattern = DEFAULT_PATTERN } = props;
214
+ const errorSubject = placeholder || "Phone number";
215
+ const { getValues } = useFormContext() != undefined
216
+ ? useFormContext()
217
+ : // If there isn't a Form Context being provided, get a form for this field.
218
+ useForm({ mode: "onTouched" });
219
+ return (React__default.createElement(InputMask, { pattern: pattern, strict: false },
220
+ React__default.createElement(FormField, Object.assign({}, props, { type: "tel", pattern: pattern, validations: Object.assign(Object.assign({ required: {
221
+ value: Boolean(required),
222
+ message: `${errorSubject} is required`,
223
+ } }, validations), { validate: getPhoneNumberValidation }) }))));
224
+ function getPhoneNumberValidation(value) {
225
+ // Get unique characters that aren't * in the pattern
226
+ const patternNonDelimterCharacters = pattern
227
+ .split("")
228
+ .filter(char => char !== "*")
229
+ .filter((char, index, arr) => arr.indexOf(char) === index);
230
+ const specialCharacters = patternNonDelimterCharacters.join(" ");
231
+ // Remove special characters from pattern
232
+ const cleanValue = value.replace(new RegExp(`[${specialCharacters}]`, "g"), "");
233
+ const cleanValueRequiredLength = (pattern.match(/\*/g) || []).length;
234
+ if (cleanValue.length > 0 && cleanValue.length < cleanValueRequiredLength) {
235
+ return `${errorSubject} must contain ${cleanValueRequiredLength} or more digits`;
236
+ }
237
+ if (typeof (validations === null || validations === void 0 ? void 0 : validations.validate) === "function") {
238
+ return validations.validate(value, getValues);
239
+ }
240
+ return true;
241
+ }
242
+ }
243
+
244
+ function isNewInputPhoneNumberProps(props) {
245
+ return props.version === 2;
246
+ }
247
+ const InputPhoneNumber = forwardRef(function InputPhoneNumberShim(props, ref) {
248
+ if (isNewInputPhoneNumberProps(props)) {
249
+ return React__default.createElement(InputPhoneNumberRebuilt, Object.assign({}, props, { ref: ref }));
250
+ }
251
+ else {
252
+ return React__default.createElement(InputPhoneNumber$1, Object.assign({}, props));
253
+ }
254
+ });
255
+
256
+ export { InputPhoneNumber };
@@ -0,0 +1,48 @@
1
+ interface UseInputMaskParams {
2
+ /**
3
+ * The current value of the input
4
+ */
5
+ value: string;
6
+ /**
7
+ * A pattern to specify the format (e.g. "(***) ***-****")
8
+ */
9
+ pattern: string;
10
+ /**
11
+ * Character in the pattern that will be replaced with input values
12
+ * @default "*"
13
+ */
14
+ delimiter?: string;
15
+ /**
16
+ * Whether to restrict input to the pattern length
17
+ * @default true
18
+ */
19
+ strict?: boolean;
20
+ /**
21
+ * Callback function to be called with the masked value on change.
22
+ */
23
+ onChange?: (maskedValue: string, event?: React.ChangeEvent<HTMLInputElement>) => void;
24
+ }
25
+ interface UseInputMaskResult {
26
+ /**
27
+ * The formatted value according to the pattern
28
+ */
29
+ formattedValue: string;
30
+ /**
31
+ * The masked placeholder text (portion of pattern not yet filled)
32
+ */
33
+ placeholderMask: string;
34
+ /**
35
+ * Whether masking is currently active
36
+ */
37
+ isMasking: boolean;
38
+ /**
39
+ * Handler for input change events that formats the input and calls the onChange prop
40
+ */
41
+ maskedOnChange: (newValue: string, event?: React.ChangeEvent<HTMLInputElement>) => void;
42
+ /**
43
+ * Raw input value from the user without formatting characters
44
+ */
45
+ inputValue: string;
46
+ }
47
+ export declare function useInputMask({ value, pattern, delimiter, strict, onChange, }: UseInputMaskParams): UseInputMaskResult;
48
+ export {};
package/dist/index.cjs CHANGED
@@ -66,7 +66,7 @@ var InputFile = require('./InputFile-cjs.js');
66
66
  var InputGroup = require('./InputGroup-cjs.js');
67
67
  var InputNumber = require('./InputNumber-cjs.js');
68
68
  var InputPassword = require('./InputPassword-cjs.js');
69
- var InputPhoneNumber = require('./InputPhoneNumber-cjs.js');
69
+ var InputPhoneNumber_index = require('./InputPhoneNumber/index.cjs');
70
70
  var InputText_index = require('./InputText/index.cjs');
71
71
  var InputTime_index = require('./InputTime/index.cjs');
72
72
  var InputValidation = require('./InputValidation-cjs.js');
@@ -290,7 +290,7 @@ exports.useInputFileContentContext = InputFile.useInputFileContentContext;
290
290
  exports.InputGroup = InputGroup.InputGroup;
291
291
  exports.InputNumber = InputNumber.InputNumber;
292
292
  exports.InputPassword = InputPassword.InputPassword;
293
- exports.InputPhoneNumber = InputPhoneNumber.InputPhoneNumber;
293
+ exports.InputPhoneNumber = InputPhoneNumber_index.InputPhoneNumber;
294
294
  exports.InputText = InputText_index.InputText;
295
295
  exports.InputTime = InputTime_index.InputTime;
296
296
  exports.InputValidation = InputValidation.InputValidation;
package/dist/index.mjs CHANGED
@@ -64,7 +64,7 @@ export { I as InputFile, a as InputFileContentContext, u as updateFiles, b as us
64
64
  export { I as InputGroup } from './InputGroup-es.js';
65
65
  export { I as InputNumber } from './InputNumber-es.js';
66
66
  export { I as InputPassword } from './InputPassword-es.js';
67
- export { I as InputPhoneNumber } from './InputPhoneNumber-es.js';
67
+ export { InputPhoneNumber } from './InputPhoneNumber/index.mjs';
68
68
  export { InputText } from './InputText/index.mjs';
69
69
  export { InputTime } from './InputTime/index.mjs';
70
70
  export { I as InputValidation } from './InputValidation-es.js';