@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.
@@ -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 {};
@@ -1,4 +1,4 @@
1
- import { ComponentPropsWithoutRef, Ref } from 'react';
1
+ import { type ComponentPropsWithoutRef, type Ref } from 'react';
2
2
  interface Option {
3
3
  value: string;
4
4
  label: string;
@@ -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;