@youngonesworks/ui 0.1.116 → 0.1.118
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/phoneInput/PhoneInput.stories.d.ts +9 -0
- package/dist/components/phoneInput/index.d.ts +15 -0
- package/dist/components/select/index.d.ts +1 -1
- package/dist/hooks/phone/usePhoneNumber.d.ts +13 -0
- package/dist/hooks/phone/usePhoneNumberPrefix.d.ts +1 -0
- package/dist/index.cjs +267 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +267 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { PhoneInput } from './index';
|
|
3
|
+
declare const meta: Meta<typeof PhoneInput>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof PhoneInput>;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const UnitedStates: Story;
|
|
8
|
+
export declare const Germany: Story;
|
|
9
|
+
export declare const CustomMessages: Story;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type TextInputProps } from '@components/textInput';
|
|
3
|
+
interface PhoneInputProps extends Omit<TextInputProps, 'onChange' | 'value' | 'ref'> {
|
|
4
|
+
searchPlaceHolder: string;
|
|
5
|
+
defaultCountry?: string;
|
|
6
|
+
invalidMessage: string;
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
ref?: RefObject<HTMLInputElement>;
|
|
10
|
+
}
|
|
11
|
+
export declare const PhoneInput: {
|
|
12
|
+
({ placeholder, searchPlaceHolder, defaultCountry, invalidMessage, defaultValue, onChange, ref, ...rest }: PhoneInputProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
displayName: string;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PhoneInvalidResult, type PhoneValidResult } from 'phone';
|
|
2
|
+
interface PhoneOptions {
|
|
3
|
+
country?: string;
|
|
4
|
+
validateMobilePrefix?: boolean;
|
|
5
|
+
strictDetection?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function usePhoneNumber(): {
|
|
8
|
+
validatePhone: (phoneNumber: string, options?: PhoneOptions) => PhoneValidResult | PhoneInvalidResult;
|
|
9
|
+
stripCountryCode: (phoneNumber: string) => string;
|
|
10
|
+
getCountryCode: (phoneNumber: string) => string;
|
|
11
|
+
formatToInternational: (phoneNumber: string, options?: PhoneOptions) => string;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const usePhoneNumberPrefix: (defaultCountry: string) => import("country-list-with-dial-code-and-flag").Country[];
|
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,8 @@ const date_fns = __toESM(require("date-fns"));
|
|
|
35
35
|
const date_fns_locale = __toESM(require("date-fns/locale"));
|
|
36
36
|
const react_dom = __toESM(require("react-dom"));
|
|
37
37
|
const react_tooltip = __toESM(require("react-tooltip"));
|
|
38
|
+
const phone = __toESM(require("phone"));
|
|
39
|
+
const country_list_with_dial_code_and_flag = __toESM(require("country-list-with-dial-code-and-flag"));
|
|
38
40
|
const __tiptap_extension_placeholder = __toESM(require("@tiptap/extension-placeholder"));
|
|
39
41
|
const __tiptap_extension_underline = __toESM(require("@tiptap/extension-underline"));
|
|
40
42
|
const __tiptap_react = __toESM(require("@tiptap/react"));
|
|
@@ -2471,9 +2473,7 @@ function Select({ id, options, placeholder, label, errorText, hideError = false,
|
|
|
2471
2473
|
case "Enter":
|
|
2472
2474
|
case " ":
|
|
2473
2475
|
event.preventDefault();
|
|
2474
|
-
if (focusedIndex >= 0 && filteredOptions[focusedIndex])
|
|
2475
|
-
handleSelect(filteredOptions[focusedIndex].value);
|
|
2476
|
-
}
|
|
2476
|
+
if (focusedIndex >= 0 && filteredOptions[focusedIndex]) handleSelect(filteredOptions[focusedIndex].value);
|
|
2477
2477
|
break;
|
|
2478
2478
|
}
|
|
2479
2479
|
}, [
|
|
@@ -3078,6 +3078,269 @@ const UnorderedListItem = ({ children, actionItem, className, header = false,...
|
|
|
3078
3078
|
})]
|
|
3079
3079
|
});
|
|
3080
3080
|
|
|
3081
|
+
//#endregion
|
|
3082
|
+
//#region src/hooks/phone/usePhoneNumber.ts
|
|
3083
|
+
function usePhoneNumber() {
|
|
3084
|
+
/**
|
|
3085
|
+
* Validates a phone number and returns parsing results
|
|
3086
|
+
*/
|
|
3087
|
+
const validatePhone = (0, react.useCallback)((phoneNumber, options) => (0, phone.default)(phoneNumber, options), []);
|
|
3088
|
+
/**
|
|
3089
|
+
* Strips the country code from a phone number
|
|
3090
|
+
* Example: +85265698900 -> 65698900
|
|
3091
|
+
*/
|
|
3092
|
+
const stripCountryCode = (0, react.useCallback)((phoneNumber) => {
|
|
3093
|
+
const result = (0, phone.default)(phoneNumber);
|
|
3094
|
+
if (!result.isValid) return phoneNumber;
|
|
3095
|
+
return result.phoneNumber.replace(result.countryCode, "");
|
|
3096
|
+
}, []);
|
|
3097
|
+
/**
|
|
3098
|
+
* Returns the country code from a phone number
|
|
3099
|
+
* Example: +85265698900 -> +852
|
|
3100
|
+
*/
|
|
3101
|
+
const getCountryCode = (0, react.useCallback)((phoneNumber) => {
|
|
3102
|
+
const result = (0, phone.default)(phoneNumber);
|
|
3103
|
+
if (!result.isValid) return phoneNumber;
|
|
3104
|
+
return result.countryCode;
|
|
3105
|
+
}, []);
|
|
3106
|
+
/**
|
|
3107
|
+
* Formats a phone number to international format with country code
|
|
3108
|
+
* Example: 0648711212 (with country: 'NL') -> +31648711212
|
|
3109
|
+
*/
|
|
3110
|
+
const formatToInternational = (0, react.useCallback)((phoneNumber, options) => {
|
|
3111
|
+
const result = (0, phone.default)(phoneNumber, options);
|
|
3112
|
+
if (!result.isValid) return phoneNumber;
|
|
3113
|
+
return result.phoneNumber;
|
|
3114
|
+
}, []);
|
|
3115
|
+
return {
|
|
3116
|
+
validatePhone,
|
|
3117
|
+
stripCountryCode,
|
|
3118
|
+
getCountryCode,
|
|
3119
|
+
formatToInternational
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
//#endregion
|
|
3124
|
+
//#region src/hooks/phone/usePhoneNumberPrefix.ts
|
|
3125
|
+
const usePhoneNumberPrefix = (defaultCountry) => {
|
|
3126
|
+
const countryList = country_list_with_dial_code_and_flag.default.getAll();
|
|
3127
|
+
return countryList.filter((country) => country.dial_code).sort((a, b) => {
|
|
3128
|
+
if (a?.code === defaultCountry) return -1;
|
|
3129
|
+
if (b?.code === defaultCountry) return 1;
|
|
3130
|
+
return a.name.localeCompare(b.name);
|
|
3131
|
+
});
|
|
3132
|
+
};
|
|
3133
|
+
|
|
3134
|
+
//#endregion
|
|
3135
|
+
//#region src/components/phoneInput/index.tsx
|
|
3136
|
+
const CountrySelector = ({ searchPlaceholder, defaultCountry, ref }) => {
|
|
3137
|
+
const phoneNumberPrefixes = usePhoneNumberPrefix(defaultCountry);
|
|
3138
|
+
const [selectedCountry, setSelectedCountry] = (0, react.useState)(phoneNumberPrefixes.find((country) => country.code === defaultCountry) || null);
|
|
3139
|
+
const [openDropdown, setOpenDropdown] = (0, react.useState)(false);
|
|
3140
|
+
const [searchQuery, setSearchQuery] = (0, react.useState)("");
|
|
3141
|
+
const [filteredCountries, setFilteredCountries] = (0, react.useState)(phoneNumberPrefixes);
|
|
3142
|
+
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(-1);
|
|
3143
|
+
const optionRefs = (0, react.useRef)([]);
|
|
3144
|
+
const { refs, floatingStyles, context } = (0, __floating_ui_react.useFloating)({
|
|
3145
|
+
open: openDropdown,
|
|
3146
|
+
onOpenChange: (isOpen) => {
|
|
3147
|
+
setOpenDropdown(isOpen);
|
|
3148
|
+
if (isOpen) setHighlightedIndex(-1);
|
|
3149
|
+
},
|
|
3150
|
+
middleware: [
|
|
3151
|
+
(0, __floating_ui_react.offset)(4),
|
|
3152
|
+
(0, __floating_ui_react.flip)(),
|
|
3153
|
+
(0, __floating_ui_react.shift)()
|
|
3154
|
+
],
|
|
3155
|
+
whileElementsMounted: __floating_ui_react.autoUpdate,
|
|
3156
|
+
placement: "bottom-start"
|
|
3157
|
+
});
|
|
3158
|
+
const click = (0, __floating_ui_react.useClick)(context);
|
|
3159
|
+
const dismiss = (0, __floating_ui_react.useDismiss)(context);
|
|
3160
|
+
const role = (0, __floating_ui_react.useRole)(context, { role: "combobox" });
|
|
3161
|
+
const { getReferenceProps, getFloatingProps } = (0, __floating_ui_react.useInteractions)([
|
|
3162
|
+
click,
|
|
3163
|
+
dismiss,
|
|
3164
|
+
role
|
|
3165
|
+
]);
|
|
3166
|
+
const performSearch = (0, react.useCallback)((query) => {
|
|
3167
|
+
if (!query) return phoneNumberPrefixes;
|
|
3168
|
+
const lowerQuery = query.toLowerCase();
|
|
3169
|
+
return phoneNumberPrefixes.filter((country) => country.name.toLowerCase().includes(lowerQuery) || country.dial_code.toLowerCase().includes(lowerQuery) || country.code.toLowerCase().includes(lowerQuery));
|
|
3170
|
+
}, [phoneNumberPrefixes]);
|
|
3171
|
+
(0, react.useEffect)(() => {
|
|
3172
|
+
const results = performSearch(searchQuery);
|
|
3173
|
+
setFilteredCountries(results);
|
|
3174
|
+
setHighlightedIndex(-1);
|
|
3175
|
+
}, [searchQuery]);
|
|
3176
|
+
(0, react.useEffect)(() => {
|
|
3177
|
+
optionRefs.current = optionRefs.current.slice(0, filteredCountries.length);
|
|
3178
|
+
}, [filteredCountries]);
|
|
3179
|
+
(0, react.useEffect)(() => {
|
|
3180
|
+
if (highlightedIndex >= 0 && optionRefs.current[highlightedIndex]) {
|
|
3181
|
+
optionRefs.current[highlightedIndex]?.scrollIntoView({
|
|
3182
|
+
behavior: "auto",
|
|
3183
|
+
block: "nearest"
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
}, [highlightedIndex]);
|
|
3187
|
+
const handleSelect = (country) => {
|
|
3188
|
+
setSelectedCountry(country);
|
|
3189
|
+
setOpenDropdown(false);
|
|
3190
|
+
};
|
|
3191
|
+
const handleKeyDown = (e) => {
|
|
3192
|
+
if (filteredCountries.length === 0) return;
|
|
3193
|
+
switch (e.key) {
|
|
3194
|
+
case "ArrowDown":
|
|
3195
|
+
e.preventDefault();
|
|
3196
|
+
setHighlightedIndex((prev) => prev < filteredCountries.length - 1 ? prev + 1 : 0);
|
|
3197
|
+
break;
|
|
3198
|
+
case "ArrowUp":
|
|
3199
|
+
e.preventDefault();
|
|
3200
|
+
setHighlightedIndex((prev) => prev > 0 ? prev - 1 : filteredCountries.length - 1);
|
|
3201
|
+
break;
|
|
3202
|
+
case "Enter": {
|
|
3203
|
+
e.preventDefault();
|
|
3204
|
+
if (highlightedIndex >= 0) handleSelect(filteredCountries[highlightedIndex]);
|
|
3205
|
+
break;
|
|
3206
|
+
}
|
|
3207
|
+
case "Escape":
|
|
3208
|
+
e.preventDefault();
|
|
3209
|
+
setOpenDropdown(false);
|
|
3210
|
+
break;
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
(0, react.useImperativeHandle)(ref, () => ({
|
|
3214
|
+
updateCountry: (countryCode) => {
|
|
3215
|
+
const country = phoneNumberPrefixes.find((c) => c.dial_code === countryCode);
|
|
3216
|
+
if (country) setSelectedCountry(country);
|
|
3217
|
+
},
|
|
3218
|
+
getSelectedCountry: () => selectedCountry
|
|
3219
|
+
}));
|
|
3220
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3221
|
+
className: "relative",
|
|
3222
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3223
|
+
type: "button",
|
|
3224
|
+
ref: refs.setReference,
|
|
3225
|
+
...getReferenceProps(),
|
|
3226
|
+
className: cn("flex h-10 items-center justify-between px-2 text-sm transition-colors"),
|
|
3227
|
+
"aria-haspopup": "listbox",
|
|
3228
|
+
"aria-expanded": openDropdown,
|
|
3229
|
+
"aria-label": `Country selector. Currently selected: ${selectedCountry?.name || "None"} ${selectedCountry?.dial_code || ""}`,
|
|
3230
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3231
|
+
className: "flex items-center gap-1",
|
|
3232
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3233
|
+
className: "text-xs font-medium",
|
|
3234
|
+
children: selectedCountry?.dial_code
|
|
3235
|
+
})
|
|
3236
|
+
})
|
|
3237
|
+
}), openDropdown && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__floating_ui_react.FloatingPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__floating_ui_react.FloatingFocusManager, {
|
|
3238
|
+
context,
|
|
3239
|
+
modal: false,
|
|
3240
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3241
|
+
ref: refs.setFloating,
|
|
3242
|
+
style: {
|
|
3243
|
+
...floatingStyles,
|
|
3244
|
+
width: "280px"
|
|
3245
|
+
},
|
|
3246
|
+
...getFloatingProps(),
|
|
3247
|
+
role: "listbox",
|
|
3248
|
+
"aria-label": "Country selection",
|
|
3249
|
+
className: cn("z-[999] rounded-md border border-gray-200 bg-white shadow-lg", "overflow-hidden animate-in fade-in"),
|
|
3250
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3251
|
+
className: "p-2 border-b border-gray-100",
|
|
3252
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextInput, {
|
|
3253
|
+
placeholder: searchPlaceholder,
|
|
3254
|
+
value: searchQuery,
|
|
3255
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
3256
|
+
onKeyDown: handleKeyDown,
|
|
3257
|
+
autoFocus: true,
|
|
3258
|
+
"aria-label": "Search countries",
|
|
3259
|
+
role: "searchbox"
|
|
3260
|
+
})
|
|
3261
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3262
|
+
className: "max-h-[200px] overflow-auto p-1",
|
|
3263
|
+
children: filteredCountries.length > 0 ? filteredCountries.map((country, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3264
|
+
ref: (el) => optionRefs.current[index] = el,
|
|
3265
|
+
type: "button",
|
|
3266
|
+
role: "option",
|
|
3267
|
+
"aria-selected": selectedCountry?.code === country.code,
|
|
3268
|
+
onClick: () => handleSelect(country),
|
|
3269
|
+
className: cn("flex w-full items-center gap-3 rounded px-3 py-2 text-sm hover:bg-gray-50", selectedCountry?.code === country.code && "bg-gray-50", highlightedIndex === index && "bg-gray-50 border border-gray-100"),
|
|
3270
|
+
children: [
|
|
3271
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3272
|
+
className: "text-base",
|
|
3273
|
+
children: country.flag
|
|
3274
|
+
}),
|
|
3275
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3276
|
+
className: "flex-1 text-left truncate",
|
|
3277
|
+
children: country.name
|
|
3278
|
+
}),
|
|
3279
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3280
|
+
className: "text-xs font-medium text-gray-600",
|
|
3281
|
+
children: country.dial_code
|
|
3282
|
+
})
|
|
3283
|
+
]
|
|
3284
|
+
}, country.code + country.name + country.dial_code)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3285
|
+
className: "px-3 py-2 text-sm text-gray-500 text-center",
|
|
3286
|
+
children: "No countries found"
|
|
3287
|
+
})
|
|
3288
|
+
})]
|
|
3289
|
+
})
|
|
3290
|
+
}) })]
|
|
3291
|
+
});
|
|
3292
|
+
};
|
|
3293
|
+
const PhoneInput = ({ placeholder, searchPlaceHolder, defaultCountry = "NL", invalidMessage, defaultValue, onChange, ref,...rest }) => {
|
|
3294
|
+
const inputRef = (0, react.useRef)(null);
|
|
3295
|
+
const [phoneNumber, setPhoneNumber] = (0, react.useState)(defaultValue || "");
|
|
3296
|
+
const { validatePhone, stripCountryCode, getCountryCode, formatToInternational } = usePhoneNumber();
|
|
3297
|
+
const countrySelectorRef = (0, react.useRef)(null);
|
|
3298
|
+
const [error, setError] = (0, react.useState)("");
|
|
3299
|
+
const filterPhoneInput = (value) => value.replace(/[^0-9+()]/g, "");
|
|
3300
|
+
const handlePhoneNumberChange = (e) => {
|
|
3301
|
+
const filteredValue = filterPhoneInput(e.target.value);
|
|
3302
|
+
if (error) setError("");
|
|
3303
|
+
if (filteredValue.startsWith("+") && filteredValue.length > 1) {
|
|
3304
|
+
const countryCode = getCountryCode(filteredValue);
|
|
3305
|
+
if (countryCode) countrySelectorRef.current?.updateCountry(countryCode);
|
|
3306
|
+
const strippedNumber = stripCountryCode(filteredValue);
|
|
3307
|
+
setPhoneNumber(strippedNumber);
|
|
3308
|
+
return;
|
|
3309
|
+
} else setPhoneNumber(filteredValue);
|
|
3310
|
+
};
|
|
3311
|
+
const handleBlur = () => {
|
|
3312
|
+
if (phoneNumber.trim()) {
|
|
3313
|
+
const selectedCountry = countrySelectorRef.current?.getSelectedCountry();
|
|
3314
|
+
const validationResult = validatePhone(phoneNumber, { country: selectedCountry?.code });
|
|
3315
|
+
if (validationResult.isValid) {
|
|
3316
|
+
const formattedNumber = formatToInternational(phoneNumber, { country: selectedCountry?.code });
|
|
3317
|
+
const displayNumber = stripCountryCode(formattedNumber);
|
|
3318
|
+
const fullNumber = selectedCountry?.dial_code + displayNumber;
|
|
3319
|
+
setPhoneNumber(displayNumber);
|
|
3320
|
+
onChange(fullNumber);
|
|
3321
|
+
} else setError(invalidMessage);
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextInput, {
|
|
3325
|
+
...rest,
|
|
3326
|
+
value: phoneNumber,
|
|
3327
|
+
onChange: handlePhoneNumberChange,
|
|
3328
|
+
onBlur: handleBlur,
|
|
3329
|
+
leftSection: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CountrySelector, {
|
|
3330
|
+
ref: countrySelectorRef,
|
|
3331
|
+
searchPlaceholder: searchPlaceHolder,
|
|
3332
|
+
defaultCountry
|
|
3333
|
+
}),
|
|
3334
|
+
placeholder,
|
|
3335
|
+
error,
|
|
3336
|
+
ref: ref || inputRef,
|
|
3337
|
+
"aria-label": "Phone number input",
|
|
3338
|
+
inputMode: "tel",
|
|
3339
|
+
autoComplete: "tel"
|
|
3340
|
+
});
|
|
3341
|
+
};
|
|
3342
|
+
PhoneInput.displayName = "PhoneInput";
|
|
3343
|
+
|
|
3081
3344
|
//#endregion
|
|
3082
3345
|
//#region src/components/profileMenu/index.tsx
|
|
3083
3346
|
const ProfileMenu = ({ title, metaTitle, icon, content, disabled = false, classNames }) => {
|
|
@@ -3409,6 +3672,7 @@ exports.NumberField = NumberField;
|
|
|
3409
3672
|
exports.NumberedStepper = NumberedStepper;
|
|
3410
3673
|
exports.PageUnavailable = PageUnavailable;
|
|
3411
3674
|
exports.PasswordInput = PasswordInput;
|
|
3675
|
+
exports.PhoneInput = PhoneInput;
|
|
3412
3676
|
exports.Popover = Popover;
|
|
3413
3677
|
exports.ProfileMenu = ProfileMenu;
|
|
3414
3678
|
exports.ProgressBar = ProgressBar;
|