Merge branch 'phone-number-input-fix'
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import Root from "./phone-number-input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as PhoneNumberInput,
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import { AsYouType, type CountryCode } from "libphonenumber-js";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type" | "value"> & {
|
||||
value?: string;
|
||||
defaultCountry?: CountryCode;
|
||||
}
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
defaultCountry = "US",
|
||||
class: className,
|
||||
"data-slot": dataSlot = "phone-input",
|
||||
oninput,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
|
||||
// Format initial value on mount
|
||||
let formattedValue = $state("");
|
||||
|
||||
$effect(() => {
|
||||
// Only format if value changed externally (not from our own input)
|
||||
if (value && value !== formattedValue) {
|
||||
const formatter = new AsYouType(defaultCountry);
|
||||
formattedValue = formatter.input(value);
|
||||
value = formattedValue;
|
||||
} else if (!value) {
|
||||
formattedValue = "";
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Count the number of digit characters before a given position in a string.
|
||||
*/
|
||||
function countDigitsBefore(str: string, position: number): number {
|
||||
let count = 0;
|
||||
for (let i = 0; i < position && i < str.length; i++) {
|
||||
if (/\d/.test(str[i])) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the position in a string where a given number of digits have been seen.
|
||||
*/
|
||||
function findPositionAfterDigits(str: string, digitCount: number): number {
|
||||
let count = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (/\d/.test(str[i])) {
|
||||
count++;
|
||||
if (count === digitCount) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return str.length;
|
||||
}
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const cursorPosition = input.selectionStart ?? 0;
|
||||
const rawValue = input.value;
|
||||
|
||||
// Count digits before cursor in the raw input
|
||||
const digitsBefore = countDigitsBefore(rawValue, cursorPosition);
|
||||
|
||||
// Format the input
|
||||
const formatter = new AsYouType(defaultCountry);
|
||||
const formatted = formatter.input(rawValue);
|
||||
|
||||
// Update state
|
||||
formattedValue = formatted;
|
||||
value = formatted;
|
||||
|
||||
// Calculate new cursor position: find where the same number of digits ends up
|
||||
const newPosition = findPositionAfterDigits(formatted, digitsBefore);
|
||||
|
||||
// Set cursor position synchronously after updating the input
|
||||
// We need to wait for Svelte to update the DOM
|
||||
queueMicrotask(() => {
|
||||
input.setSelectionRange(newPosition, newPosition);
|
||||
});
|
||||
|
||||
// Call the original oninput if provided
|
||||
if (oninput) {
|
||||
oninput(e as Event & { currentTarget: HTMLInputElement });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
type="tel"
|
||||
value={formattedValue}
|
||||
oninput={handleInput}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "$lib/components/ui/card";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { PhoneNumberInput } from "$lib/components/ui/phone-number-input";
|
||||
import { LoadingButton } from "$lib/components/ui/loading-button";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { cn } from "$lib/utils";
|
||||
@@ -224,9 +225,8 @@ function getInitials(name: string | null | undefined): string {
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="phoneNumber">Phone number</Label>
|
||||
<Input
|
||||
<PhoneNumberInput
|
||||
id="phoneNumber"
|
||||
type="tel"
|
||||
placeholder="+1 555 123 4567"
|
||||
bind:value={phoneNumber}
|
||||
onblur={handlePhoneBlur}
|
||||
|
||||
Reference in New Issue
Block a user