@youngonesworks/ui 0.1.115 → 0.1.117

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 __hooks_phone_usePhoneNumber = __toESM(require("@hooks/phone/usePhoneNumber"));
39
+ const __hooks_phone_usePhoneNumberPrefix = __toESM(require("@hooks/phone/usePhoneNumberPrefix"));
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,216 @@ const UnorderedListItem = ({ children, actionItem, className, header = false,...
3078
3078
  })]
3079
3079
  });
3080
3080
 
3081
+ //#endregion
3082
+ //#region src/components/phoneInput/index.tsx
3083
+ const CountrySelector = ({ searchPlaceholder, defaultCountry, ref }) => {
3084
+ const phoneNumberPrefixes = (0, __hooks_phone_usePhoneNumberPrefix.usePhoneNumberPrefix)(defaultCountry);
3085
+ const [selectedCountry, setSelectedCountry] = (0, react.useState)(phoneNumberPrefixes.find((country) => country.code === defaultCountry) || null);
3086
+ const [openDropdown, setOpenDropdown] = (0, react.useState)(false);
3087
+ const [searchQuery, setSearchQuery] = (0, react.useState)("");
3088
+ const [filteredCountries, setFilteredCountries] = (0, react.useState)(phoneNumberPrefixes);
3089
+ const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(-1);
3090
+ const optionRefs = (0, react.useRef)([]);
3091
+ const { refs, floatingStyles, context } = (0, __floating_ui_react.useFloating)({
3092
+ open: openDropdown,
3093
+ onOpenChange: (isOpen) => {
3094
+ setOpenDropdown(isOpen);
3095
+ if (isOpen) setHighlightedIndex(-1);
3096
+ },
3097
+ middleware: [
3098
+ (0, __floating_ui_react.offset)(4),
3099
+ (0, __floating_ui_react.flip)(),
3100
+ (0, __floating_ui_react.shift)()
3101
+ ],
3102
+ whileElementsMounted: __floating_ui_react.autoUpdate,
3103
+ placement: "bottom-start"
3104
+ });
3105
+ const click = (0, __floating_ui_react.useClick)(context);
3106
+ const dismiss = (0, __floating_ui_react.useDismiss)(context);
3107
+ const role = (0, __floating_ui_react.useRole)(context, { role: "combobox" });
3108
+ const { getReferenceProps, getFloatingProps } = (0, __floating_ui_react.useInteractions)([
3109
+ click,
3110
+ dismiss,
3111
+ role
3112
+ ]);
3113
+ const performSearch = (0, react.useCallback)((query) => {
3114
+ if (!query) return phoneNumberPrefixes;
3115
+ const lowerQuery = query.toLowerCase();
3116
+ return phoneNumberPrefixes.filter((country) => country.name.toLowerCase().includes(lowerQuery) || country.dial_code.toLowerCase().includes(lowerQuery) || country.code.toLowerCase().includes(lowerQuery));
3117
+ }, [phoneNumberPrefixes]);
3118
+ (0, react.useEffect)(() => {
3119
+ const results = performSearch(searchQuery);
3120
+ setFilteredCountries(results);
3121
+ setHighlightedIndex(-1);
3122
+ }, [searchQuery]);
3123
+ (0, react.useEffect)(() => {
3124
+ optionRefs.current = optionRefs.current.slice(0, filteredCountries.length);
3125
+ }, [filteredCountries]);
3126
+ (0, react.useEffect)(() => {
3127
+ if (highlightedIndex >= 0 && optionRefs.current[highlightedIndex]) {
3128
+ optionRefs.current[highlightedIndex]?.scrollIntoView({
3129
+ behavior: "auto",
3130
+ block: "nearest"
3131
+ });
3132
+ }
3133
+ }, [highlightedIndex]);
3134
+ const handleSelect = (country) => {
3135
+ setSelectedCountry(country);
3136
+ setOpenDropdown(false);
3137
+ };
3138
+ const handleKeyDown = (e) => {
3139
+ if (filteredCountries.length === 0) return;
3140
+ switch (e.key) {
3141
+ case "ArrowDown":
3142
+ e.preventDefault();
3143
+ setHighlightedIndex((prev) => prev < filteredCountries.length - 1 ? prev + 1 : 0);
3144
+ break;
3145
+ case "ArrowUp":
3146
+ e.preventDefault();
3147
+ setHighlightedIndex((prev) => prev > 0 ? prev - 1 : filteredCountries.length - 1);
3148
+ break;
3149
+ case "Enter": {
3150
+ e.preventDefault();
3151
+ if (highlightedIndex >= 0) handleSelect(filteredCountries[highlightedIndex]);
3152
+ break;
3153
+ }
3154
+ case "Escape":
3155
+ e.preventDefault();
3156
+ setOpenDropdown(false);
3157
+ break;
3158
+ }
3159
+ };
3160
+ (0, react.useImperativeHandle)(ref, () => ({
3161
+ updateCountry: (countryCode) => {
3162
+ const country = phoneNumberPrefixes.find((c) => c.dial_code === countryCode);
3163
+ if (country) setSelectedCountry(country);
3164
+ },
3165
+ getSelectedCountry: () => selectedCountry
3166
+ }));
3167
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3168
+ className: "relative",
3169
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3170
+ type: "button",
3171
+ ref: refs.setReference,
3172
+ ...getReferenceProps(),
3173
+ className: cn("flex h-10 items-center justify-between px-2 text-sm transition-colors"),
3174
+ "aria-haspopup": "listbox",
3175
+ "aria-expanded": openDropdown,
3176
+ "aria-label": `Country selector. Currently selected: ${selectedCountry?.name || "None"} ${selectedCountry?.dial_code || ""}`,
3177
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3178
+ className: "flex items-center gap-1",
3179
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3180
+ className: "text-xs font-medium",
3181
+ children: selectedCountry?.dial_code
3182
+ })
3183
+ })
3184
+ }), openDropdown && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__floating_ui_react.FloatingPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__floating_ui_react.FloatingFocusManager, {
3185
+ context,
3186
+ modal: false,
3187
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3188
+ ref: refs.setFloating,
3189
+ style: {
3190
+ ...floatingStyles,
3191
+ width: "280px"
3192
+ },
3193
+ ...getFloatingProps(),
3194
+ role: "listbox",
3195
+ "aria-label": "Country selection",
3196
+ className: cn("z-[999] rounded-md border border-gray-200 bg-white shadow-lg", "overflow-hidden animate-in fade-in"),
3197
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3198
+ className: "p-2 border-b border-gray-100",
3199
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextInput, {
3200
+ placeholder: searchPlaceholder,
3201
+ value: searchQuery,
3202
+ onChange: (e) => setSearchQuery(e.target.value),
3203
+ onKeyDown: handleKeyDown,
3204
+ autoFocus: true,
3205
+ "aria-label": "Search countries",
3206
+ role: "searchbox"
3207
+ })
3208
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3209
+ className: "max-h-[200px] overflow-auto p-1",
3210
+ children: filteredCountries.length > 0 ? filteredCountries.map((country, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3211
+ ref: (el) => optionRefs.current[index] = el,
3212
+ type: "button",
3213
+ role: "option",
3214
+ "aria-selected": selectedCountry?.code === country.code,
3215
+ onClick: () => handleSelect(country),
3216
+ 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"),
3217
+ children: [
3218
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3219
+ className: "text-base",
3220
+ children: country.flag
3221
+ }),
3222
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3223
+ className: "flex-1 text-left truncate",
3224
+ children: country.name
3225
+ }),
3226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3227
+ className: "text-xs font-medium text-gray-600",
3228
+ children: country.dial_code
3229
+ })
3230
+ ]
3231
+ }, country.code + country.name + country.dial_code)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3232
+ className: "px-3 py-2 text-sm text-gray-500 text-center",
3233
+ children: "No countries found"
3234
+ })
3235
+ })]
3236
+ })
3237
+ }) })]
3238
+ });
3239
+ };
3240
+ const PhoneInput = ({ placeholder, searchPlaceHolder, defaultCountry = "NL", invalidMessage, defaultValue, onChange, ref,...rest }) => {
3241
+ const inputRef = (0, react.useRef)(null);
3242
+ const [phoneNumber, setPhoneNumber] = (0, react.useState)(defaultValue || "");
3243
+ const { validatePhone, stripCountryCode, getCountryCode, formatToInternational } = (0, __hooks_phone_usePhoneNumber.usePhoneNumber)();
3244
+ const countrySelectorRef = (0, react.useRef)(null);
3245
+ const [error, setError] = (0, react.useState)("");
3246
+ const filterPhoneInput = (value) => value.replace(/[^0-9+()]/g, "");
3247
+ const handlePhoneNumberChange = (e) => {
3248
+ const filteredValue = filterPhoneInput(e.target.value);
3249
+ if (error) setError("");
3250
+ if (filteredValue.startsWith("+") && filteredValue.length > 1) {
3251
+ const countryCode = getCountryCode(filteredValue);
3252
+ if (countryCode) countrySelectorRef.current?.updateCountry(countryCode);
3253
+ const strippedNumber = stripCountryCode(filteredValue);
3254
+ setPhoneNumber(strippedNumber);
3255
+ return;
3256
+ } else setPhoneNumber(filteredValue);
3257
+ };
3258
+ const handleBlur = () => {
3259
+ if (phoneNumber.trim()) {
3260
+ const selectedCountry = countrySelectorRef.current?.getSelectedCountry();
3261
+ const validationResult = validatePhone(phoneNumber, { country: selectedCountry?.code });
3262
+ if (validationResult.isValid) {
3263
+ const formattedNumber = formatToInternational(phoneNumber, { country: selectedCountry?.code });
3264
+ const displayNumber = stripCountryCode(formattedNumber);
3265
+ const fullNumber = selectedCountry?.dial_code + displayNumber;
3266
+ setPhoneNumber(displayNumber);
3267
+ onChange(fullNumber);
3268
+ } else setError(invalidMessage);
3269
+ }
3270
+ };
3271
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextInput, {
3272
+ ...rest,
3273
+ value: phoneNumber,
3274
+ onChange: handlePhoneNumberChange,
3275
+ onBlur: handleBlur,
3276
+ leftSection: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CountrySelector, {
3277
+ ref: countrySelectorRef,
3278
+ searchPlaceholder: searchPlaceHolder,
3279
+ defaultCountry
3280
+ }),
3281
+ placeholder,
3282
+ error,
3283
+ ref: ref || inputRef,
3284
+ "aria-label": "Phone number input",
3285
+ inputMode: "tel",
3286
+ autoComplete: "tel"
3287
+ });
3288
+ };
3289
+ PhoneInput.displayName = "PhoneInput";
3290
+
3081
3291
  //#endregion
3082
3292
  //#region src/components/profileMenu/index.tsx
3083
3293
  const ProfileMenu = ({ title, metaTitle, icon, content, disabled = false, classNames }) => {
@@ -3409,6 +3619,7 @@ exports.NumberField = NumberField;
3409
3619
  exports.NumberedStepper = NumberedStepper;
3410
3620
  exports.PageUnavailable = PageUnavailable;
3411
3621
  exports.PasswordInput = PasswordInput;
3622
+ exports.PhoneInput = PhoneInput;
3412
3623
  exports.Popover = Popover;
3413
3624
  exports.ProfileMenu = ProfileMenu;
3414
3625
  exports.ProgressBar = ProgressBar;