@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.
- 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 +214 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +214 -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 __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;
|