@clubmed/trident-ui 2.0.0-beta.38 → 2.0.0-beta.40
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/examples/switch-demo.js +1 -1
- package/examples/switch-demo.js.map +1 -1
- package/package.json +1 -1
- package/styles/ui/controls.css +8 -52
- package/ui/forms/PhoneField.d.ts +5 -10
- package/ui/forms/PhoneField.js +29 -211
- package/ui/forms/PhoneField.js.map +1 -1
- package/ui/forms/PhoneFieldFullInput.d.ts +16 -0
- package/ui/forms/PhoneFieldFullInput.js +116 -0
- package/ui/forms/PhoneFieldFullInput.js.map +1 -0
- package/ui/forms/PhoneFieldSplitInput.d.ts +19 -0
- package/ui/forms/PhoneFieldSplitInput.js +159 -0
- package/ui/forms/PhoneFieldSplitInput.js.map +1 -0
- package/ui/forms/Switch.d.ts +3 -3
- package/ui/forms/Switch.js +51 -34
- package/ui/forms/Switch.js.map +1 -1
- package/ui/helpers/phone/formatters.d.ts +11 -0
- package/ui/helpers/phone/formatters.js +32 -1
- package/ui/helpers/phone/formatters.js.map +1 -1
- package/ui/helpers/phone/index.d.ts +1 -1
- package/ui/helpers/phone/index.js +3 -3
package/examples/switch-demo.js
CHANGED
|
@@ -9,7 +9,7 @@ function i() {
|
|
|
9
9
|
className: "flex items-center gap-3",
|
|
10
10
|
children: [/* @__PURE__ */ n(e, {
|
|
11
11
|
checked: i,
|
|
12
|
-
|
|
12
|
+
onChange: (e) => a(e.target.checked)
|
|
13
13
|
}), /* @__PURE__ */ n("span", {
|
|
14
14
|
className: "text-b2 font-semibold",
|
|
15
15
|
children: "Enable notifications"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"switch-demo.js","names":[],"sources":["../../lib/examples/switch-demo.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Switch } from '@/ui/forms/Switch';\n\nexport default function SwitchDemo() {\n const [isEnabled, setIsEnabled] = useState(false);\n\n return (\n <div className=\"flex items-center gap-3\">\n <Switch checked={isEnabled}
|
|
1
|
+
{"version":3,"file":"switch-demo.js","names":[],"sources":["../../lib/examples/switch-demo.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Switch } from '@/ui/forms/Switch';\n\nexport default function SwitchDemo() {\n const [isEnabled, setIsEnabled] = useState(false);\n\n return (\n <div className=\"flex items-center gap-3\">\n <Switch checked={isEnabled} onChange={(e) => setIsEnabled(e.target.checked)} />\n <span className=\"text-b2 font-semibold\">Enable notifications</span>\n </div>\n );\n}\n"],"mappings":";;;;;AAKA,SAAwB,IAAa;CACnC,IAAM,CAAC,GAAW,KAAgB,EAAS,GAAM;AAEjD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GAAQ,SAAS;GAAW,WAAW,MAAM,EAAa,EAAE,OAAO,QAAA;GAAY,CAAA,EAC/E,kBAAC,QAAD;GAAM,WAAU;aAAwB;GAA2B,CAAA,CAAA"}
|
package/package.json
CHANGED
package/styles/ui/controls.css
CHANGED
|
@@ -90,62 +90,21 @@
|
|
|
90
90
|
--tw-ring-color: color-mix(in oklab, var(--color-lavender) 20%, transparent);
|
|
91
91
|
}
|
|
92
92
|
/* Switch */
|
|
93
|
-
|
|
94
|
-
@apply transition-all;
|
|
93
|
+
input[role='switch'][data-name='Switch'] + span {
|
|
95
94
|
@apply relative flex items-center transition-colors duration-200;
|
|
96
95
|
@apply ring-0;
|
|
97
96
|
--tw-ring-color: color-mix(in oklab, var(--color-lavender) 0%, transparent);
|
|
98
|
-
background-color: var(--color-middleGrey);
|
|
99
|
-
border-radius: var(--radius-pill);
|
|
100
|
-
}
|
|
101
|
-
label > span > input[type='checkbox'][role='switch'] + span > svg {
|
|
102
|
-
transition: all var(--transition-duration-boop) var(--transition-fn-boop);
|
|
103
|
-
transform: translate(calc(0% + 4px));
|
|
104
|
-
}
|
|
105
|
-
label > span > input[type='checkbox'][role='switch'] + span > svg circle + circle {
|
|
106
|
-
opacity: 1;
|
|
107
|
-
transition: opacity var(--transition-duration-boop) var(--transition-fn-boop);
|
|
108
|
-
}
|
|
109
|
-
label > span > input[type='checkbox'][role='switch'] + span > svg path {
|
|
110
|
-
opacity: 0;
|
|
111
|
-
scale: 0;
|
|
112
|
-
transform-origin: center;
|
|
113
|
-
transition:
|
|
114
|
-
opacity var(--transition-duration-boop) var(--transition-fn-boop),
|
|
115
|
-
scale var(--transition-duration-boop) var(--transition-fn-boop);
|
|
116
|
-
}
|
|
117
|
-
label > span > input[type='checkbox'][role='switch']:checked + span {
|
|
118
|
-
background-color: var(--color-saffron);
|
|
119
|
-
}
|
|
120
|
-
label > span > input[type='checkbox'][role='switch']:checked + span > svg {
|
|
121
|
-
transform: translate(calc(100% + 4px));
|
|
122
|
-
}
|
|
123
|
-
label > span > input[type='checkbox'][role='switch']:checked + span > svg circle + circle {
|
|
124
|
-
opacity: 0;
|
|
125
|
-
}
|
|
126
|
-
label > span > input[type='checkbox'][role='switch']:checked + span > svg path {
|
|
127
|
-
scale: 1;
|
|
128
|
-
opacity: 1;
|
|
129
|
-
}
|
|
130
|
-
label > span > input[type='checkbox'][role='switch']:focus-visible + span {
|
|
131
|
-
@apply ring-8;
|
|
132
|
-
--tw-ring-color: color-mix(in oklab, var(--color-lavender) 20%, transparent);
|
|
133
97
|
}
|
|
134
98
|
|
|
135
|
-
|
|
136
|
-
@apply relative flex items-center transition-colors duration-200;
|
|
137
|
-
@apply ring-0;
|
|
138
|
-
--tw-ring-color: color-mix(in oklab, var(--color-lavender) 0%, transparent);
|
|
139
|
-
}
|
|
140
|
-
button[role='switch'][data-name='Switch'] svg {
|
|
99
|
+
input[role='switch'][data-name='Switch'] + span svg {
|
|
141
100
|
transition: all var(--transition-duration-boop) var(--transition-fn-boop);
|
|
142
101
|
transform: translate(calc(0%));
|
|
143
102
|
}
|
|
144
|
-
|
|
103
|
+
input[role='switch'][data-name='Switch'] + span svg circle + circle {
|
|
145
104
|
opacity: 1;
|
|
146
105
|
transition: opacity var(--transition-duration-boop) var(--transition-fn-boop);
|
|
147
106
|
}
|
|
148
|
-
|
|
107
|
+
input[role='switch'][data-name='Switch'] + span svg path {
|
|
149
108
|
opacity: 0;
|
|
150
109
|
scale: 0;
|
|
151
110
|
transform-origin: center;
|
|
@@ -153,20 +112,17 @@
|
|
|
153
112
|
opacity var(--transition-duration-boop) var(--transition-fn-boop),
|
|
154
113
|
scale var(--transition-duration-boop) var(--transition-fn-boop);
|
|
155
114
|
}
|
|
156
|
-
|
|
157
|
-
background-color: var(--color-saffron);
|
|
158
|
-
}
|
|
159
|
-
button[role='switch'][data-name='Switch'][aria-checked='true'] svg {
|
|
115
|
+
input[role='switch'][data-name='Switch']:checked + span svg {
|
|
160
116
|
transform: translate(calc(100%));
|
|
161
117
|
}
|
|
162
|
-
|
|
118
|
+
input[role='switch'][data-name='Switch']:checked + span svg circle + circle {
|
|
163
119
|
opacity: 0;
|
|
164
120
|
}
|
|
165
|
-
|
|
121
|
+
input[role='switch'][data-name='Switch']:checked + span svg path {
|
|
166
122
|
scale: 1;
|
|
167
123
|
opacity: 1;
|
|
168
124
|
}
|
|
169
|
-
|
|
125
|
+
input[role='switch'][data-name='Switch']:focus-visible + span {
|
|
170
126
|
@apply ring-8;
|
|
171
127
|
--tw-ring-color: color-mix(in oklab, var(--color-lavender) 20%, transparent);
|
|
172
128
|
}
|
package/ui/forms/PhoneField.d.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
+
import { IconicTypes } from '@clubmed/trident-icons';
|
|
1
2
|
import { FormControlProps } from './FormControl';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { PhoneFieldFullProps } from './PhoneFieldFullInput';
|
|
4
|
+
import { PhoneFieldSplitProps } from './PhoneFieldSplitInput';
|
|
4
5
|
export interface PhoneValue {
|
|
5
6
|
full: string;
|
|
6
7
|
prefix?: string;
|
|
7
8
|
number?: string;
|
|
8
9
|
raw: string;
|
|
9
10
|
}
|
|
10
|
-
export
|
|
11
|
+
export type PhoneFieldProps<Value = PhoneValue> = FormControlProps<Value> & PhoneFieldFullProps & PhoneFieldSplitProps & {
|
|
11
12
|
mode?: 'full' | 'split';
|
|
12
13
|
pattern?: string;
|
|
13
|
-
prefixes?: PhonePrefix[];
|
|
14
|
-
defaultPrefix?: string;
|
|
15
|
-
description?: string;
|
|
16
|
-
icon?: IconicNames;
|
|
17
14
|
iconType?: IconicTypes;
|
|
18
|
-
errorMessage?: string;
|
|
19
|
-
dataTestId?: string;
|
|
20
15
|
placeholder?: string;
|
|
21
|
-
}
|
|
16
|
+
};
|
|
22
17
|
export declare const PhoneField: <Value = PhoneValue>(props: PhoneFieldProps<Value>) => import("react/jsx-runtime").JSX.Element;
|
package/ui/forms/PhoneField.js
CHANGED
|
@@ -1,219 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import { Icon as f } from "@clubmed/trident-icons";
|
|
8
|
-
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
1
|
+
import { FormControl as e } from "./FormControl.js";
|
|
2
|
+
import { PhoneFieldFullInput as t } from "./PhoneFieldFullInput.js";
|
|
3
|
+
import { PhoneFieldSplitInput as n } from "./PhoneFieldSplitInput.js";
|
|
4
|
+
import { useId as r } from "react";
|
|
5
|
+
import "@clubmed/trident-icons";
|
|
6
|
+
import { jsx as i } from "react/jsx-runtime";
|
|
9
7
|
//#region lib/ui/forms/PhoneField.tsx
|
|
10
|
-
var
|
|
11
|
-
let
|
|
12
|
-
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let r = 0, i = 0;
|
|
18
|
-
for (let a = 0; a < t.length; a++) if (/\d/.test(t[a])) {
|
|
19
|
-
if (i < n.length) {
|
|
20
|
-
i++;
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (r++, r === e) return a + 1;
|
|
24
|
-
}
|
|
25
|
-
return t.length;
|
|
26
|
-
}, y = (o) => {
|
|
27
|
-
let y = l(), { id: b = y, name: x = b, label: S, value: C, mode: w = "full", pattern: T = "## ## ## ## ##", prefixes: E = h, defaultPrefix: D = E[0]?.code || "+1", description: O, validationStatus: k = "default", icon: A, iconType: j, errorMessage: M, disabled: N = !1, required: P = !1, hideRequiredStar: F, className: I, dataTestId: L = "PhoneField", placeholder: R = "", onChange: z, ...B } = o, V = u(null), H = u(null), U = u(null), [W, G] = d(D), [K, q] = d(""), [J, Y] = d(""), X = t({
|
|
28
|
-
isDisabled: N,
|
|
29
|
-
validationStatus: k
|
|
30
|
-
});
|
|
31
|
-
c(() => {
|
|
32
|
-
let e = w === "full" ? V : H;
|
|
33
|
-
U.current !== null && e.current && (e.current.setSelectionRange(U.current, U.current), U.current = null);
|
|
34
|
-
}, [
|
|
35
|
-
J,
|
|
36
|
-
K,
|
|
37
|
-
w
|
|
38
|
-
]), c(() => {
|
|
39
|
-
if (C && typeof C == "object" && "full" in C) {
|
|
40
|
-
let e = C;
|
|
41
|
-
w === "full" ? Y(e.full || "") : (G(e.prefix || D), q(e.number || ""));
|
|
42
|
-
}
|
|
43
|
-
}, [
|
|
44
|
-
C,
|
|
45
|
-
w,
|
|
46
|
-
D
|
|
47
|
-
]);
|
|
48
|
-
let Z = s((e, t, n) => {
|
|
49
|
-
let r = {
|
|
50
|
-
full: e,
|
|
51
|
-
prefix: t,
|
|
52
|
-
number: n,
|
|
53
|
-
raw: i(e)
|
|
54
|
-
};
|
|
55
|
-
z?.(x, r);
|
|
56
|
-
}, [z, x]), Q = s((e, t, n) => {
|
|
57
|
-
if (e.key !== "Backspace" && e.key !== "Delete") return;
|
|
58
|
-
let r = e.currentTarget, o = r.selectionStart || 0;
|
|
59
|
-
if (o !== (r.selectionEnd || 0)) return;
|
|
60
|
-
let { literalPrefixDigits: s } = g(T), c = e.key === "Backspace" ? t[o - 1] : t[o];
|
|
61
|
-
if (!c || /\d/.test(c)) return;
|
|
62
|
-
e.preventDefault();
|
|
63
|
-
let l = _(i(t), s), u = _(i(t.substring(0, o)), s).length, d, f;
|
|
64
|
-
if (e.key === "Backspace") {
|
|
65
|
-
if (u === 0) return;
|
|
66
|
-
d = l.slice(0, u - 1) + l.slice(u), f = u - 1;
|
|
67
|
-
} else {
|
|
68
|
-
if (u >= l.length) return;
|
|
69
|
-
d = l.slice(0, u) + l.slice(u + 1), f = u;
|
|
70
|
-
}
|
|
71
|
-
let p = a(d, T);
|
|
72
|
-
U.current = v(f, p, s), n ? (Y(p), Z(p)) : (q(p), G((e) => (Z(`${e} ${p}`.trim(), e, p), e)));
|
|
73
|
-
}, [T, Z]), $ = s((e, t) => {
|
|
74
|
-
let n = e.target.value, o = e.target.selectionStart || 0, { literalPrefixDigits: s } = g(T), c = _(i(n), s), l = r(T);
|
|
75
|
-
if (c.length > l) return;
|
|
76
|
-
let u = _(i(n.substring(0, o)), s).length, d = a(c, T);
|
|
77
|
-
U.current = v(u, d, s), t ? (Y(d), Z(d)) : (q(d), G((e) => (Z(`${e} ${d}`.trim(), e, d), e)));
|
|
78
|
-
}, [T, Z]), ee = s((e) => {
|
|
79
|
-
G(e), q((t) => (Z(`${e} ${t}`.trim(), e, t), t));
|
|
80
|
-
}, [Z]);
|
|
81
|
-
return /* @__PURE__ */ p(n, {
|
|
82
|
-
id: b,
|
|
83
|
-
label: S,
|
|
84
|
-
className: I,
|
|
85
|
-
description: O,
|
|
8
|
+
var a = (a) => {
|
|
9
|
+
let { id: o, name: s, value: c, label: l, className: u, description: d, mode: f = "full", dataTestId: p = "PhoneField", disabled: m, required: h, hideRequiredStar: g, validationStatus: _, errorMessage: v, onChange: y, ...b } = a, x = y, S = r();
|
|
10
|
+
return /* @__PURE__ */ i(e, {
|
|
11
|
+
id: o || S,
|
|
12
|
+
label: l,
|
|
13
|
+
className: u,
|
|
14
|
+
description: d,
|
|
86
15
|
dataName: "PhoneField",
|
|
87
|
-
dataTestId:
|
|
88
|
-
disabled:
|
|
89
|
-
required:
|
|
90
|
-
hideRequiredStar:
|
|
91
|
-
validationStatus:
|
|
92
|
-
errorMessage:
|
|
93
|
-
children:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
required: P,
|
|
103
|
-
value: J,
|
|
104
|
-
onChange: (e) => $(e, !0),
|
|
105
|
-
onKeyDown: (e) => Q(e, J, !0),
|
|
106
|
-
placeholder: R,
|
|
107
|
-
className: e("text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none", {
|
|
108
|
-
"border-middleGrey focus:border-black active:border-black": X === "default",
|
|
109
|
-
"ps-[52px]": A,
|
|
110
|
-
"pe-[52px]": X === "error" || X === "success",
|
|
111
|
-
"bg-white text-black": X !== "disabled",
|
|
112
|
-
"bg-pearl border-middleGrey": X === "disabled",
|
|
113
|
-
"border-red": X === "error",
|
|
114
|
-
"border-green": X === "success"
|
|
115
|
-
}),
|
|
116
|
-
"aria-label": x
|
|
117
|
-
}), /* @__PURE__ */ m("div", {
|
|
118
|
-
className: e("pointer-events-none absolute inset-0 flex items-center justify-between px-20 py-12", {
|
|
119
|
-
"text-grey": X === "disabled",
|
|
120
|
-
"text-red": X === "error",
|
|
121
|
-
"text-green": X === "success"
|
|
122
|
-
}),
|
|
123
|
-
children: [A && /* @__PURE__ */ p(f, {
|
|
124
|
-
name: A,
|
|
125
|
-
width: "24px"
|
|
126
|
-
}), /* @__PURE__ */ m("span", {
|
|
127
|
-
className: "ms-auto flex gap-x-8",
|
|
128
|
-
children: [X === "error" && /* @__PURE__ */ p(f, {
|
|
129
|
-
name: "CrossDefault",
|
|
130
|
-
width: "24px",
|
|
131
|
-
type: j
|
|
132
|
-
}), X === "success" && /* @__PURE__ */ p(f, {
|
|
133
|
-
name: "CheckDefault",
|
|
134
|
-
width: "24px",
|
|
135
|
-
type: j
|
|
136
|
-
})]
|
|
137
|
-
})]
|
|
138
|
-
})]
|
|
139
|
-
}) : /* @__PURE__ */ m("div", {
|
|
140
|
-
className: "flex gap-8",
|
|
141
|
-
children: [/* @__PURE__ */ m("div", {
|
|
142
|
-
className: e("relative rounded-pill z-0 w-[130px] flex-shrink-0", {
|
|
143
|
-
"bg-white": X !== "disabled",
|
|
144
|
-
"bg-pearl border-middleGrey": X === "disabled"
|
|
145
|
-
}),
|
|
146
|
-
children: [/* @__PURE__ */ p("select", {
|
|
147
|
-
disabled: N,
|
|
148
|
-
value: W,
|
|
149
|
-
onChange: (e) => ee(e.target.value),
|
|
150
|
-
className: e("text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-semibold outline-none appearance-none bg-transparent", {
|
|
151
|
-
"border-middleGrey focus:border-black active:border-black": X === "default",
|
|
152
|
-
"text-black": X !== "disabled",
|
|
153
|
-
"bg-pearl border-middleGrey": X === "disabled",
|
|
154
|
-
"border-red": X === "error",
|
|
155
|
-
"border-green": X === "success",
|
|
156
|
-
"pe-[40px]": !0
|
|
157
|
-
}),
|
|
158
|
-
id: `${b}-prefix`,
|
|
159
|
-
"aria-label": `${x}-prefix`,
|
|
160
|
-
children: E.map((e) => /* @__PURE__ */ p("option", {
|
|
161
|
-
value: e.code,
|
|
162
|
-
children: e.label ?? e.code
|
|
163
|
-
}, e.code))
|
|
164
|
-
}), /* @__PURE__ */ p("div", {
|
|
165
|
-
className: "pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12 -z-1",
|
|
166
|
-
children: /* @__PURE__ */ p(f, {
|
|
167
|
-
name: "ArrowDefaultDown",
|
|
168
|
-
type: "svg",
|
|
169
|
-
width: "24px",
|
|
170
|
-
color: "black"
|
|
171
|
-
})
|
|
172
|
-
})]
|
|
173
|
-
}), /* @__PURE__ */ m("div", {
|
|
174
|
-
className: "relative flex-1",
|
|
175
|
-
children: [/* @__PURE__ */ p("input", {
|
|
176
|
-
ref: H,
|
|
177
|
-
type: "tel",
|
|
178
|
-
disabled: N,
|
|
179
|
-
required: P,
|
|
180
|
-
value: K,
|
|
181
|
-
onChange: (e) => $(e, !1),
|
|
182
|
-
onKeyDown: (e) => Q(e, K, !1),
|
|
183
|
-
placeholder: R,
|
|
184
|
-
className: e("text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none", {
|
|
185
|
-
"border-middleGrey focus:border-black active:border-black": X === "default",
|
|
186
|
-
"pe-[52px]": X === "error" || X === "success",
|
|
187
|
-
"bg-white text-black": X !== "disabled",
|
|
188
|
-
"bg-pearl border-middleGrey": X === "disabled",
|
|
189
|
-
"border-red": X === "error",
|
|
190
|
-
"border-green": X === "success"
|
|
191
|
-
}),
|
|
192
|
-
"aria-label": `${x}-number`
|
|
193
|
-
}), /* @__PURE__ */ p("div", {
|
|
194
|
-
className: e("pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12", {
|
|
195
|
-
"text-grey": X === "disabled",
|
|
196
|
-
"text-red": X === "error",
|
|
197
|
-
"text-green": X === "success"
|
|
198
|
-
}),
|
|
199
|
-
children: /* @__PURE__ */ m("span", {
|
|
200
|
-
className: "flex gap-x-8",
|
|
201
|
-
children: [X === "error" && /* @__PURE__ */ p(f, {
|
|
202
|
-
name: "CrossDefault",
|
|
203
|
-
width: "24px",
|
|
204
|
-
type: j
|
|
205
|
-
}), X === "success" && /* @__PURE__ */ p(f, {
|
|
206
|
-
name: "CheckDefault",
|
|
207
|
-
width: "24px",
|
|
208
|
-
type: j
|
|
209
|
-
})]
|
|
210
|
-
})
|
|
211
|
-
})]
|
|
212
|
-
})]
|
|
16
|
+
dataTestId: p,
|
|
17
|
+
disabled: m,
|
|
18
|
+
required: h,
|
|
19
|
+
hideRequiredStar: g,
|
|
20
|
+
validationStatus: _,
|
|
21
|
+
errorMessage: v,
|
|
22
|
+
children: i(f === "full" ? t : n, {
|
|
23
|
+
id: o,
|
|
24
|
+
name: s,
|
|
25
|
+
value: c,
|
|
26
|
+
disabled: m,
|
|
27
|
+
required: h,
|
|
28
|
+
validationStatus: _,
|
|
29
|
+
onChange: x,
|
|
30
|
+
...b
|
|
213
31
|
})
|
|
214
32
|
});
|
|
215
33
|
};
|
|
216
34
|
//#endregion
|
|
217
|
-
export {
|
|
35
|
+
export { a as PhoneField };
|
|
218
36
|
|
|
219
37
|
//# sourceMappingURL=PhoneField.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhoneField.js","names":[],"sources":["../../../lib/ui/forms/PhoneField.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport { useInternalStatus } from '../hooks/useInternalStatus';\nimport { FormControl, type FormControlProps } from './FormControl';\nimport { Icon, type IconicNames, type IconicTypes } from '@clubmed/trident-icons';\nimport {\n type ChangeEvent,\n type KeyboardEvent,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\nimport {\n extractDigits,\n formatWithPattern,\n countDigitPlaceholders,\n DEFAULT_PHONE_PREFIXES,\n type PhonePrefix,\n} from '../helpers/phone';\n\nexport interface PhoneValue {\n full: string;\n prefix?: string;\n number?: string;\n raw: string;\n}\n\nexport interface PhoneFieldProps<Value = PhoneValue> extends FormControlProps<Value> {\n mode?: 'full' | 'split';\n pattern?: string;\n prefixes?: PhonePrefix[];\n defaultPrefix?: string;\n description?: string;\n icon?: IconicNames;\n iconType?: IconicTypes;\n errorMessage?: string;\n dataTestId?: string;\n placeholder?: string;\n}\n\n// Extract default to constant to prevent breaking memoization (rerender-memo-with-default-value)\nconst DEFAULT_PREFIXES = DEFAULT_PHONE_PREFIXES;\n\n// Extract literal prefix from pattern (everything before first #)\nconst getLiteralPrefixInfo = (pattern: string) => {\n const firstHashIndex = pattern.indexOf('#');\n const literalPrefix = firstHashIndex > 0 ? pattern.substring(0, firstHashIndex) : '';\n const literalPrefixDigits = extractDigits(literalPrefix);\n return { literalPrefix, literalPrefixDigits };\n};\n\n// Strip literal prefix digits from digit string\nconst stripLiteralPrefix = (allDigits: string, literalPrefixDigits: string) => {\n if (literalPrefixDigits.length > 0 && allDigits.startsWith(literalPrefixDigits)) {\n return allDigits.substring(literalPrefixDigits.length);\n }\n return allDigits;\n};\n\n// Calculate cursor position after formatting\nconst calculateCursorPosition = (\n digitsBeforeCursor: number,\n formatted: string,\n literalPrefixDigits: string,\n) => {\n let digitCount = 0;\n let skippedPrefixDigits = 0;\n\n for (let i = 0; i < formatted.length; i++) {\n if (/\\d/.test(formatted[i])) {\n if (skippedPrefixDigits < literalPrefixDigits.length) {\n skippedPrefixDigits++;\n continue;\n }\n\n digitCount++;\n if (digitCount === digitsBeforeCursor) {\n return i + 1;\n }\n }\n }\n\n return formatted.length;\n};\n\nexport const PhoneField = <Value = PhoneValue,>(props: PhoneFieldProps<Value>) => {\n const internalId = useId();\n\n const {\n id = internalId,\n name = id,\n label,\n value: externalValue,\n mode = 'full',\n pattern = '## ## ## ## ##',\n prefixes = DEFAULT_PREFIXES,\n defaultPrefix = prefixes[0]?.code || '+1',\n description,\n validationStatus = 'default',\n icon,\n iconType,\n errorMessage,\n disabled = false,\n required = false,\n hideRequiredStar,\n className,\n dataTestId = 'PhoneField',\n placeholder = '',\n onChange,\n ...rest\n } = props;\n\n const inputRef = useRef<HTMLInputElement>(null);\n const numberInputRef = useRef<HTMLInputElement>(null);\n const cursorPositionRef = useRef<number | null>(null);\n const [prefix, setPrefix] = useState(defaultPrefix);\n const [number, setNumber] = useState('');\n const [fullNumber, setFullNumber] = useState('');\n\n const internalStatus = useInternalStatus({\n isDisabled: disabled,\n validationStatus,\n });\n\n // Apply cursor position after state updates\n useEffect(() => {\n const ref = mode === 'full' ? inputRef : numberInputRef;\n if (cursorPositionRef.current !== null && ref.current) {\n ref.current.setSelectionRange(cursorPositionRef.current, cursorPositionRef.current);\n cursorPositionRef.current = null;\n }\n }, [fullNumber, number, mode]);\n\n // Initialize from external value\n useEffect(() => {\n if (externalValue && typeof externalValue === 'object' && 'full' in externalValue) {\n const phoneValue = externalValue as unknown as PhoneValue;\n if (mode === 'full') {\n setFullNumber(phoneValue.full || '');\n } else {\n setPrefix(phoneValue.prefix || defaultPrefix);\n setNumber(phoneValue.number || '');\n }\n }\n }, [externalValue, mode, defaultPrefix]);\n\n // Emit onChange with PhoneValue structure\n const emitChange = useCallback(\n (full: string, prefix?: string, number?: string) => {\n const raw = extractDigits(full);\n const phoneValue: PhoneValue = {\n full,\n prefix,\n number,\n raw,\n };\n onChange?.(name, phoneValue as Value);\n },\n [onChange, name],\n );\n\n // Unified handler for backspace/delete on non-digit characters\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>, currentValue: string, isFullMode: boolean) => {\n if (e.key !== 'Backspace' && e.key !== 'Delete') {\n return;\n }\n\n const input = e.currentTarget;\n const cursorPosition = input.selectionStart || 0;\n const selectionEnd = input.selectionEnd || 0;\n\n // If there's a selection, let default behavior handle it\n if (cursorPosition !== selectionEnd) {\n return;\n }\n\n const { literalPrefixDigits } = getLiteralPrefixInfo(pattern);\n\n // Check if we're on a non-digit character\n const targetChar =\n e.key === 'Backspace' ? currentValue[cursorPosition - 1] : currentValue[cursorPosition];\n\n if (!targetChar || /\\d/.test(targetChar)) {\n return;\n }\n\n e.preventDefault();\n\n // Extract and clean digits\n const allDigits = stripLiteralPrefix(extractDigits(currentValue), literalPrefixDigits);\n const digitsBeforeCursorStr = stripLiteralPrefix(\n extractDigits(currentValue.substring(0, cursorPosition)),\n literalPrefixDigits,\n );\n const digitsBeforeCursor = digitsBeforeCursorStr.length;\n\n // Calculate new digits based on key\n let newDigits: string;\n let targetCursorDigits: number;\n\n if (e.key === 'Backspace') {\n if (digitsBeforeCursor === 0) return;\n newDigits =\n allDigits.slice(0, digitsBeforeCursor - 1) + allDigits.slice(digitsBeforeCursor);\n targetCursorDigits = digitsBeforeCursor - 1;\n } else {\n // Delete\n if (digitsBeforeCursor >= allDigits.length) return;\n newDigits =\n allDigits.slice(0, digitsBeforeCursor) + allDigits.slice(digitsBeforeCursor + 1);\n targetCursorDigits = digitsBeforeCursor;\n }\n\n // Format and update\n const formatted = formatWithPattern(newDigits, pattern);\n const newCursor = calculateCursorPosition(targetCursorDigits, formatted, literalPrefixDigits);\n cursorPositionRef.current = newCursor;\n\n if (isFullMode) {\n setFullNumber(formatted);\n emitChange(formatted);\n } else {\n setNumber(formatted);\n setPrefix((currentPrefix) => {\n const combined = `${currentPrefix} ${formatted}`.trim();\n emitChange(combined, currentPrefix, formatted);\n return currentPrefix;\n });\n }\n },\n [pattern, emitChange],\n );\n\n // Unified handler for input changes\n const handleInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>, isFullMode: boolean) => {\n const inputValue = e.target.value;\n const cursorPosition = e.target.selectionStart || 0;\n\n const { literalPrefixDigits } = getLiteralPrefixInfo(pattern);\n\n // Extract and clean digits\n const allDigits = stripLiteralPrefix(extractDigits(inputValue), literalPrefixDigits);\n\n // Check against pattern limit\n const maxDigits = countDigitPlaceholders(pattern);\n if (allDigits.length > maxDigits) {\n return;\n }\n\n // Count digits before cursor\n const digitsBeforeCursorInInput = stripLiteralPrefix(\n extractDigits(inputValue.substring(0, cursorPosition)),\n literalPrefixDigits,\n );\n const digitsBeforeCursor = digitsBeforeCursorInInput.length;\n\n // Format the value\n const formatted = formatWithPattern(allDigits, pattern);\n\n // Calculate cursor position\n const newCursor = calculateCursorPosition(digitsBeforeCursor, formatted, literalPrefixDigits);\n cursorPositionRef.current = newCursor;\n\n // Update state\n if (isFullMode) {\n setFullNumber(formatted);\n emitChange(formatted);\n } else {\n setNumber(formatted);\n setPrefix((currentPrefix) => {\n const combined = `${currentPrefix} ${formatted}`.trim();\n emitChange(combined, currentPrefix, formatted);\n return currentPrefix;\n });\n }\n },\n [pattern, emitChange],\n );\n\n // Split mode: handle prefix change\n const handlePrefixChange = useCallback(\n (newPrefix: string) => {\n setPrefix(newPrefix);\n setNumber((currentNumber) => {\n const combined = `${newPrefix} ${currentNumber}`.trim();\n emitChange(combined, newPrefix, currentNumber);\n return currentNumber;\n });\n },\n [emitChange],\n );\n\n const formControlProps = {\n id,\n label,\n className,\n description,\n dataName: 'PhoneField',\n dataTestId,\n disabled,\n required,\n hideRequiredStar,\n validationStatus,\n errorMessage,\n };\n\n return (\n <FormControl {...formControlProps}>\n {mode === 'full' ? (\n // Full mode: single input with pattern masking\n <div className=\"relative\">\n <input\n {...(rest as any)}\n ref={inputRef}\n id={id}\n name={name}\n type=\"tel\"\n disabled={disabled}\n required={required}\n value={fullNumber}\n onChange={(e) => handleInputChange(e, true)}\n onKeyDown={(e) => handleKeyDown(e, fullNumber, true)}\n placeholder={placeholder}\n className={clsx(\n 'text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none',\n {\n 'border-middleGrey focus:border-black active:border-black':\n internalStatus === 'default',\n 'ps-[52px]': icon,\n 'pe-[52px]': internalStatus === 'error' || internalStatus === 'success',\n 'bg-white text-black': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n },\n )}\n aria-label={name}\n />\n\n <div\n className={clsx(\n 'pointer-events-none absolute inset-0 flex items-center justify-between px-20 py-12',\n {\n 'text-grey': internalStatus === 'disabled',\n 'text-red': internalStatus === 'error',\n 'text-green': internalStatus === 'success',\n },\n )}\n >\n {icon && <Icon name={icon} width=\"24px\" />}\n\n <span className=\"ms-auto flex gap-x-8\">\n {internalStatus === 'error' && (\n <Icon name=\"CrossDefault\" width=\"24px\" type={iconType} />\n )}\n\n {internalStatus === 'success' && (\n <Icon name=\"CheckDefault\" width=\"24px\" type={iconType} />\n )}\n </span>\n </div>\n </div>\n ) : (\n // Split mode: prefix dropdown + number input\n <div className=\"flex gap-8\">\n <div\n className={clsx('relative rounded-pill z-0 w-[130px] flex-shrink-0', {\n 'bg-white': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n })}\n >\n <select\n disabled={disabled}\n value={prefix}\n onChange={(e) => handlePrefixChange(e.target.value)}\n className={clsx(\n 'text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-semibold outline-none appearance-none bg-transparent',\n {\n 'border-middleGrey focus:border-black active:border-black':\n internalStatus === 'default',\n 'text-black': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n 'pe-[40px]': true, // Space for dropdown arrow\n },\n )}\n id={`${id}-prefix`}\n aria-label={`${name}-prefix`}\n >\n {prefixes.map((p) => (\n <option key={p.code} value={p.code}>\n {p.label ?? p.code}\n </option>\n ))}\n </select>\n\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12 -z-1\">\n <Icon name=\"ArrowDefaultDown\" type=\"svg\" width=\"24px\" color=\"black\" />\n </div>\n </div>\n\n <div className=\"relative flex-1\">\n <input\n ref={numberInputRef}\n type=\"tel\"\n disabled={disabled}\n required={required}\n value={number}\n onChange={(e) => handleInputChange(e, false)}\n onKeyDown={(e) => handleKeyDown(e, number, false)}\n placeholder={placeholder}\n className={clsx(\n 'text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none',\n {\n 'border-middleGrey focus:border-black active:border-black':\n internalStatus === 'default',\n 'pe-[52px]': internalStatus === 'error' || internalStatus === 'success',\n 'bg-white text-black': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n },\n )}\n aria-label={`${name}-number`}\n />\n\n <div\n className={clsx(\n 'pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12',\n {\n 'text-grey': internalStatus === 'disabled',\n 'text-red': internalStatus === 'error',\n 'text-green': internalStatus === 'success',\n },\n )}\n >\n <span className=\"flex gap-x-8\">\n {internalStatus === 'error' && (\n <Icon name=\"CrossDefault\" width=\"24px\" type={iconType} />\n )}\n\n {internalStatus === 'success' && (\n <Icon name=\"CheckDefault\" width=\"24px\" type={iconType} />\n )}\n </span>\n </div>\n </div>\n </div>\n )}\n </FormControl>\n );\n};\n"],"mappings":";;;;;;;;;AA0CA,IAAM,IAAmB,GAGnB,KAAwB,MAAoB;CAChD,IAAM,IAAiB,EAAQ,QAAQ,IAAI,EACrC,IAAgB,IAAiB,IAAI,EAAQ,UAAU,GAAG,EAAe,GAAG;AAElF,QAAO;EAAE;EAAe,qBADI,EAAc,EAAc;EACX;GAIzC,KAAsB,GAAmB,MACzC,EAAoB,SAAS,KAAK,EAAU,WAAW,EAAoB,GACtE,EAAU,UAAU,EAAoB,OAAO,GAEjD,GAIH,KACJ,GACA,GACA,MACG;CACH,IAAI,IAAa,GACb,IAAsB;AAE1B,MAAK,IAAI,IAAI,GAAG,IAAI,EAAU,QAAQ,IACpC,KAAI,KAAK,KAAK,EAAU,GAAG,EAAE;AAC3B,MAAI,IAAsB,EAAoB,QAAQ;AACpD;AACA;;AAIF,MADA,KACI,MAAe,EACjB,QAAO,IAAI;;AAKjB,QAAO,EAAU;GAGN,KAAmC,MAAkC;CAChF,IAAM,IAAa,GAAO,EAEpB,EACJ,QAAK,GACL,UAAO,GACP,UACA,OAAO,GACP,UAAO,QACP,aAAU,kBACV,cAAW,GACX,mBAAgB,EAAS,IAAI,QAAQ,MACrC,gBACA,sBAAmB,WACnB,SACA,aACA,iBACA,cAAW,IACX,cAAW,IACX,qBACA,cACA,gBAAa,cACb,iBAAc,IACd,aACA,GAAG,MACD,GAEE,IAAW,EAAyB,KAAK,EACzC,IAAiB,EAAyB,KAAK,EAC/C,IAAoB,EAAsB,KAAK,EAC/C,CAAC,GAAQ,KAAa,EAAS,EAAc,EAC7C,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAY,KAAiB,EAAS,GAAG,EAE1C,IAAiB,EAAkB;EACvC,YAAY;EACZ;EACD,CAAC;AAYF,CATA,QAAgB;EACd,IAAM,IAAM,MAAS,SAAS,IAAW;AACzC,EAAI,EAAkB,YAAY,QAAQ,EAAI,YAC5C,EAAI,QAAQ,kBAAkB,EAAkB,SAAS,EAAkB,QAAQ,EACnF,EAAkB,UAAU;IAE7B;EAAC;EAAY;EAAQ;EAAK,CAAC,EAG9B,QAAgB;AACd,MAAI,KAAiB,OAAO,KAAkB,YAAY,UAAU,GAAe;GACjF,IAAM,IAAa;AACnB,GAAI,MAAS,SACX,EAAc,EAAW,QAAQ,GAAG,IAEpC,EAAU,EAAW,UAAU,EAAc,EAC7C,EAAU,EAAW,UAAU,GAAG;;IAGrC;EAAC;EAAe;EAAM;EAAc,CAAC;CAGxC,IAAM,IAAa,GAChB,GAAc,GAAiB,MAAoB;EAElD,IAAM,IAAyB;GAC7B;GACA;GACA;GACA,KALU,EAAc,EAAK;GAM9B;AACD,MAAW,GAAM,EAAoB;IAEvC,CAAC,GAAU,EAAK,CACjB,EAGK,IAAgB,GACnB,GAAoC,GAAsB,MAAwB;AACjF,MAAI,EAAE,QAAQ,eAAe,EAAE,QAAQ,SACrC;EAGF,IAAM,IAAQ,EAAE,eACV,IAAiB,EAAM,kBAAkB;AAI/C,MAAI,OAHiB,EAAM,gBAAgB,GAIzC;EAGF,IAAM,EAAE,2BAAwB,EAAqB,EAAQ,EAGvD,IACJ,EAAE,QAAQ,cAAc,EAAa,IAAiB,KAAK,EAAa;AAE1E,MAAI,CAAC,KAAc,KAAK,KAAK,EAAW,CACtC;AAGF,IAAE,gBAAgB;EAGlB,IAAM,IAAY,EAAmB,EAAc,EAAa,EAAE,EAAoB,EAKhF,IAJwB,EAC5B,EAAc,EAAa,UAAU,GAAG,EAAe,CAAC,EACxD,EACD,CACgD,QAG7C,GACA;AAEJ,MAAI,EAAE,QAAQ,aAAa;AACzB,OAAI,MAAuB,EAAG;AAG9B,GAFA,IACE,EAAU,MAAM,GAAG,IAAqB,EAAE,GAAG,EAAU,MAAM,EAAmB,EAClF,IAAqB,IAAqB;SACrC;AAEL,OAAI,KAAsB,EAAU,OAAQ;AAG5C,GAFA,IACE,EAAU,MAAM,GAAG,EAAmB,GAAG,EAAU,MAAM,IAAqB,EAAE,EAClF,IAAqB;;EAIvB,IAAM,IAAY,EAAkB,GAAW,EAAQ;AAIvD,EAFA,EAAkB,UADA,EAAwB,GAAoB,GAAW,EAAoB,EAGzF,KACF,EAAc,EAAU,EACxB,EAAW,EAAU,KAErB,EAAU,EAAU,EACpB,GAAW,OAET,EADiB,GAAG,EAAc,GAAG,IAAY,MAAM,EAClC,GAAe,EAAU,EACvC,GACP;IAGN,CAAC,GAAS,EAAW,CACtB,EAGK,IAAoB,GACvB,GAAkC,MAAwB;EACzD,IAAM,IAAa,EAAE,OAAO,OACtB,IAAiB,EAAE,OAAO,kBAAkB,GAE5C,EAAE,2BAAwB,EAAqB,EAAQ,EAGvD,IAAY,EAAmB,EAAc,EAAW,EAAE,EAAoB,EAG9E,IAAY,EAAuB,EAAQ;AACjD,MAAI,EAAU,SAAS,EACrB;EAQF,IAAM,IAJ4B,EAChC,EAAc,EAAW,UAAU,GAAG,EAAe,CAAC,EACtD,EACD,CACoD,QAG/C,IAAY,EAAkB,GAAW,EAAQ;AAOvD,EAHA,EAAkB,UADA,EAAwB,GAAoB,GAAW,EAAoB,EAIzF,KACF,EAAc,EAAU,EACxB,EAAW,EAAU,KAErB,EAAU,EAAU,EACpB,GAAW,OAET,EADiB,GAAG,EAAc,GAAG,IAAY,MAAM,EAClC,GAAe,EAAU,EACvC,GACP;IAGN,CAAC,GAAS,EAAW,CACtB,EAGK,KAAqB,GACxB,MAAsB;AAErB,EADA,EAAU,EAAU,EACpB,GAAW,OAET,EADiB,GAAG,EAAU,GAAG,IAAgB,MAAM,EAClC,GAAW,EAAc,EACvC,GACP;IAEJ,CAAC,EAAW,CACb;AAgBD,QACE,kBAAC,GAAD;EAdA;EACA;EACA;EACA;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;YAKG,MAAS,SAER,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,SAAD;IACE,GAAK;IACL,KAAK;IACD;IACE;IACN,MAAK;IACK;IACA;IACV,OAAO;IACP,WAAW,MAAM,EAAkB,GAAG,GAAK;IAC3C,YAAY,MAAM,EAAc,GAAG,GAAY,GAAK;IACvC;IACb,WAAW,EACT,2FACA;KACE,4DACE,MAAmB;KACrB,aAAa;KACb,aAAa,MAAmB,WAAW,MAAmB;KAC9D,uBAAuB,MAAmB;KAC1C,8BAA8B,MAAmB;KACjD,cAAc,MAAmB;KACjC,gBAAgB,MAAmB;KACpC,CACF;IACD,cAAY;IACZ,CAAA,EAEF,kBAAC,OAAD;IACE,WAAW,EACT,sFACA;KACE,aAAa,MAAmB;KAChC,YAAY,MAAmB;KAC/B,cAAc,MAAmB;KAClC,CACF;cARH,CAUG,KAAQ,kBAAC,GAAD;KAAM,MAAM;KAAM,OAAM;KAAS,CAAA,EAE1C,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACG,MAAmB,WAClB,kBAAC,GAAD;MAAM,MAAK;MAAe,OAAM;MAAO,MAAM;MAAY,CAAA,EAG1D,MAAmB,aAClB,kBAAC,GAAD;MAAM,MAAK;MAAe,OAAM;MAAO,MAAM;MAAY,CAAA,CAEtD;OACH;MACF;OAGN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAW,EAAK,qDAAqD;KACnE,YAAY,MAAmB;KAC/B,8BAA8B,MAAmB;KAClD,CAAC;cAJJ,CAME,kBAAC,UAAD;KACY;KACV,OAAO;KACP,WAAW,MAAM,GAAmB,EAAE,OAAO,MAAM;KACnD,WAAW,EACT,4HACA;MACE,4DACE,MAAmB;MACrB,cAAc,MAAmB;MACjC,8BAA8B,MAAmB;MACjD,cAAc,MAAmB;MACjC,gBAAgB,MAAmB;MACnC,aAAa;MACd,CACF;KACD,IAAI,GAAG,EAAG;KACV,cAAY,GAAG,EAAK;eAEnB,EAAS,KAAK,MACb,kBAAC,UAAD;MAAqB,OAAO,EAAE;gBAC3B,EAAE,SAAS,EAAE;MACP,EAFI,EAAE,KAEN,CACT;KACK,CAAA,EAET,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAM,MAAK;MAAmB,MAAK;MAAM,OAAM;MAAO,OAAM;MAAU,CAAA;KAClE,CAAA,CACF;OAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,SAAD;KACE,KAAK;KACL,MAAK;KACK;KACA;KACV,OAAO;KACP,WAAW,MAAM,EAAkB,GAAG,GAAM;KAC5C,YAAY,MAAM,EAAc,GAAG,GAAQ,GAAM;KACpC;KACb,WAAW,EACT,2FACA;MACE,4DACE,MAAmB;MACrB,aAAa,MAAmB,WAAW,MAAmB;MAC9D,uBAAuB,MAAmB;MAC1C,8BAA8B,MAAmB;MACjD,cAAc,MAAmB;MACjC,gBAAgB,MAAmB;MACpC,CACF;KACD,cAAY,GAAG,EAAK;KACpB,CAAA,EAEF,kBAAC,OAAD;KACE,WAAW,EACT,kFACA;MACE,aAAa,MAAmB;MAChC,YAAY,MAAmB;MAC/B,cAAc,MAAmB;MAClC,CACF;eAED,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,MAAmB,WAClB,kBAAC,GAAD;OAAM,MAAK;OAAe,OAAM;OAAO,MAAM;OAAY,CAAA,EAG1D,MAAmB,aAClB,kBAAC,GAAD;OAAM,MAAK;OAAe,OAAM;OAAO,MAAM;OAAY,CAAA,CAEtD;;KACH,CAAA,CACF;MACF;;EAEI,CAAA"}
|
|
1
|
+
{"version":3,"file":"PhoneField.js","names":[],"sources":["../../../lib/ui/forms/PhoneField.tsx"],"sourcesContent":["import { type IconicTypes } from '@clubmed/trident-icons';\nimport { FormControl, type FormControlProps } from './FormControl';\nimport { PhoneFieldFullInput, type PhoneFieldFullProps } from './PhoneFieldFullInput';\nimport { PhoneFieldSplitInput, type PhoneFieldSplitProps } from './PhoneFieldSplitInput';\nimport { useId } from 'react';\n\nexport interface PhoneValue {\n full: string;\n prefix?: string;\n number?: string;\n raw: string;\n}\n\nexport type PhoneFieldProps<Value = PhoneValue> = FormControlProps<Value> &\n PhoneFieldFullProps &\n PhoneFieldSplitProps & {\n mode?: 'full' | 'split';\n pattern?: string;\n iconType?: IconicTypes;\n placeholder?: string;\n };\n\nexport const PhoneField = <Value = PhoneValue,>(props: PhoneFieldProps<Value>) => {\n const {\n id,\n name,\n value,\n label,\n className,\n description,\n mode = 'full',\n dataTestId = 'PhoneField',\n disabled,\n required,\n hideRequiredStar,\n validationStatus,\n errorMessage,\n onChange,\n ...rest\n } = props;\n const onPhoneChange = onChange as ((name: string, value: PhoneValue) => void) | undefined;\n const internalId = useId();\n\n const formControlProps = {\n id: id || internalId,\n label,\n className,\n description,\n dataName: 'PhoneField',\n dataTestId,\n disabled,\n required,\n hideRequiredStar,\n validationStatus,\n errorMessage,\n };\n\n return (\n <FormControl {...formControlProps}>\n {mode === 'full' ? (\n <PhoneFieldFullInput\n id={id}\n name={name}\n value={value as PhoneValue | undefined}\n disabled={disabled}\n required={required}\n validationStatus={validationStatus}\n onChange={onPhoneChange}\n {...rest}\n />\n ) : (\n <PhoneFieldSplitInput\n id={id}\n name={name}\n value={value as PhoneValue | undefined}\n disabled={disabled}\n required={required}\n validationStatus={validationStatus}\n onChange={onPhoneChange}\n {...rest}\n />\n )}\n </FormControl>\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAmC,MAAkC;CAChF,IAAM,EACJ,OACA,SACA,UACA,UACA,cACA,gBACA,UAAO,QACP,gBAAa,cACb,aACA,aACA,qBACA,qBACA,iBACA,aACA,GAAG,MACD,GACE,IAAgB,GAChB,IAAa,GAAO;AAgB1B,QACE,kBAAC,GAAD;EAdA,IAAI,KAAM;EACV;EACA;EACA;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;YAMI,EADD,MAAS,SACP,IAWA,GAXD;GACM;GACE;GACC;GACG;GACA;GACQ;GAClB,UAAU;GACV,GAAI;GACJ,CAWA;EAEQ,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IconicNames, IconicTypes } from '@clubmed/trident-icons';
|
|
2
|
+
import { InputHTMLAttributes } from 'react';
|
|
3
|
+
import { FormControlProps } from './FormControl';
|
|
4
|
+
import { PhoneValue } from './PhoneField';
|
|
5
|
+
export interface PhoneFieldFullProps {
|
|
6
|
+
icon?: IconicNames;
|
|
7
|
+
}
|
|
8
|
+
export interface PhoneFieldFullInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'pattern'> {
|
|
9
|
+
value?: PhoneValue;
|
|
10
|
+
pattern?: string;
|
|
11
|
+
icon?: PhoneFieldFullProps['icon'];
|
|
12
|
+
iconType?: IconicTypes;
|
|
13
|
+
validationStatus?: FormControlProps<PhoneValue>['validationStatus'];
|
|
14
|
+
onChange?: (name: string, value: PhoneValue) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare const PhoneFieldFullInput: ({ id, name, value: externalValue, pattern, placeholder, icon, iconType, disabled, required, validationStatus, onChange, ...inputProps }: PhoneFieldFullInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { t as e } from "../../chunks/clsx.js";
|
|
2
|
+
import { useInternalStatus as t } from "../hooks/useInternalStatus.js";
|
|
3
|
+
import { calculateCursorPosition as n, extractDigits as r, formatPhoneDigits as i, formatWithPattern as a, getLiteralPrefixInfo as o, stripLiteralPrefix as s } from "../helpers/phone/formatters.js";
|
|
4
|
+
import { useCallback as c, useEffect as l, useId as u, useRef as d, useState as f } from "react";
|
|
5
|
+
import { Icon as p } from "@clubmed/trident-icons";
|
|
6
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
7
|
+
//#region lib/ui/forms/PhoneFieldFullInput.tsx
|
|
8
|
+
var g = (e) => {
|
|
9
|
+
let { externalValue: t, name: u, pattern: p, onChange: m } = e, h = d(null), g = d(null), [_, v] = f("");
|
|
10
|
+
return l(() => {
|
|
11
|
+
g.current !== null && h.current && (h.current.setSelectionRange(g.current, g.current), g.current = null);
|
|
12
|
+
}, [_]), l(() => {
|
|
13
|
+
t && typeof t == "object" && "full" in t && v(t.full || "");
|
|
14
|
+
}, [t]), {
|
|
15
|
+
inputRef: h,
|
|
16
|
+
value: _,
|
|
17
|
+
onKeyDown: c((e) => {
|
|
18
|
+
if (e.key !== "Backspace" && e.key !== "Delete") return;
|
|
19
|
+
let t = e.currentTarget.selectionStart || 0;
|
|
20
|
+
if (t !== (e.currentTarget.selectionEnd || 0)) return;
|
|
21
|
+
let { literalPrefixDigits: i } = o(p), c = e.key === "Backspace" ? _[t - 1] : _[t];
|
|
22
|
+
if (!c || /\d/.test(c)) return;
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
let l = s(r(_), i), d = s(r(_.substring(0, t)), i).length, f, h;
|
|
25
|
+
if (e.key === "Backspace") {
|
|
26
|
+
if (d === 0) return;
|
|
27
|
+
f = l.slice(0, d - 1) + l.slice(d), h = d - 1;
|
|
28
|
+
} else {
|
|
29
|
+
if (d >= l.length) return;
|
|
30
|
+
f = l.slice(0, d) + l.slice(d + 1), h = d;
|
|
31
|
+
}
|
|
32
|
+
let y = a(f, p);
|
|
33
|
+
g.current = n(h, y, i), v(y), m?.(u, {
|
|
34
|
+
full: y,
|
|
35
|
+
raw: r(y)
|
|
36
|
+
});
|
|
37
|
+
}, [
|
|
38
|
+
u,
|
|
39
|
+
p,
|
|
40
|
+
_,
|
|
41
|
+
m
|
|
42
|
+
]),
|
|
43
|
+
onInputChange: c((e) => {
|
|
44
|
+
let t = i(e.target.value, p, e.target.selectionStart || 0);
|
|
45
|
+
t && (g.current = t.nextCursor, v(t.formatted), m?.(u, {
|
|
46
|
+
full: t.formatted,
|
|
47
|
+
raw: r(t.formatted)
|
|
48
|
+
}));
|
|
49
|
+
}, [
|
|
50
|
+
u,
|
|
51
|
+
p,
|
|
52
|
+
m
|
|
53
|
+
])
|
|
54
|
+
};
|
|
55
|
+
}, _ = ({ id: n, name: r, value: i, pattern: a = "## ## ## ## ##", placeholder: o = "", icon: s, iconType: c, disabled: l = !1, required: d = !1, validationStatus: f = "default", onChange: _, ...v }) => {
|
|
56
|
+
let y = u(), b = n ?? y, x = r ?? b, S = t({
|
|
57
|
+
isDisabled: l,
|
|
58
|
+
validationStatus: f
|
|
59
|
+
}), { inputRef: C, value: w, onKeyDown: T, onInputChange: E } = g({
|
|
60
|
+
externalValue: i,
|
|
61
|
+
name: x,
|
|
62
|
+
pattern: a,
|
|
63
|
+
onChange: _
|
|
64
|
+
});
|
|
65
|
+
return /* @__PURE__ */ h("div", {
|
|
66
|
+
className: "relative",
|
|
67
|
+
children: [/* @__PURE__ */ m("input", {
|
|
68
|
+
...v,
|
|
69
|
+
ref: C,
|
|
70
|
+
id: b,
|
|
71
|
+
name: x,
|
|
72
|
+
type: "tel",
|
|
73
|
+
disabled: l,
|
|
74
|
+
required: d,
|
|
75
|
+
value: w,
|
|
76
|
+
onChange: E,
|
|
77
|
+
onKeyDown: T,
|
|
78
|
+
placeholder: o,
|
|
79
|
+
className: e("text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none", {
|
|
80
|
+
"border-middleGrey focus:border-black active:border-black": S === "default",
|
|
81
|
+
"ps-[52px]": s,
|
|
82
|
+
"pe-[52px]": S === "error" || S === "success",
|
|
83
|
+
"bg-white text-black": S !== "disabled",
|
|
84
|
+
"bg-pearl border-middleGrey": S === "disabled",
|
|
85
|
+
"border-red": S === "error",
|
|
86
|
+
"border-green": S === "success"
|
|
87
|
+
}),
|
|
88
|
+
"aria-label": x
|
|
89
|
+
}), /* @__PURE__ */ h("div", {
|
|
90
|
+
className: e("pointer-events-none absolute inset-0 flex items-center justify-between px-20 py-12", {
|
|
91
|
+
"text-grey": S === "disabled",
|
|
92
|
+
"text-red": S === "error",
|
|
93
|
+
"text-green": S === "success"
|
|
94
|
+
}),
|
|
95
|
+
children: [s && /* @__PURE__ */ m(p, {
|
|
96
|
+
name: s,
|
|
97
|
+
width: "24px"
|
|
98
|
+
}), /* @__PURE__ */ h("span", {
|
|
99
|
+
className: "ms-auto flex gap-x-8",
|
|
100
|
+
children: [S === "error" && /* @__PURE__ */ m(p, {
|
|
101
|
+
name: "CrossDefault",
|
|
102
|
+
width: "24px",
|
|
103
|
+
type: c
|
|
104
|
+
}), S === "success" && /* @__PURE__ */ m(p, {
|
|
105
|
+
name: "CheckDefault",
|
|
106
|
+
width: "24px",
|
|
107
|
+
type: c
|
|
108
|
+
})]
|
|
109
|
+
})]
|
|
110
|
+
})]
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
//#endregion
|
|
114
|
+
export { _ as PhoneFieldFullInput };
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=PhoneFieldFullInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhoneFieldFullInput.js","names":[],"sources":["../../../lib/ui/forms/PhoneFieldFullInput.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport { Icon, type IconicNames, type IconicTypes } from '@clubmed/trident-icons';\nimport {\n type ChangeEvent,\n type InputHTMLAttributes,\n type KeyboardEvent,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\nimport {\n calculateCursorPosition,\n extractDigits,\n formatPhoneDigits,\n formatWithPattern,\n getLiteralPrefixInfo,\n stripLiteralPrefix,\n} from '../helpers/phone';\nimport { useInternalStatus } from '../hooks/useInternalStatus';\nimport type { FormControlProps } from './FormControl';\nimport type { PhoneValue } from './PhoneField';\n\nexport interface PhoneFieldFullProps {\n icon?: IconicNames;\n}\n\nconst usePhoneField = (args: {\n externalValue?: PhoneValue;\n name: string;\n pattern: string;\n onChange?: (name: string, value: PhoneValue) => void;\n}) => {\n const { externalValue, name, pattern, onChange } = args;\n const inputRef = useRef<HTMLInputElement>(null);\n const cursorPositionRef = useRef<number | null>(null);\n const [value, setValue] = useState('');\n\n useEffect(() => {\n if (cursorPositionRef.current !== null && inputRef.current) {\n inputRef.current.setSelectionRange(cursorPositionRef.current, cursorPositionRef.current);\n cursorPositionRef.current = null;\n }\n }, [value]);\n\n useEffect(() => {\n if (externalValue && typeof externalValue === 'object' && 'full' in externalValue) {\n setValue(externalValue.full || '');\n }\n }, [externalValue]);\n\n const onKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key !== 'Backspace' && e.key !== 'Delete') return;\n const cursorPosition = e.currentTarget.selectionStart || 0;\n const selectionEnd = e.currentTarget.selectionEnd || 0;\n if (cursorPosition !== selectionEnd) return;\n\n const { literalPrefixDigits } = getLiteralPrefixInfo(pattern);\n const targetChar = e.key === 'Backspace' ? value[cursorPosition - 1] : value[cursorPosition];\n if (!targetChar || /\\d/.test(targetChar)) return;\n\n e.preventDefault();\n const allDigits = stripLiteralPrefix(extractDigits(value), literalPrefixDigits);\n const digitsBeforeCursor = stripLiteralPrefix(\n extractDigits(value.substring(0, cursorPosition)),\n literalPrefixDigits,\n ).length;\n\n let newDigits: string;\n let targetCursorDigits: number;\n if (e.key === 'Backspace') {\n if (digitsBeforeCursor === 0) return;\n newDigits =\n allDigits.slice(0, digitsBeforeCursor - 1) + allDigits.slice(digitsBeforeCursor);\n targetCursorDigits = digitsBeforeCursor - 1;\n } else {\n if (digitsBeforeCursor >= allDigits.length) return;\n newDigits =\n allDigits.slice(0, digitsBeforeCursor) + allDigits.slice(digitsBeforeCursor + 1);\n targetCursorDigits = digitsBeforeCursor;\n }\n\n const formatted = formatWithPattern(newDigits, pattern);\n cursorPositionRef.current = calculateCursorPosition(\n targetCursorDigits,\n formatted,\n literalPrefixDigits,\n );\n setValue(formatted);\n onChange?.(name, { full: formatted, raw: extractDigits(formatted) });\n },\n [name, pattern, value, onChange],\n );\n\n const onInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n const formattedValue = formatPhoneDigits(\n e.target.value,\n pattern,\n e.target.selectionStart || 0,\n );\n if (!formattedValue) return;\n cursorPositionRef.current = formattedValue.nextCursor;\n setValue(formattedValue.formatted);\n\n onChange?.(name, {\n full: formattedValue.formatted,\n raw: extractDigits(formattedValue.formatted),\n });\n },\n [name, pattern, onChange],\n );\n\n return { inputRef, value, onKeyDown, onInputChange };\n};\n\nexport interface PhoneFieldFullInputProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'pattern'> {\n value?: PhoneValue;\n pattern?: string;\n icon?: PhoneFieldFullProps['icon'];\n iconType?: IconicTypes;\n validationStatus?: FormControlProps<PhoneValue>['validationStatus'];\n onChange?: (name: string, value: PhoneValue) => void;\n}\n\nexport const PhoneFieldFullInput = ({\n id,\n name,\n value: externalValue,\n pattern = '## ## ## ## ##',\n placeholder = '',\n icon,\n iconType,\n disabled = false,\n required = false,\n validationStatus = 'default',\n onChange,\n ...inputProps\n}: PhoneFieldFullInputProps) => {\n const internalId = useId();\n const resolvedId = id ?? internalId;\n const resolvedName = name ?? resolvedId;\n\n const internalStatus = useInternalStatus({ isDisabled: disabled, validationStatus });\n const { inputRef, value, onKeyDown, onInputChange } = usePhoneField({\n externalValue,\n name: resolvedName,\n pattern,\n onChange,\n });\n\n return (\n <div className=\"relative\">\n <input\n {...(inputProps as any)}\n ref={inputRef}\n id={resolvedId}\n name={resolvedName}\n type=\"tel\"\n disabled={disabled}\n required={required}\n value={value}\n onChange={onInputChange}\n onKeyDown={onKeyDown}\n placeholder={placeholder}\n className={clsx(\n 'text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none',\n {\n 'border-middleGrey focus:border-black active:border-black':\n internalStatus === 'default',\n 'ps-[52px]': icon,\n 'pe-[52px]': internalStatus === 'error' || internalStatus === 'success',\n 'bg-white text-black': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n },\n )}\n aria-label={resolvedName}\n />\n <div\n className={clsx(\n 'pointer-events-none absolute inset-0 flex items-center justify-between px-20 py-12',\n {\n 'text-grey': internalStatus === 'disabled',\n 'text-red': internalStatus === 'error',\n 'text-green': internalStatus === 'success',\n },\n )}\n >\n {icon && <Icon name={icon} width=\"24px\" />}\n <span className=\"ms-auto flex gap-x-8\">\n {internalStatus === 'error' && <Icon name=\"CrossDefault\" width=\"24px\" type={iconType} />}\n {internalStatus === 'success' && (\n <Icon name=\"CheckDefault\" width=\"24px\" type={iconType} />\n )}\n </span>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { IconicTypes } from '@clubmed/trident-icons';
|
|
2
|
+
import { InputHTMLAttributes } from 'react';
|
|
3
|
+
import { PhonePrefix } from '../helpers/phone';
|
|
4
|
+
import { FormControlProps } from './FormControl';
|
|
5
|
+
import { PhoneValue } from './PhoneField';
|
|
6
|
+
export interface PhoneFieldSplitProps {
|
|
7
|
+
prefixes?: PhonePrefix[];
|
|
8
|
+
defaultPrefix?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PhoneFieldSplitInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'pattern'> {
|
|
11
|
+
value?: PhoneValue;
|
|
12
|
+
pattern?: string;
|
|
13
|
+
prefixes?: PhoneFieldSplitProps['prefixes'];
|
|
14
|
+
defaultPrefix?: PhoneFieldSplitProps['defaultPrefix'];
|
|
15
|
+
iconType?: IconicTypes;
|
|
16
|
+
validationStatus?: FormControlProps<PhoneValue>['validationStatus'];
|
|
17
|
+
onChange?: (name: string, value: PhoneValue) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare const PhoneFieldSplitInput: ({ id, name, value: externalValue, pattern, prefixes, defaultPrefix, placeholder, iconType, disabled, required, validationStatus, onChange, ...rest }: PhoneFieldSplitInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { t as e } from "../../chunks/clsx.js";
|
|
2
|
+
import { useInternalStatus as t } from "../hooks/useInternalStatus.js";
|
|
3
|
+
import { calculateCursorPosition as n, extractDigits as r, formatPhoneDigits as i, formatWithPattern as a, getLiteralPrefixInfo as o, stripLiteralPrefix as s } from "../helpers/phone/formatters.js";
|
|
4
|
+
import { DEFAULT_PHONE_PREFIXES as c } from "../helpers/phone/defaultPrefixes.js";
|
|
5
|
+
import { useCallback as l, useEffect as u, useId as d, useRef as f, useState as p } from "react";
|
|
6
|
+
import { Icon as m } from "@clubmed/trident-icons";
|
|
7
|
+
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
8
|
+
//#region lib/ui/forms/PhoneFieldSplitInput.tsx
|
|
9
|
+
var _ = (e) => {
|
|
10
|
+
let { externalValue: t, name: c, pattern: d, defaultPrefix: m, onChange: h } = e, g = f(null), _ = f(null), [v, y] = p(m), [b, x] = p(""), S = l((e, t) => {
|
|
11
|
+
if (e === v && t === b) return;
|
|
12
|
+
let n = `${e} ${t}`.trim();
|
|
13
|
+
h?.(c, {
|
|
14
|
+
full: n,
|
|
15
|
+
prefix: e,
|
|
16
|
+
number: t,
|
|
17
|
+
raw: r(n)
|
|
18
|
+
});
|
|
19
|
+
}, [
|
|
20
|
+
c,
|
|
21
|
+
b,
|
|
22
|
+
h,
|
|
23
|
+
v
|
|
24
|
+
]);
|
|
25
|
+
return u(() => {
|
|
26
|
+
_.current !== null && g.current && (g.current.setSelectionRange(_.current, _.current), _.current = null);
|
|
27
|
+
}, [b]), u(() => {
|
|
28
|
+
t && typeof t == "object" && "full" in t && (y(t.prefix || m), x(t.number || ""));
|
|
29
|
+
}, [t, m]), {
|
|
30
|
+
numberInputRef: g,
|
|
31
|
+
prefix: v,
|
|
32
|
+
number: b,
|
|
33
|
+
onKeyDown: l((e) => {
|
|
34
|
+
if (e.key !== "Backspace" && e.key !== "Delete") return;
|
|
35
|
+
let t = e.currentTarget.selectionStart || 0;
|
|
36
|
+
if (t !== (e.currentTarget.selectionEnd || 0)) return;
|
|
37
|
+
let { literalPrefixDigits: i } = o(d), c = e.key === "Backspace" ? b[t - 1] : b[t];
|
|
38
|
+
if (!c || /\d/.test(c)) return;
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
let l = s(r(b), i), u = s(r(b.substring(0, t)), i).length, f, p;
|
|
41
|
+
if (e.key === "Backspace") {
|
|
42
|
+
if (u === 0) return;
|
|
43
|
+
f = l.slice(0, u - 1) + l.slice(u), p = u - 1;
|
|
44
|
+
} else {
|
|
45
|
+
if (u >= l.length) return;
|
|
46
|
+
f = l.slice(0, u) + l.slice(u + 1), p = u;
|
|
47
|
+
}
|
|
48
|
+
let m = a(f, d);
|
|
49
|
+
_.current = n(p, m, i), x(m), S(v, m);
|
|
50
|
+
}, [
|
|
51
|
+
S,
|
|
52
|
+
d,
|
|
53
|
+
b,
|
|
54
|
+
v
|
|
55
|
+
]),
|
|
56
|
+
onInputChange: l((e) => {
|
|
57
|
+
let t = i(e.target.value, d, e.target.selectionStart || 0);
|
|
58
|
+
t && (_.current = t.nextCursor, x(t.formatted), S(v, t.formatted));
|
|
59
|
+
}, [
|
|
60
|
+
S,
|
|
61
|
+
d,
|
|
62
|
+
v
|
|
63
|
+
]),
|
|
64
|
+
onPrefixChange: l((e) => {
|
|
65
|
+
y(e), S(e, b);
|
|
66
|
+
}, [S, b])
|
|
67
|
+
};
|
|
68
|
+
}, v = ({ id: n, name: r, value: i, pattern: a = "## ## ## ## ##", prefixes: o = c, defaultPrefix: s = o[0]?.code || "+1", placeholder: l = "", iconType: u, disabled: f = !1, required: p = !1, validationStatus: v = "default", onChange: y, ...b }) => {
|
|
69
|
+
let x = d(), S = n ?? x, C = r ?? S, w = t({
|
|
70
|
+
isDisabled: f,
|
|
71
|
+
validationStatus: v
|
|
72
|
+
}), { numberInputRef: T, prefix: E, number: D, onKeyDown: O, onInputChange: k, onPrefixChange: A } = _({
|
|
73
|
+
externalValue: i,
|
|
74
|
+
name: C,
|
|
75
|
+
pattern: a,
|
|
76
|
+
defaultPrefix: s,
|
|
77
|
+
onChange: y
|
|
78
|
+
});
|
|
79
|
+
return /* @__PURE__ */ g("div", {
|
|
80
|
+
className: "flex gap-8",
|
|
81
|
+
children: [/* @__PURE__ */ g("div", {
|
|
82
|
+
className: e("relative rounded-pill z-0 w-[130px] flex-shrink-0", {
|
|
83
|
+
"bg-white": w !== "disabled",
|
|
84
|
+
"bg-pearl border-middleGrey": w === "disabled"
|
|
85
|
+
}),
|
|
86
|
+
children: [/* @__PURE__ */ h("select", {
|
|
87
|
+
disabled: f,
|
|
88
|
+
value: E,
|
|
89
|
+
onChange: (e) => A(e.target.value),
|
|
90
|
+
className: e("text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-semibold outline-none appearance-none bg-transparent", {
|
|
91
|
+
"border-middleGrey focus:border-black active:border-black": w === "default",
|
|
92
|
+
"text-black": w !== "disabled",
|
|
93
|
+
"bg-pearl border-middleGrey": w === "disabled",
|
|
94
|
+
"border-red": w === "error",
|
|
95
|
+
"border-green": w === "success",
|
|
96
|
+
"pe-[40px]": !0
|
|
97
|
+
}),
|
|
98
|
+
id: `${S}-prefix`,
|
|
99
|
+
"aria-label": `${C}-prefix`,
|
|
100
|
+
children: o.map((e) => /* @__PURE__ */ h("option", {
|
|
101
|
+
value: e.code,
|
|
102
|
+
children: e.label ?? e.code
|
|
103
|
+
}, e.code))
|
|
104
|
+
}), /* @__PURE__ */ h("div", {
|
|
105
|
+
className: "pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12 -z-1",
|
|
106
|
+
children: /* @__PURE__ */ h(m, {
|
|
107
|
+
name: "ArrowDefaultDown",
|
|
108
|
+
type: "svg",
|
|
109
|
+
width: "24px",
|
|
110
|
+
color: "black"
|
|
111
|
+
})
|
|
112
|
+
})]
|
|
113
|
+
}), /* @__PURE__ */ g("div", {
|
|
114
|
+
className: "relative flex-1",
|
|
115
|
+
children: [/* @__PURE__ */ h("input", {
|
|
116
|
+
...b,
|
|
117
|
+
ref: T,
|
|
118
|
+
type: "tel",
|
|
119
|
+
disabled: f,
|
|
120
|
+
required: p,
|
|
121
|
+
value: D,
|
|
122
|
+
onChange: k,
|
|
123
|
+
onKeyDown: O,
|
|
124
|
+
placeholder: l,
|
|
125
|
+
className: e("text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none", {
|
|
126
|
+
"border-middleGrey focus:border-black active:border-black": w === "default",
|
|
127
|
+
"pe-[52px]": w === "error" || w === "success",
|
|
128
|
+
"bg-white text-black": w !== "disabled",
|
|
129
|
+
"bg-pearl border-middleGrey": w === "disabled",
|
|
130
|
+
"border-red": w === "error",
|
|
131
|
+
"border-green": w === "success"
|
|
132
|
+
}),
|
|
133
|
+
"aria-label": `${C}-number`
|
|
134
|
+
}), /* @__PURE__ */ h("div", {
|
|
135
|
+
className: e("pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12", {
|
|
136
|
+
"text-grey": w === "disabled",
|
|
137
|
+
"text-red": w === "error",
|
|
138
|
+
"text-green": w === "success"
|
|
139
|
+
}),
|
|
140
|
+
children: /* @__PURE__ */ g("span", {
|
|
141
|
+
className: "flex gap-x-8",
|
|
142
|
+
children: [w === "error" && /* @__PURE__ */ h(m, {
|
|
143
|
+
name: "CrossDefault",
|
|
144
|
+
width: "24px",
|
|
145
|
+
type: u
|
|
146
|
+
}), w === "success" && /* @__PURE__ */ h(m, {
|
|
147
|
+
name: "CheckDefault",
|
|
148
|
+
width: "24px",
|
|
149
|
+
type: u
|
|
150
|
+
})]
|
|
151
|
+
})
|
|
152
|
+
})]
|
|
153
|
+
})]
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
157
|
+
export { v as PhoneFieldSplitInput };
|
|
158
|
+
|
|
159
|
+
//# sourceMappingURL=PhoneFieldSplitInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhoneFieldSplitInput.js","names":[],"sources":["../../../lib/ui/forms/PhoneFieldSplitInput.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport { Icon, type IconicTypes } from '@clubmed/trident-icons';\nimport {\n type ChangeEvent,\n type InputHTMLAttributes,\n type KeyboardEvent,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\nimport {\n calculateCursorPosition,\n DEFAULT_PHONE_PREFIXES,\n extractDigits,\n formatPhoneDigits,\n formatWithPattern,\n getLiteralPrefixInfo,\n type PhonePrefix,\n stripLiteralPrefix,\n} from '../helpers/phone';\nimport { useInternalStatus } from '../hooks/useInternalStatus';\nimport type { FormControlProps } from './FormControl';\nimport type { PhoneValue } from './PhoneField';\n\ntype PhoneFieldChangeHandler = (name: string, value: PhoneValue) => void;\n\nexport interface PhoneFieldSplitProps {\n prefixes?: PhonePrefix[];\n defaultPrefix?: string;\n}\n\nconst usePhoneField = (args: {\n externalValue?: PhoneValue;\n name: string;\n pattern: string;\n defaultPrefix: string;\n onChange?: PhoneFieldChangeHandler;\n}) => {\n const { externalValue, name, pattern, defaultPrefix, onChange } = args;\n const numberInputRef = useRef<HTMLInputElement>(null);\n const cursorPositionRef = useRef<number | null>(null);\n const [prefix, setPrefix] = useState(defaultPrefix);\n const [number, setNumber] = useState('');\n\n const emitChange = useCallback(\n (nextPrefix: string, nextNumber: string) => {\n if (nextPrefix === prefix && nextNumber === number) {\n return;\n }\n\n const full = `${nextPrefix} ${nextNumber}`.trim();\n\n onChange?.(name, {\n full,\n prefix: nextPrefix,\n number: nextNumber,\n raw: extractDigits(full),\n });\n },\n [name, number, onChange, prefix],\n );\n\n useEffect(() => {\n if (cursorPositionRef.current !== null && numberInputRef.current) {\n numberInputRef.current.setSelectionRange(\n cursorPositionRef.current,\n cursorPositionRef.current,\n );\n cursorPositionRef.current = null;\n }\n }, [number]);\n\n useEffect(() => {\n if (externalValue && typeof externalValue === 'object' && 'full' in externalValue) {\n setPrefix(externalValue.prefix || defaultPrefix);\n setNumber(externalValue.number || '');\n }\n }, [externalValue, defaultPrefix]);\n\n const onKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key !== 'Backspace' && e.key !== 'Delete') {\n return;\n }\n\n const cursorPosition = e.currentTarget.selectionStart || 0;\n const selectionEnd = e.currentTarget.selectionEnd || 0;\n\n if (cursorPosition !== selectionEnd) {\n return;\n }\n\n const { literalPrefixDigits } = getLiteralPrefixInfo(pattern);\n\n const targetChar =\n e.key === 'Backspace' ? number[cursorPosition - 1] : number[cursorPosition];\n\n if (!targetChar || /\\d/.test(targetChar)) {\n return;\n }\n\n e.preventDefault();\n\n const allDigits = stripLiteralPrefix(extractDigits(number), literalPrefixDigits);\n const digitsBeforeCursor = stripLiteralPrefix(\n extractDigits(number.substring(0, cursorPosition)),\n literalPrefixDigits,\n ).length;\n\n let newDigits: string;\n let targetCursorDigits: number;\n\n if (e.key === 'Backspace') {\n if (digitsBeforeCursor === 0) {\n return;\n }\n\n newDigits =\n allDigits.slice(0, digitsBeforeCursor - 1) + allDigits.slice(digitsBeforeCursor);\n targetCursorDigits = digitsBeforeCursor - 1;\n } else {\n if (digitsBeforeCursor >= allDigits.length) {\n return;\n }\n\n newDigits =\n allDigits.slice(0, digitsBeforeCursor) + allDigits.slice(digitsBeforeCursor + 1);\n targetCursorDigits = digitsBeforeCursor;\n }\n\n const formatted = formatWithPattern(newDigits, pattern);\n cursorPositionRef.current = calculateCursorPosition(\n targetCursorDigits,\n formatted,\n literalPrefixDigits,\n );\n\n setNumber(formatted);\n\n emitChange(prefix, formatted);\n },\n [emitChange, pattern, number, prefix],\n );\n\n const onInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n const formattedValue = formatPhoneDigits(\n e.target.value,\n pattern,\n e.target.selectionStart || 0,\n );\n if (!formattedValue) return;\n\n cursorPositionRef.current = formattedValue.nextCursor;\n setNumber(formattedValue.formatted);\n\n emitChange(prefix, formattedValue.formatted);\n },\n [emitChange, pattern, prefix],\n );\n\n const onPrefixChange = useCallback(\n (newPrefix: string) => {\n setPrefix(newPrefix);\n\n emitChange(newPrefix, number);\n },\n [emitChange, number],\n );\n\n return { numberInputRef, prefix, number, onKeyDown, onInputChange, onPrefixChange };\n};\n\nexport interface PhoneFieldSplitInputProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'pattern'> {\n value?: PhoneValue;\n pattern?: string;\n prefixes?: PhoneFieldSplitProps['prefixes'];\n defaultPrefix?: PhoneFieldSplitProps['defaultPrefix'];\n iconType?: IconicTypes;\n validationStatus?: FormControlProps<PhoneValue>['validationStatus'];\n onChange?: (name: string, value: PhoneValue) => void;\n}\n\nexport const PhoneFieldSplitInput = ({\n id,\n name,\n value: externalValue,\n pattern = '## ## ## ## ##',\n prefixes = DEFAULT_PHONE_PREFIXES,\n defaultPrefix = prefixes[0]?.code || '+1',\n placeholder = '',\n iconType,\n disabled = false,\n required = false,\n validationStatus = 'default',\n onChange,\n ...rest\n}: PhoneFieldSplitInputProps) => {\n const internalId = useId();\n const resolvedId = id ?? internalId;\n const resolvedName = name ?? resolvedId;\n\n const internalStatus = useInternalStatus({ isDisabled: disabled, validationStatus });\n const { numberInputRef, prefix, number, onKeyDown, onInputChange, onPrefixChange } =\n usePhoneField({\n externalValue,\n name: resolvedName,\n pattern,\n defaultPrefix,\n onChange,\n });\n\n return (\n <div className=\"flex gap-8\">\n <div\n className={clsx('relative rounded-pill z-0 w-[130px] flex-shrink-0', {\n 'bg-white': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n })}\n >\n <select\n disabled={disabled}\n value={prefix}\n onChange={(e) => onPrefixChange(e.target.value)}\n className={clsx(\n 'text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-semibold outline-none appearance-none bg-transparent',\n {\n 'border-middleGrey focus:border-black active:border-black':\n internalStatus === 'default',\n 'text-black': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n 'pe-[40px]': true,\n },\n )}\n id={`${resolvedId}-prefix`}\n aria-label={`${resolvedName}-prefix`}\n >\n {(prefixes as PhonePrefix[]).map((p) => (\n <option key={p.code} value={p.code}>\n {p.label ?? p.code}\n </option>\n ))}\n </select>\n\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12 -z-1\">\n <Icon name=\"ArrowDefaultDown\" type=\"svg\" width=\"24px\" color=\"black\" />\n </div>\n </div>\n\n <div className=\"relative flex-1\">\n <input\n {...(rest as Record<string, unknown>)}\n ref={numberInputRef}\n type=\"tel\"\n disabled={disabled}\n required={required}\n value={number}\n onChange={onInputChange}\n onKeyDown={onKeyDown}\n placeholder={placeholder}\n className={clsx(\n 'text-b3 rounded-pill w-full border overflow-hidden px-20 py-12 font-normal outline-none',\n {\n 'border-middleGrey focus:border-black active:border-black':\n internalStatus === 'default',\n 'pe-[52px]': internalStatus === 'error' || internalStatus === 'success',\n 'bg-white text-black': internalStatus !== 'disabled',\n 'bg-pearl border-middleGrey': internalStatus === 'disabled',\n 'border-red': internalStatus === 'error',\n 'border-green': internalStatus === 'success',\n },\n )}\n aria-label={`${resolvedName}-number`}\n />\n\n <div\n className={clsx(\n 'pointer-events-none absolute inset-0 flex items-center justify-end px-20 py-12',\n {\n 'text-grey': internalStatus === 'disabled',\n 'text-red': internalStatus === 'error',\n 'text-green': internalStatus === 'success',\n },\n )}\n >\n <span className=\"flex gap-x-8\">\n {internalStatus === 'error' && (\n <Icon name=\"CrossDefault\" width=\"24px\" type={iconType} />\n )}\n {internalStatus === 'success' && (\n <Icon name=\"CheckDefault\" width=\"24px\" type={iconType} />\n )}\n </span>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
package/ui/forms/Switch.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef
|
|
1
|
+
import { ComponentPropsWithoutRef } from 'react';
|
|
2
2
|
import { Colors } from '../types/Colors';
|
|
3
|
-
export interface SwitchProps extends ComponentPropsWithoutRef<'
|
|
3
|
+
export interface SwitchProps extends Omit<ComponentPropsWithoutRef<'input'>, 'size' | 'type' | 'defaultChecked'> {
|
|
4
4
|
checked: boolean;
|
|
5
5
|
color?: Colors;
|
|
6
6
|
}
|
|
7
|
-
export declare
|
|
7
|
+
export declare function Switch({ id, name, checked, className, color, onChange, disabled, tabIndex, children, role, ...attrs }: SwitchProps): import("react/jsx-runtime").JSX.Element;
|
package/ui/forms/Switch.js
CHANGED
|
@@ -1,42 +1,59 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { t as e } from "../../chunks/clsx.js";
|
|
3
3
|
import { getBgColor as t } from "../helpers/colors/colors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { useId as n } from "react";
|
|
5
|
+
import { jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
5
6
|
//#region lib/ui/forms/Switch.tsx
|
|
6
|
-
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
...c,
|
|
12
|
-
"aria-checked": i,
|
|
13
|
-
className: e(a, "rounded-pill inline-flex min-w-56 items-center p-4 align-middle transition-colors", { "bg-middleGrey": !i }, { [t(o)]: i }),
|
|
14
|
-
"data-name": "Switch",
|
|
15
|
-
onClick: l,
|
|
16
|
-
role: "switch",
|
|
17
|
-
type: "button",
|
|
18
|
-
children: /* @__PURE__ */ r("svg", {
|
|
19
|
-
className: "size-24",
|
|
20
|
-
viewBox: "0 0 24 24",
|
|
21
|
-
children: [
|
|
22
|
-
/* @__PURE__ */ n("circle", {
|
|
23
|
-
className: "fill-white",
|
|
24
|
-
cx: 12,
|
|
25
|
-
cy: 12,
|
|
26
|
-
r: 12
|
|
27
|
-
}),
|
|
28
|
-
/* @__PURE__ */ n("circle", {
|
|
29
|
-
className: "fill-middleGrey",
|
|
30
|
-
cx: 12,
|
|
31
|
-
cy: 12,
|
|
32
|
-
r: 4
|
|
33
|
-
}),
|
|
34
|
-
/* @__PURE__ */ n("path", { d: "M18.73 6c.35 0 .68.14.9.37.5.5.5 1.35 0 1.85l-8.54 8.75q-.37.38-.9.38t-.9-.38l-4.91-5.02a1.33 1.33 0 0 1 0-1.85 1.26 1.26 0 0 1 1.8 0l4.01 4.09 7.64-7.82c.23-.24.56-.37.9-.37" })
|
|
35
|
-
]
|
|
36
|
-
})
|
|
7
|
+
function a({ id: a, name: o, checked: s, className: c, color: l = "saffron", onChange: u, disabled: d = !1, tabIndex: f = 0, children: p, role: m = "switch", ...h }) {
|
|
8
|
+
let g = n(), _ = a ?? g, v = o ?? _, y = p != null, b = e("inline-flex items-center relative", {
|
|
9
|
+
"gap-4": y,
|
|
10
|
+
"cursor-pointer": !d,
|
|
11
|
+
"cursor-not-allowed": d
|
|
37
12
|
});
|
|
38
|
-
|
|
13
|
+
return /* @__PURE__ */ i(y ? "label" : "span", {
|
|
14
|
+
htmlFor: _,
|
|
15
|
+
className: b,
|
|
16
|
+
children: [
|
|
17
|
+
/* @__PURE__ */ r("input", {
|
|
18
|
+
...h,
|
|
19
|
+
id: _,
|
|
20
|
+
name: v,
|
|
21
|
+
checked: s,
|
|
22
|
+
"data-name": "Switch",
|
|
23
|
+
disabled: d,
|
|
24
|
+
role: m,
|
|
25
|
+
tabIndex: f,
|
|
26
|
+
type: "checkbox",
|
|
27
|
+
className: "absolute opacity-0 left-0 w-full top-0 m-0 p-0 z-1 h-full",
|
|
28
|
+
onChange: u
|
|
29
|
+
}),
|
|
30
|
+
/* @__PURE__ */ r("span", {
|
|
31
|
+
className: e(c, "rounded-pill inline-flex min-w-56 items-center p-4 align-middle transition-colors", { "bg-middleGrey": !s }, { [t(l)]: s }),
|
|
32
|
+
children: /* @__PURE__ */ r("svg", {
|
|
33
|
+
className: "size-24",
|
|
34
|
+
viewBox: "0 0 24 24",
|
|
35
|
+
children: /* @__PURE__ */ i("g", { children: [
|
|
36
|
+
/* @__PURE__ */ r("circle", {
|
|
37
|
+
cx: 12,
|
|
38
|
+
cy: 12,
|
|
39
|
+
r: 12,
|
|
40
|
+
fill: "var(--color-white)"
|
|
41
|
+
}),
|
|
42
|
+
/* @__PURE__ */ r("circle", {
|
|
43
|
+
cx: 12,
|
|
44
|
+
cy: 12,
|
|
45
|
+
r: 4,
|
|
46
|
+
fill: "var(--color-middleGrey)"
|
|
47
|
+
}),
|
|
48
|
+
/* @__PURE__ */ r("path", { d: "M18.73 6c.35 0 .68.14.9.37.5.5.5 1.35 0 1.85l-8.54 8.75q-.37.38-.9.38t-.9-.38l-4.91-5.02a1.33 1.33 0 0 1 0-1.85 1.26 1.26 0 0 1 1.8 0l4.01 4.09 7.64-7.82c.23-.24.56-.37.9-.37" })
|
|
49
|
+
] })
|
|
50
|
+
})
|
|
51
|
+
}),
|
|
52
|
+
p
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
39
56
|
//#endregion
|
|
40
|
-
export {
|
|
57
|
+
export { a as Switch };
|
|
41
58
|
|
|
42
59
|
//# sourceMappingURL=Switch.js.map
|
package/ui/forms/Switch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Switch.js","names":[],"sources":["../../../lib/ui/forms/Switch.tsx"],"sourcesContent":["'use client';\n\nimport clsx from 'clsx';\nimport type
|
|
1
|
+
{"version":3,"file":"Switch.js","names":[],"sources":["../../../lib/ui/forms/Switch.tsx"],"sourcesContent":["'use client';\n\nimport clsx from 'clsx';\nimport { type ComponentPropsWithoutRef, useId } from 'react';\n\nimport { getBgColor } from '../helpers/colors/colors';\nimport type { Colors } from '../types/Colors';\n\nexport interface SwitchProps\n extends Omit<ComponentPropsWithoutRef<'input'>, 'size' | 'type' | 'defaultChecked'> {\n checked: boolean;\n color?: Colors;\n}\n\nexport function Switch({\n id,\n name,\n checked,\n className,\n color = 'saffron',\n onChange,\n disabled = false,\n tabIndex = 0,\n children,\n role = 'switch',\n ...attrs\n}: SwitchProps) {\n const internalId = useId();\n const inputId = id ?? internalId;\n const inputName = name ?? inputId;\n const hasChildren = children !== undefined && children !== null;\n const rootClassName = clsx('inline-flex items-center relative', {\n 'gap-4': hasChildren,\n 'cursor-pointer': !disabled,\n 'cursor-not-allowed': disabled,\n });\n const Node = hasChildren ? 'label' : 'span';\n\n return (\n <Node htmlFor={inputId} className={rootClassName}>\n <input\n {...attrs}\n id={inputId}\n name={inputName}\n checked={checked}\n data-name=\"Switch\"\n disabled={disabled}\n role={role}\n tabIndex={tabIndex}\n type=\"checkbox\"\n className=\"absolute opacity-0 left-0 w-full top-0 m-0 p-0 z-1 h-full\"\n onChange={onChange}\n />\n\n <span\n className={clsx(\n className,\n 'rounded-pill inline-flex min-w-56 items-center p-4 align-middle transition-colors',\n { 'bg-middleGrey': !checked },\n { [getBgColor(color)]: checked },\n )}\n >\n <svg className=\"size-24\" viewBox=\"0 0 24 24\">\n <g>\n <circle cx={12} cy={12} r={12} fill=\"var(--color-white)\" />\n <circle cx={12} cy={12} r={4} fill=\"var(--color-middleGrey)\" />\n <path d=\"M18.73 6c.35 0 .68.14.9.37.5.5.5 1.35 0 1.85l-8.54 8.75q-.37.38-.9.38t-.9-.38l-4.91-5.02a1.33 1.33 0 0 1 0-1.85 1.26 1.26 0 0 1 1.8 0l4.01 4.09 7.64-7.82c.23-.24.56-.37.9-.37\" />\n </g>\n </svg>\n </span>\n {children}\n </Node>\n );\n}\n"],"mappings":";;;;;;AAcA,SAAgB,EAAO,EACrB,OACA,SACA,YACA,cACA,WAAQ,WACR,aACA,cAAW,IACX,cAAW,GACX,aACA,UAAO,UACP,GAAG,KACW;CACd,IAAM,IAAa,GAAO,EACpB,IAAU,KAAM,GAChB,IAAY,KAAQ,GACpB,IAAc,KAAuC,MACrD,IAAgB,EAAK,qCAAqC;EAC9D,SAAS;EACT,kBAAkB,CAAC;EACnB,sBAAsB;EACvB,CAAC;AAGF,QACE,kBAHW,IAAc,UAAU,QAGnC;EAAM,SAAS;EAAS,WAAW;YAAnC;GACE,kBAAC,SAAD;IACE,GAAI;IACJ,IAAI;IACJ,MAAM;IACG;IACT,aAAU;IACA;IACJ;IACI;IACV,MAAK;IACL,WAAU;IACA;IACV,CAAA;GAEF,kBAAC,QAAD;IACE,WAAW,EACT,GACA,qFACA,EAAE,iBAAiB,CAAC,GAAS,EAC7B,GAAG,EAAW,EAAM,GAAG,GAAS,CACjC;cAED,kBAAC,OAAD;KAAK,WAAU;KAAU,SAAQ;eAC/B,kBAAC,KAAD,EAAA,UAAA;MACE,kBAAC,UAAD;OAAQ,IAAI;OAAI,IAAI;OAAI,GAAG;OAAI,MAAK;OAAuB,CAAA;MAC3D,kBAAC,UAAD;OAAQ,IAAI;OAAI,IAAI;OAAI,GAAG;OAAG,MAAK;OAA4B,CAAA;MAC/D,kBAAC,QAAD,EAAM,GAAE,kLAAmL,CAAA;MACzL,EAAA,CAAA;KACA,CAAA;IACD,CAAA;GACN"}
|
|
@@ -44,3 +44,14 @@ export declare function getCursorPosition(oldValue: string, newValue: string, ol
|
|
|
44
44
|
* @returns Number of '#' characters in pattern
|
|
45
45
|
*/
|
|
46
46
|
export declare function countDigitPlaceholders(pattern: string): number;
|
|
47
|
+
export declare function getLiteralPrefixInfo(pattern: string): {
|
|
48
|
+
literalPrefix: string;
|
|
49
|
+
literalPrefixDigits: string;
|
|
50
|
+
};
|
|
51
|
+
export declare function stripLiteralPrefix(allDigits: string, literalPrefixDigits: string): string;
|
|
52
|
+
export declare function calculateCursorPosition(digitsBeforeCursor: number, formatted: string, literalPrefixDigits: string): number;
|
|
53
|
+
export declare function formatPhoneDigits(inputValue: string, pattern: string, cursorPosition: number): {
|
|
54
|
+
formatted: string;
|
|
55
|
+
nextCursor: number;
|
|
56
|
+
literalPrefixDigits: string;
|
|
57
|
+
} | null;
|
|
@@ -27,7 +27,38 @@ function n(t, n, r) {
|
|
|
27
27
|
function r(e) {
|
|
28
28
|
return (e.match(/#/g) || []).length;
|
|
29
29
|
}
|
|
30
|
+
function i(t) {
|
|
31
|
+
let n = t.indexOf("#"), r = n > 0 ? t.substring(0, n) : "";
|
|
32
|
+
return {
|
|
33
|
+
literalPrefix: r,
|
|
34
|
+
literalPrefixDigits: e(r)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function a(e, t) {
|
|
38
|
+
return t.length > 0 && e.startsWith(t) ? e.substring(t.length) : e;
|
|
39
|
+
}
|
|
40
|
+
function o(e, t, n) {
|
|
41
|
+
let r = 0, i = 0;
|
|
42
|
+
for (let a = 0; a < t.length; a++) if (/\d/.test(t[a])) {
|
|
43
|
+
if (i < n.length) {
|
|
44
|
+
i++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (r++, r === e) return a + 1;
|
|
48
|
+
}
|
|
49
|
+
return t.length;
|
|
50
|
+
}
|
|
51
|
+
function s(n, s, c) {
|
|
52
|
+
let { literalPrefixDigits: l } = i(s), u = a(e(n), l), d = r(s);
|
|
53
|
+
if (u.length > d) return null;
|
|
54
|
+
let f = a(e(n.substring(0, c)), l).length, p = t(u, s);
|
|
55
|
+
return {
|
|
56
|
+
formatted: p,
|
|
57
|
+
nextCursor: o(f, p, l),
|
|
58
|
+
literalPrefixDigits: l
|
|
59
|
+
};
|
|
60
|
+
}
|
|
30
61
|
//#endregion
|
|
31
|
-
export { r as countDigitPlaceholders, e as extractDigits, t as formatWithPattern, n as getCursorPosition };
|
|
62
|
+
export { o as calculateCursorPosition, r as countDigitPlaceholders, e as extractDigits, s as formatPhoneDigits, t as formatWithPattern, n as getCursorPosition, i as getLiteralPrefixInfo, a as stripLiteralPrefix };
|
|
32
63
|
|
|
33
64
|
//# sourceMappingURL=formatters.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatters.js","names":[],"sources":["../../../../lib/ui/helpers/phone/formatters.ts"],"sourcesContent":["/**\n * Phone number formatting utilities\n * Provides pattern-based formatting without external dependencies\n */\n\n/**\n * Extract only digits from a string\n * @param value - Input string that may contain non-digit characters\n * @returns String containing only digits (0-9)\n */\nexport function extractDigits(value: string): string {\n return value.replace(/\\D/g, '');\n}\n\n/**\n * Apply a pattern to input digits\n * Pattern uses '#' as placeholder for digits, other characters are literals\n *\n * @example\n * formatWithPattern(\"5551234567\", \"+1 (###) ###-####\")\n * // returns \"+1 (555) 123-4567\"\n *\n * @param value - String containing the digits to format\n * @param pattern - Format pattern where '#' represents a digit placeholder\n * @returns Formatted string according to pattern\n */\nexport function formatWithPattern(value: string, pattern: string): string {\n const digits = extractDigits(value);\n if (digits.length === 0) return '';\n\n let digitIndex = 0;\n let result = '';\n let started = false;\n\n for (let i = 0; i < pattern.length; i++) {\n if (pattern[i] === '#') {\n if (digitIndex < digits.length) {\n // Add any leading literals before the first digit\n if (!started) {\n // Add all literals from start until this digit\n for (let j = 0; j < i; j++) {\n if (pattern[j] !== '#') {\n result += pattern[j];\n }\n }\n started = true;\n }\n\n result += digits[digitIndex];\n digitIndex++;\n } else {\n // No more digits available\n break;\n }\n } else if (started) {\n // Literal character after we've started\n result += pattern[i];\n }\n }\n\n return result;\n}\n\n/**\n * Calculate cursor position after formatting\n * Critical for UX - ensures cursor stays in correct position relative to digits\n *\n * @example\n * // User types \"4\" at position: \"1|23\" → \"1423\"\n * // After formatting: \"(14) 23\"\n * // Cursor should be: \"(14|) 23\" (after the 4, not at end)\n *\n * @param oldValue - Previous input value\n * @param newValue - New formatted value\n * @param oldCursor - Cursor position before formatting\n * @returns New cursor position after formatting\n */\nexport function getCursorPosition(oldValue: string, newValue: string, oldCursor: number): number {\n // Count digits before cursor in old value\n const digitsBeforeCursor = extractDigits(oldValue.substring(0, oldCursor)).length;\n\n // Find position in new value where we have the same number of digits\n let digitCount = 0;\n let newCursor = 0;\n\n for (let i = 0; i < newValue.length; i++) {\n if (/\\d/.test(newValue[i])) {\n digitCount++;\n if (digitCount === digitsBeforeCursor) {\n newCursor = i + 1;\n break;\n }\n }\n }\n\n // If we didn't find enough digits, cursor goes to end\n if (digitCount < digitsBeforeCursor) {\n newCursor = newValue.length;\n }\n\n return newCursor;\n}\n\n/**\n * Count digit placeholders in a pattern\n * Used to determine maximum number of digits allowed\n *\n * @param pattern - Format pattern\n * @returns Number of '#' characters in pattern\n */\nexport function countDigitPlaceholders(pattern: string): number {\n return (pattern.match(/#/g) || []).length;\n}\n"],"mappings":";AAUA,SAAgB,EAAc,GAAuB;AACnD,QAAO,EAAM,QAAQ,OAAO,GAAG;;AAejC,SAAgB,EAAkB,GAAe,GAAyB;CACxE,IAAM,IAAS,EAAc,EAAM;AACnC,KAAI,EAAO,WAAW,EAAG,QAAO;CAEhC,IAAI,IAAa,GACb,IAAS,IACT,IAAU;AAEd,MAAK,IAAI,IAAI,GAAG,IAAI,EAAQ,QAAQ,IAClC,KAAI,EAAQ,OAAO,IACjB,KAAI,IAAa,EAAO,QAAQ;AAE9B,MAAI,CAAC,GAAS;AAEZ,QAAK,IAAI,IAAI,GAAG,IAAI,GAAG,IACrB,CAAI,EAAQ,OAAO,QACjB,KAAU,EAAQ;AAGtB,OAAU;;AAIZ,EADA,KAAU,EAAO,IACjB;OAGA;MAEO,MAET,KAAU,EAAQ;AAItB,QAAO;;AAiBT,SAAgB,EAAkB,GAAkB,GAAkB,GAA2B;CAE/F,IAAM,IAAqB,EAAc,EAAS,UAAU,GAAG,EAAU,CAAC,CAAC,QAGvE,IAAa,GACb,IAAY;AAEhB,MAAK,IAAI,IAAI,GAAG,IAAI,EAAS,QAAQ,IACnC,KAAI,KAAK,KAAK,EAAS,GAAG,KACxB,KACI,MAAe,IAAoB;AACrC,MAAY,IAAI;AAChB;;AAUN,QAJI,IAAa,MACf,IAAY,EAAS,SAGhB;;AAUT,SAAgB,EAAuB,GAAyB;AAC9D,SAAQ,EAAQ,MAAM,KAAK,IAAI,EAAE,EAAE"}
|
|
1
|
+
{"version":3,"file":"formatters.js","names":[],"sources":["../../../../lib/ui/helpers/phone/formatters.ts"],"sourcesContent":["/**\n * Phone number formatting utilities\n * Provides pattern-based formatting without external dependencies\n */\n\n/**\n * Extract only digits from a string\n * @param value - Input string that may contain non-digit characters\n * @returns String containing only digits (0-9)\n */\nexport function extractDigits(value: string): string {\n return value.replace(/\\D/g, '');\n}\n\n/**\n * Apply a pattern to input digits\n * Pattern uses '#' as placeholder for digits, other characters are literals\n *\n * @example\n * formatWithPattern(\"5551234567\", \"+1 (###) ###-####\")\n * // returns \"+1 (555) 123-4567\"\n *\n * @param value - String containing the digits to format\n * @param pattern - Format pattern where '#' represents a digit placeholder\n * @returns Formatted string according to pattern\n */\nexport function formatWithPattern(value: string, pattern: string): string {\n const digits = extractDigits(value);\n if (digits.length === 0) return '';\n\n let digitIndex = 0;\n let result = '';\n let started = false;\n\n for (let i = 0; i < pattern.length; i++) {\n if (pattern[i] === '#') {\n if (digitIndex < digits.length) {\n // Add any leading literals before the first digit\n if (!started) {\n // Add all literals from start until this digit\n for (let j = 0; j < i; j++) {\n if (pattern[j] !== '#') {\n result += pattern[j];\n }\n }\n started = true;\n }\n\n result += digits[digitIndex];\n digitIndex++;\n } else {\n // No more digits available\n break;\n }\n } else if (started) {\n // Literal character after we've started\n result += pattern[i];\n }\n }\n\n return result;\n}\n\n/**\n * Calculate cursor position after formatting\n * Critical for UX - ensures cursor stays in correct position relative to digits\n *\n * @example\n * // User types \"4\" at position: \"1|23\" → \"1423\"\n * // After formatting: \"(14) 23\"\n * // Cursor should be: \"(14|) 23\" (after the 4, not at end)\n *\n * @param oldValue - Previous input value\n * @param newValue - New formatted value\n * @param oldCursor - Cursor position before formatting\n * @returns New cursor position after formatting\n */\nexport function getCursorPosition(oldValue: string, newValue: string, oldCursor: number): number {\n // Count digits before cursor in old value\n const digitsBeforeCursor = extractDigits(oldValue.substring(0, oldCursor)).length;\n\n // Find position in new value where we have the same number of digits\n let digitCount = 0;\n let newCursor = 0;\n\n for (let i = 0; i < newValue.length; i++) {\n if (/\\d/.test(newValue[i])) {\n digitCount++;\n if (digitCount === digitsBeforeCursor) {\n newCursor = i + 1;\n break;\n }\n }\n }\n\n // If we didn't find enough digits, cursor goes to end\n if (digitCount < digitsBeforeCursor) {\n newCursor = newValue.length;\n }\n\n return newCursor;\n}\n\n/**\n * Count digit placeholders in a pattern\n * Used to determine maximum number of digits allowed\n *\n * @param pattern - Format pattern\n * @returns Number of '#' characters in pattern\n */\nexport function countDigitPlaceholders(pattern: string): number {\n return (pattern.match(/#/g) || []).length;\n}\n\nexport function getLiteralPrefixInfo(pattern: string): {\n literalPrefix: string;\n literalPrefixDigits: string;\n} {\n const firstHashIndex = pattern.indexOf('#');\n const literalPrefix = firstHashIndex > 0 ? pattern.substring(0, firstHashIndex) : '';\n const literalPrefixDigits = extractDigits(literalPrefix);\n return { literalPrefix, literalPrefixDigits };\n}\n\nexport function stripLiteralPrefix(allDigits: string, literalPrefixDigits: string): string {\n if (literalPrefixDigits.length > 0 && allDigits.startsWith(literalPrefixDigits)) {\n return allDigits.substring(literalPrefixDigits.length);\n }\n return allDigits;\n}\n\nexport function calculateCursorPosition(\n digitsBeforeCursor: number,\n formatted: string,\n literalPrefixDigits: string,\n): number {\n let digitCount = 0;\n let skippedPrefixDigits = 0;\n\n for (let i = 0; i < formatted.length; i++) {\n if (/\\d/.test(formatted[i])) {\n if (skippedPrefixDigits < literalPrefixDigits.length) {\n skippedPrefixDigits++;\n continue;\n }\n digitCount++;\n if (digitCount === digitsBeforeCursor) {\n return i + 1;\n }\n }\n }\n\n return formatted.length;\n}\n\nexport function formatPhoneDigits(\n inputValue: string,\n pattern: string,\n cursorPosition: number,\n): { formatted: string; nextCursor: number; literalPrefixDigits: string } | null {\n const { literalPrefixDigits } = getLiteralPrefixInfo(pattern);\n const allDigits = stripLiteralPrefix(extractDigits(inputValue), literalPrefixDigits);\n const maxDigits = countDigitPlaceholders(pattern);\n if (allDigits.length > maxDigits) return null;\n\n const digitsBeforeCursorInInput = stripLiteralPrefix(\n extractDigits(inputValue.substring(0, cursorPosition)),\n literalPrefixDigits,\n );\n const digitsBeforeCursor = digitsBeforeCursorInInput.length;\n const formatted = formatWithPattern(allDigits, pattern);\n const nextCursor = calculateCursorPosition(digitsBeforeCursor, formatted, literalPrefixDigits);\n return { formatted, nextCursor, literalPrefixDigits };\n}\n"],"mappings":";AAUA,SAAgB,EAAc,GAAuB;AACnD,QAAO,EAAM,QAAQ,OAAO,GAAG;;AAejC,SAAgB,EAAkB,GAAe,GAAyB;CACxE,IAAM,IAAS,EAAc,EAAM;AACnC,KAAI,EAAO,WAAW,EAAG,QAAO;CAEhC,IAAI,IAAa,GACb,IAAS,IACT,IAAU;AAEd,MAAK,IAAI,IAAI,GAAG,IAAI,EAAQ,QAAQ,IAClC,KAAI,EAAQ,OAAO,IACjB,KAAI,IAAa,EAAO,QAAQ;AAE9B,MAAI,CAAC,GAAS;AAEZ,QAAK,IAAI,IAAI,GAAG,IAAI,GAAG,IACrB,CAAI,EAAQ,OAAO,QACjB,KAAU,EAAQ;AAGtB,OAAU;;AAIZ,EADA,KAAU,EAAO,IACjB;OAGA;MAEO,MAET,KAAU,EAAQ;AAItB,QAAO;;AAiBT,SAAgB,EAAkB,GAAkB,GAAkB,GAA2B;CAE/F,IAAM,IAAqB,EAAc,EAAS,UAAU,GAAG,EAAU,CAAC,CAAC,QAGvE,IAAa,GACb,IAAY;AAEhB,MAAK,IAAI,IAAI,GAAG,IAAI,EAAS,QAAQ,IACnC,KAAI,KAAK,KAAK,EAAS,GAAG,KACxB,KACI,MAAe,IAAoB;AACrC,MAAY,IAAI;AAChB;;AAUN,QAJI,IAAa,MACf,IAAY,EAAS,SAGhB;;AAUT,SAAgB,EAAuB,GAAyB;AAC9D,SAAQ,EAAQ,MAAM,KAAK,IAAI,EAAE,EAAE;;AAGrC,SAAgB,EAAqB,GAGnC;CACA,IAAM,IAAiB,EAAQ,QAAQ,IAAI,EACrC,IAAgB,IAAiB,IAAI,EAAQ,UAAU,GAAG,EAAe,GAAG;AAElF,QAAO;EAAE;EAAe,qBADI,EAAc,EAAc;EACX;;AAG/C,SAAgB,EAAmB,GAAmB,GAAqC;AAIzF,QAHI,EAAoB,SAAS,KAAK,EAAU,WAAW,EAAoB,GACtE,EAAU,UAAU,EAAoB,OAAO,GAEjD;;AAGT,SAAgB,EACd,GACA,GACA,GACQ;CACR,IAAI,IAAa,GACb,IAAsB;AAE1B,MAAK,IAAI,IAAI,GAAG,IAAI,EAAU,QAAQ,IACpC,KAAI,KAAK,KAAK,EAAU,GAAG,EAAE;AAC3B,MAAI,IAAsB,EAAoB,QAAQ;AACpD;AACA;;AAGF,MADA,KACI,MAAe,EACjB,QAAO,IAAI;;AAKjB,QAAO,EAAU;;AAGnB,SAAgB,EACd,GACA,GACA,GAC+E;CAC/E,IAAM,EAAE,2BAAwB,EAAqB,EAAQ,EACvD,IAAY,EAAmB,EAAc,EAAW,EAAE,EAAoB,EAC9E,IAAY,EAAuB,EAAQ;AACjD,KAAI,EAAU,SAAS,EAAW,QAAO;CAMzC,IAAM,IAJ4B,EAChC,EAAc,EAAW,UAAU,GAAG,EAAe,CAAC,EACtD,EACD,CACoD,QAC/C,IAAY,EAAkB,GAAW,EAAQ;AAEvD,QAAO;EAAE;EAAW,YADD,EAAwB,GAAoB,GAAW,EAAoB;EAC9D;EAAqB"}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Phone number utilities
|
|
3
3
|
* Provides formatting, validation, and default prefixes for phone number inputs
|
|
4
4
|
*/
|
|
5
|
-
export { extractDigits, formatWithPattern, getCursorPosition, countDigitPlaceholders, } from './formatters';
|
|
5
|
+
export { extractDigits, formatWithPattern, getCursorPosition, countDigitPlaceholders, getLiteralPrefixInfo, stripLiteralPrefix, calculateCursorPosition, formatPhoneDigits, } from './formatters';
|
|
6
6
|
export { DEFAULT_PHONE_PREFIXES, type PhonePrefix } from './defaultPrefixes';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { DEFAULT_PHONE_PREFIXES as
|
|
3
|
-
export {
|
|
1
|
+
import { calculateCursorPosition as e, countDigitPlaceholders as t, extractDigits as n, formatPhoneDigits as r, formatWithPattern as i, getCursorPosition as a, getLiteralPrefixInfo as o, stripLiteralPrefix as s } from "./formatters.js";
|
|
2
|
+
import { DEFAULT_PHONE_PREFIXES as c } from "./defaultPrefixes.js";
|
|
3
|
+
export { c as DEFAULT_PHONE_PREFIXES, e as calculateCursorPosition, t as countDigitPlaceholders, n as extractDigits, r as formatPhoneDigits, i as formatWithPattern, a as getCursorPosition, o as getLiteralPrefixInfo, s as stripLiteralPrefix };
|