@meetelise/chat 1.50.6 → 1.51.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +7 -3
- package/dist/src/WebComponent/actions/email-us-window.d.ts +5 -4
- package/dist/src/WebComponent/actions/formatPhoneNumber.d.ts +22 -3
- package/dist/src/WebComponent/actions/international-phone-input.d.ts +30 -0
- package/dist/src/WebComponent/me-select.d.ts +6 -0
- package/package.json +2 -1
- package/public/dist/index.js +718 -593
- package/src/WebComponent/Scheduler/tour-scheduler.ts +37 -189
- package/src/WebComponent/actions/call-us-window.ts +2 -2
- package/src/WebComponent/actions/email-us-window.ts +27 -54
- package/src/WebComponent/actions/formatPhoneNumber.ts +146 -12
- package/src/WebComponent/actions/international-phone-input.ts +308 -0
- package/src/WebComponent/me-select.ts +90 -45
- package/src/formatPhoneNumber.test.ts +91 -0
- package/src/internationalPhoneInput.test.ts +253 -0
- package/src/internationalPhoneSubmission.test.ts +145 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
|
2
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
4
|
+
import { InputStyles } from "./InputStyles";
|
|
5
|
+
import {
|
|
6
|
+
formatToPhoneInput,
|
|
7
|
+
getCountryFromPhoneInput,
|
|
8
|
+
getE164PhoneNumber,
|
|
9
|
+
getNationalPhoneNumber,
|
|
10
|
+
getPhoneCountry,
|
|
11
|
+
phoneCountryOptions,
|
|
12
|
+
} from "./formatPhoneNumber";
|
|
13
|
+
|
|
14
|
+
export type InternationalPhoneChangeDetail = {
|
|
15
|
+
country: string;
|
|
16
|
+
e164: string | null;
|
|
17
|
+
formattedValue: string;
|
|
18
|
+
isValid: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
@customElement("international-phone-input")
|
|
22
|
+
export class InternationalPhoneInput extends LitElement {
|
|
23
|
+
static styles = [
|
|
24
|
+
InputStyles,
|
|
25
|
+
css`
|
|
26
|
+
:host {
|
|
27
|
+
display: block;
|
|
28
|
+
width: 100%;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.phone-input-container {
|
|
32
|
+
align-items: center;
|
|
33
|
+
background: #d9d9d9;
|
|
34
|
+
border: 1px solid #efefef;
|
|
35
|
+
border-radius: 100px;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
display: flex;
|
|
38
|
+
min-height: 44px;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
width: 100%;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.phone-input-container:focus-within {
|
|
44
|
+
border-color: #202020;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.phone-input-container--invalid {
|
|
48
|
+
border-color: #ff0000;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.country-select-wrapper {
|
|
52
|
+
align-items: center;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
display: flex;
|
|
56
|
+
flex: 0 0 96px;
|
|
57
|
+
height: 42px;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
position: relative;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.country-select-wrapper::after {
|
|
63
|
+
border-bottom: 1.5px solid #4b4b4b;
|
|
64
|
+
border-right: 1.5px solid #4b4b4b;
|
|
65
|
+
content: "";
|
|
66
|
+
height: 5px;
|
|
67
|
+
margin-left: 8px;
|
|
68
|
+
margin-top: -3px;
|
|
69
|
+
transform: rotate(45deg);
|
|
70
|
+
width: 5px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.country-select-label {
|
|
74
|
+
color: #202020;
|
|
75
|
+
font-family: "Helvetica Neue", Arial;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 400;
|
|
78
|
+
line-height: 22px;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
select {
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
inset: 0;
|
|
85
|
+
opacity: 0;
|
|
86
|
+
position: absolute;
|
|
87
|
+
width: 100%;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.phone-input-divider {
|
|
91
|
+
background: rgba(32, 32, 32, 0.18);
|
|
92
|
+
flex: 0 0 1px;
|
|
93
|
+
height: 22px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.phone-input-container input.webchat-input {
|
|
97
|
+
background: transparent;
|
|
98
|
+
border: 0;
|
|
99
|
+
border-radius: 0;
|
|
100
|
+
box-sizing: border-box;
|
|
101
|
+
flex: 1;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
padding: 10px 16px 10px 12px;
|
|
104
|
+
width: 100%;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.phone-input-container input.webchat-input:active,
|
|
108
|
+
.phone-input-container input.webchat-input:focus-within {
|
|
109
|
+
border: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@media screen and (max-width: 767px) {
|
|
113
|
+
input {
|
|
114
|
+
font-size: 16px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.country-select-wrapper {
|
|
118
|
+
flex-basis: 92px;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
@property({ attribute: false })
|
|
125
|
+
country: string | null = null;
|
|
126
|
+
|
|
127
|
+
@property({ type: String })
|
|
128
|
+
value = "";
|
|
129
|
+
|
|
130
|
+
@property({ type: Boolean })
|
|
131
|
+
invalid = false;
|
|
132
|
+
|
|
133
|
+
@state()
|
|
134
|
+
private selectedCountry = "US";
|
|
135
|
+
|
|
136
|
+
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
|
137
|
+
if (changedProperties.has("country")) {
|
|
138
|
+
this.selectedCountry = getPhoneCountry(this.country);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get e164(): string | null {
|
|
143
|
+
return getE164PhoneNumber(this.value, this.selectedCountry);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get isValid(): boolean {
|
|
147
|
+
return this.e164 !== null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private dispatchPhoneChange(): void {
|
|
151
|
+
this.dispatchEvent(
|
|
152
|
+
new CustomEvent<InternationalPhoneChangeDetail>("phone-change", {
|
|
153
|
+
bubbles: true,
|
|
154
|
+
composed: true,
|
|
155
|
+
detail: {
|
|
156
|
+
country: this.selectedCountry,
|
|
157
|
+
e164: this.e164,
|
|
158
|
+
formattedValue: this.value,
|
|
159
|
+
isValid: this.isValid,
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private setFormattedInputValue(
|
|
166
|
+
input: HTMLInputElement,
|
|
167
|
+
rawValue: string,
|
|
168
|
+
country: string
|
|
169
|
+
): void {
|
|
170
|
+
const selectionStart = input.selectionStart;
|
|
171
|
+
const digitsAfterCursor =
|
|
172
|
+
selectionStart === null
|
|
173
|
+
? 0
|
|
174
|
+
: rawValue.slice(selectionStart).replace(/\D/g, "").length;
|
|
175
|
+
this.value = formatToPhoneInput(rawValue, country);
|
|
176
|
+
input.value = this.value;
|
|
177
|
+
|
|
178
|
+
let cursorPosition = selectionStart === 0 ? 0 : this.value.length;
|
|
179
|
+
let remainingDigits = digitsAfterCursor;
|
|
180
|
+
while (remainingDigits > 0 && cursorPosition > 0) {
|
|
181
|
+
cursorPosition -= 1;
|
|
182
|
+
if (/\d/.test(this.value[cursorPosition])) remainingDigits -= 1;
|
|
183
|
+
}
|
|
184
|
+
input.setSelectionRange(cursorPosition, cursorPosition);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private handleInput(event: Event): void {
|
|
188
|
+
const input = event.target as HTMLInputElement;
|
|
189
|
+
const inferredCountry = getCountryFromPhoneInput(
|
|
190
|
+
input.value,
|
|
191
|
+
this.selectedCountry
|
|
192
|
+
);
|
|
193
|
+
this.selectedCountry = inferredCountry;
|
|
194
|
+
this.setFormattedInputValue(input, input.value, inferredCountry);
|
|
195
|
+
this.dispatchPhoneChange();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private handleCountryChange(event: Event): void {
|
|
199
|
+
const previousCountry = this.selectedCountry;
|
|
200
|
+
this.selectedCountry = (event.target as HTMLSelectElement).value;
|
|
201
|
+
const nationalNumber = getNationalPhoneNumber(this.value, previousCountry);
|
|
202
|
+
this.value = formatToPhoneInput(nationalNumber, this.selectedCountry);
|
|
203
|
+
this.dispatchPhoneChange();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private handleKeydown(event: KeyboardEvent): void {
|
|
207
|
+
const input = event.target as HTMLInputElement;
|
|
208
|
+
if (
|
|
209
|
+
event.key !== "Backspace" ||
|
|
210
|
+
input.selectionStart === null ||
|
|
211
|
+
input.selectionEnd !== input.selectionStart ||
|
|
212
|
+
input.selectionStart === 0 ||
|
|
213
|
+
(input.selectionStart === 1 && input.value.startsWith("+")) ||
|
|
214
|
+
/\d/.test(input.value[input.selectionStart - 1])
|
|
215
|
+
) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const digitsBeforeCursor = input.value
|
|
220
|
+
.slice(0, input.selectionStart)
|
|
221
|
+
.replace(/\D/g, "");
|
|
222
|
+
const digitsAfterCursor = input.value
|
|
223
|
+
.slice(input.selectionStart)
|
|
224
|
+
.replace(/\D/g, "");
|
|
225
|
+
const internationalPrefix = input.value.trimStart().startsWith("+")
|
|
226
|
+
? "+"
|
|
227
|
+
: "";
|
|
228
|
+
const digitsBeforeUpdatedCursor = digitsBeforeCursor.slice(0, -1);
|
|
229
|
+
const updatedValue = `${internationalPrefix}${digitsBeforeUpdatedCursor}${digitsAfterCursor}`;
|
|
230
|
+
const updatedCursorPosition =
|
|
231
|
+
internationalPrefix.length + digitsBeforeUpdatedCursor.length;
|
|
232
|
+
const inferredCountry = getCountryFromPhoneInput(
|
|
233
|
+
updatedValue,
|
|
234
|
+
this.selectedCountry
|
|
235
|
+
);
|
|
236
|
+
event.preventDefault();
|
|
237
|
+
input.setSelectionRange(updatedCursorPosition, updatedCursorPosition);
|
|
238
|
+
this.selectedCountry = inferredCountry;
|
|
239
|
+
this.setFormattedInputValue(input, updatedValue, inferredCountry);
|
|
240
|
+
this.dispatchPhoneChange();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private handleBlur(): void {
|
|
244
|
+
this.dispatchEvent(
|
|
245
|
+
new Event("phone-blur", { bubbles: true, composed: true })
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private get selectedCountryLabel(): string {
|
|
250
|
+
return (
|
|
251
|
+
phoneCountryOptions.find(
|
|
252
|
+
(option) => option.value === this.selectedCountry
|
|
253
|
+
)?.compactLabel ?? this.selectedCountry
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
render(): TemplateResult {
|
|
258
|
+
return html`
|
|
259
|
+
<div
|
|
260
|
+
class=${classMap({
|
|
261
|
+
"phone-input-container": true,
|
|
262
|
+
"phone-input-container--invalid": this.invalid,
|
|
263
|
+
})}
|
|
264
|
+
>
|
|
265
|
+
<div class="country-select-wrapper">
|
|
266
|
+
<span class="country-select-label" aria-hidden="true"
|
|
267
|
+
>${this.selectedCountryLabel}</span
|
|
268
|
+
>
|
|
269
|
+
<select
|
|
270
|
+
aria-label="Country calling code"
|
|
271
|
+
.value=${this.selectedCountry}
|
|
272
|
+
@change=${this.handleCountryChange}
|
|
273
|
+
>
|
|
274
|
+
${phoneCountryOptions.map(
|
|
275
|
+
(option) =>
|
|
276
|
+
html`<option
|
|
277
|
+
value=${option.value}
|
|
278
|
+
?selected=${option.value === this.selectedCountry}
|
|
279
|
+
>
|
|
280
|
+
${option.label}
|
|
281
|
+
</option>`
|
|
282
|
+
)}
|
|
283
|
+
</select>
|
|
284
|
+
</div>
|
|
285
|
+
<span class="phone-input-divider" aria-hidden="true"></span>
|
|
286
|
+
<input
|
|
287
|
+
class="webchat-input"
|
|
288
|
+
type="tel"
|
|
289
|
+
inputmode="tel"
|
|
290
|
+
placeholder="Phone"
|
|
291
|
+
name="phone"
|
|
292
|
+
autocomplete="tel-national"
|
|
293
|
+
maxlength="30"
|
|
294
|
+
.value=${this.value}
|
|
295
|
+
@keydown=${this.handleKeydown}
|
|
296
|
+
@input=${this.handleInput}
|
|
297
|
+
@blur=${this.handleBlur}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
declare global {
|
|
305
|
+
interface HTMLElementTagNameMap {
|
|
306
|
+
"international-phone-input": InternationalPhoneInput;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -29,20 +29,47 @@ export class MESelect extends LitElement {
|
|
|
29
29
|
@state()
|
|
30
30
|
private activeOptionIndex: number | null = null;
|
|
31
31
|
|
|
32
|
+
@state()
|
|
33
|
+
private searchTerm = "";
|
|
34
|
+
|
|
32
35
|
@query("#select", true)
|
|
33
36
|
meSelect!: HTMLDivElement;
|
|
34
37
|
|
|
38
|
+
@query("#selectText", true)
|
|
39
|
+
selectInput!: HTMLInputElement;
|
|
40
|
+
|
|
35
41
|
focus = (): void => {
|
|
36
|
-
this.
|
|
42
|
+
this.selectInput.focus();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
private closeSelect = (): void => {
|
|
46
|
+
this.isOpen = false;
|
|
47
|
+
this.searchTerm = "";
|
|
48
|
+
this.activeOption = null;
|
|
49
|
+
this.activeOptionIndex = null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
private openSelect = (): void => {
|
|
53
|
+
this.isOpen = true;
|
|
37
54
|
};
|
|
38
55
|
|
|
39
56
|
toggleSelect = (): void => {
|
|
40
57
|
if (this.isOpen) {
|
|
41
|
-
this.
|
|
58
|
+
this.closeSelect();
|
|
59
|
+
} else {
|
|
60
|
+
this.openSelect();
|
|
61
|
+
this.selectInput.focus();
|
|
42
62
|
}
|
|
43
|
-
this.isOpen = !this.isOpen;
|
|
44
63
|
};
|
|
45
64
|
|
|
65
|
+
private get visibleOptions(): MeSelectOption[] {
|
|
66
|
+
const searchTerm = this.searchTerm.trim().toLowerCase();
|
|
67
|
+
if (!searchTerm) return this.options;
|
|
68
|
+
return this.options.filter((option) =>
|
|
69
|
+
option.label.toLowerCase().includes(searchTerm)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
setSelectedOption = (option: MeSelectOption, closeSelect = true): void => {
|
|
47
74
|
if (this.value !== option.value) {
|
|
48
75
|
this.value = option.value;
|
|
@@ -51,8 +78,7 @@ export class MESelect extends LitElement {
|
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
if (closeSelect) {
|
|
54
|
-
this.
|
|
55
|
-
this.activeOption = null;
|
|
81
|
+
this.closeSelect();
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
if (!this.value) {
|
|
@@ -86,6 +112,12 @@ export class MESelect extends LitElement {
|
|
|
86
112
|
}
|
|
87
113
|
}
|
|
88
114
|
|
|
115
|
+
private handleSearchInput = (e: Event): void => {
|
|
116
|
+
this.searchTerm = (e.target as HTMLInputElement).value;
|
|
117
|
+
this.activeOption = null;
|
|
118
|
+
this.activeOptionIndex = null;
|
|
119
|
+
};
|
|
120
|
+
|
|
89
121
|
constructor() {
|
|
90
122
|
super();
|
|
91
123
|
// TODO: the current thing works, but want to investigate more. why doesn't it work with `this`?
|
|
@@ -94,8 +126,8 @@ export class MESelect extends LitElement {
|
|
|
94
126
|
// – for example, to Window, Document, or some element in the main DOM – you should add the
|
|
95
127
|
// listener in connectedCallback and remove it in disconnectedCallback." (https://lit.dev/docs/components/events/)
|
|
96
128
|
document.addEventListener("click", (event: MouseEvent) => {
|
|
97
|
-
if (!event.composedPath().includes(this
|
|
98
|
-
this.
|
|
129
|
+
if (!event.composedPath().includes(this)) {
|
|
130
|
+
this.closeSelect();
|
|
99
131
|
}
|
|
100
132
|
});
|
|
101
133
|
}
|
|
@@ -120,6 +152,7 @@ export class MESelect extends LitElement {
|
|
|
120
152
|
height: 14px;
|
|
121
153
|
display: flex;
|
|
122
154
|
align-items: center;
|
|
155
|
+
color: inherit;
|
|
123
156
|
}
|
|
124
157
|
|
|
125
158
|
ul {
|
|
@@ -215,6 +248,13 @@ export class MESelect extends LitElement {
|
|
|
215
248
|
render(): TemplateResult {
|
|
216
249
|
const activeIndex = this.activeOptionIndex;
|
|
217
250
|
const accessibleName = this.placeholder ?? "Select";
|
|
251
|
+
const visibleOptions = this.visibleOptions;
|
|
252
|
+
const selectedLabel = this.options.find(
|
|
253
|
+
(o) => o.value === this.value
|
|
254
|
+
)?.label;
|
|
255
|
+
const inputValue = this.isOpen
|
|
256
|
+
? this.searchTerm
|
|
257
|
+
: selectedLabel || this.value || "";
|
|
218
258
|
return html`
|
|
219
259
|
<div id="outer-select-container">
|
|
220
260
|
<div
|
|
@@ -224,34 +264,24 @@ export class MESelect extends LitElement {
|
|
|
224
264
|
["webchat-font__desktop"]: !isMobile(),
|
|
225
265
|
["webchat-font__mobile"]: isMobile(),
|
|
226
266
|
})}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
aria-controls=${this.listboxId}
|
|
232
|
-
aria-label=${accessibleName}
|
|
233
|
-
aria-activedescendant=${this.isOpen && activeIndex != null
|
|
234
|
-
? this.optionId(activeIndex)
|
|
235
|
-
: ""}
|
|
236
|
-
@click=${() => (this.isOpen = !this.isOpen)}
|
|
267
|
+
@mousedown=${(e: MouseEvent) => {
|
|
268
|
+
if (e.target !== this.selectInput) e.preventDefault();
|
|
269
|
+
}}
|
|
270
|
+
@click=${this.toggleSelect}
|
|
237
271
|
@keydown="${(e: KeyboardEvent) => {
|
|
238
272
|
switch (e.key) {
|
|
239
273
|
case "Enter":
|
|
240
|
-
case " ":
|
|
241
274
|
e.preventDefault();
|
|
242
275
|
if (this.isOpen && this.activeOption) {
|
|
243
276
|
this.setSelectedOption(this.activeOption);
|
|
244
|
-
} else {
|
|
245
|
-
this.
|
|
277
|
+
} else if (!this.isOpen) {
|
|
278
|
+
this.openSelect();
|
|
246
279
|
}
|
|
247
280
|
break;
|
|
248
281
|
case "Escape":
|
|
249
|
-
e.preventDefault();
|
|
250
|
-
this.isOpen = false;
|
|
251
|
-
break;
|
|
252
|
-
case "Tab":
|
|
253
282
|
if (this.isOpen) {
|
|
254
283
|
e.preventDefault();
|
|
284
|
+
this.closeSelect();
|
|
255
285
|
}
|
|
256
286
|
break;
|
|
257
287
|
case "ArrowUp":
|
|
@@ -261,41 +291,55 @@ export class MESelect extends LitElement {
|
|
|
261
291
|
0,
|
|
262
292
|
this.activeOptionIndex - 1
|
|
263
293
|
);
|
|
264
|
-
this.activeOption =
|
|
294
|
+
this.activeOption = visibleOptions[this.activeOptionIndex];
|
|
265
295
|
this.adjustScrollToActiveOption();
|
|
266
296
|
}
|
|
267
297
|
break;
|
|
268
|
-
|
|
269
298
|
case "ArrowDown":
|
|
270
299
|
e.preventDefault();
|
|
271
|
-
if (this.isOpen) {
|
|
300
|
+
if (this.isOpen && visibleOptions.length > 0) {
|
|
272
301
|
if (this.activeOptionIndex == null) {
|
|
273
302
|
this.activeOptionIndex = 0;
|
|
274
|
-
} else if (
|
|
303
|
+
} else if (
|
|
304
|
+
this.activeOptionIndex <
|
|
305
|
+
visibleOptions.length - 1
|
|
306
|
+
) {
|
|
275
307
|
this.activeOptionIndex++;
|
|
276
308
|
}
|
|
277
|
-
this.activeOption =
|
|
309
|
+
this.activeOption = visibleOptions[this.activeOptionIndex];
|
|
278
310
|
this.adjustScrollToActiveOption();
|
|
279
311
|
}
|
|
280
312
|
break;
|
|
281
313
|
}
|
|
282
314
|
}}"
|
|
283
315
|
>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
316
|
+
<input
|
|
317
|
+
id="selectText"
|
|
318
|
+
role="combobox"
|
|
319
|
+
aria-haspopup="listbox"
|
|
320
|
+
aria-expanded=${this.isOpen ? "true" : "false"}
|
|
321
|
+
aria-controls=${this.listboxId}
|
|
322
|
+
aria-label=${accessibleName}
|
|
323
|
+
aria-autocomplete="list"
|
|
324
|
+
aria-activedescendant=${this.isOpen && activeIndex != null
|
|
325
|
+
? this.optionId(activeIndex)
|
|
326
|
+
: ""}
|
|
327
|
+
autocomplete="off"
|
|
328
|
+
?readonly=${!this.isOpen}
|
|
329
|
+
.value=${inputValue}
|
|
330
|
+
placeholder=${this.isOpen
|
|
331
|
+
? selectedLabel || this.placeholder
|
|
332
|
+
: this.placeholder}
|
|
333
|
+
@click=${(e: Event) => {
|
|
334
|
+
e.stopPropagation();
|
|
335
|
+
if (!this.isOpen) this.openSelect();
|
|
336
|
+
}}
|
|
337
|
+
@focus=${() => {
|
|
338
|
+
if (!this.isOpen) this.openSelect();
|
|
339
|
+
}}
|
|
340
|
+
@blur=${this.closeSelect}
|
|
341
|
+
@input=${this.handleSearchInput}
|
|
342
|
+
/>
|
|
299
343
|
|
|
300
344
|
<svg
|
|
301
345
|
width="10"
|
|
@@ -312,7 +356,7 @@ export class MESelect extends LitElement {
|
|
|
312
356
|
</div>
|
|
313
357
|
${this.isOpen
|
|
314
358
|
? html`<ul id=${this.listboxId} role="listbox">
|
|
315
|
-
${
|
|
359
|
+
${visibleOptions.map(
|
|
316
360
|
(option, index) =>
|
|
317
361
|
html`<li
|
|
318
362
|
id=${this.optionId(index)}
|
|
@@ -320,6 +364,7 @@ export class MESelect extends LitElement {
|
|
|
320
364
|
aria-selected=${this.value === option.value
|
|
321
365
|
? "true"
|
|
322
366
|
: "false"}
|
|
367
|
+
@mousedown=${(e: MouseEvent) => e.preventDefault()}
|
|
323
368
|
@click="${() => this.setSelectedOption(option)}"
|
|
324
369
|
@mousemove="${() => {
|
|
325
370
|
this.activeOption = option;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { expect } from "@esm-bundle/chai";
|
|
2
|
+
import {
|
|
3
|
+
formatPhoneNumber,
|
|
4
|
+
formatToPhoneInput,
|
|
5
|
+
getCountryFromPhoneInput,
|
|
6
|
+
formatToUsPhoneInput,
|
|
7
|
+
getE164PhoneNumber,
|
|
8
|
+
getPhoneCountry,
|
|
9
|
+
isValidInternationalPhoneNumber,
|
|
10
|
+
phoneCountryOptions,
|
|
11
|
+
} from "./WebComponent/actions/formatPhoneNumber";
|
|
12
|
+
|
|
13
|
+
describe("international phone numbers", () => {
|
|
14
|
+
it("preserves and formats a UK E.164 number instead of truncating it", () => {
|
|
15
|
+
expect(formatToPhoneInput("+44 20 7946 0958")).to.equal("+44 20 7946 0958");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("keeps existing US formatting and E.164 output", () => {
|
|
19
|
+
expect(formatToPhoneInput("2025550123", "US")).to.equal("(202) 555-0123");
|
|
20
|
+
expect(getE164PhoneNumber("(202) 555-0123", "US")).to.equal("+12025550123");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("normalizes national and international UK input", () => {
|
|
24
|
+
expect(getE164PhoneNumber("07400 123456", "GB")).to.equal("+447400123456");
|
|
25
|
+
expect(getE164PhoneNumber("+44 7400 123456", "US")).to.equal(
|
|
26
|
+
"+447400123456"
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("handles shared calling codes and significant leading zeroes", () => {
|
|
31
|
+
expect(getE164PhoneNumber("416 555 0123", "CA")).to.equal("+14165550123");
|
|
32
|
+
expect(getE164PhoneNumber("02 1234 5678", "IT")).to.equal("+390212345678");
|
|
33
|
+
expect(getCountryFromPhoneInput("2025550123", "CA")).to.equal("CA");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("normalizes representative national numbers across regions", () => {
|
|
37
|
+
const samples = [
|
|
38
|
+
["US", "2025550123", "+12025550123"],
|
|
39
|
+
["CA", "4165550123", "+14165550123"],
|
|
40
|
+
["MX", "5512345678", "+525512345678"],
|
|
41
|
+
["GB", "07400123456", "+447400123456"],
|
|
42
|
+
["IT", "0212345678", "+390212345678"],
|
|
43
|
+
["AU", "0412345678", "+61412345678"],
|
|
44
|
+
["DE", "030123456", "+4930123456"],
|
|
45
|
+
["IN", "9876543210", "+919876543210"],
|
|
46
|
+
["JP", "09012345678", "+819012345678"],
|
|
47
|
+
["BR", "11912345678", "+5511912345678"],
|
|
48
|
+
["ZA", "0821234567", "+27821234567"],
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
for (const [country, nationalNumber, expectedE164] of samples) {
|
|
52
|
+
expect(getE164PhoneNumber(nationalNumber, country)).to.equal(
|
|
53
|
+
expectedE164,
|
|
54
|
+
country
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects impossible numbers", () => {
|
|
60
|
+
expect(isValidInternationalPhoneNumber("123", "GB")).to.equal(false);
|
|
61
|
+
expect(getE164PhoneNumber("123", "GB")).to.equal(null);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("offers every supported country with common regions pinned", () => {
|
|
65
|
+
expect(phoneCountryOptions.length).to.be.greaterThan(200);
|
|
66
|
+
expect(
|
|
67
|
+
phoneCountryOptions.slice(0, 4).map(({ value }) => value)
|
|
68
|
+
).to.deep.equal(["US", "CA", "MX", "GB"]);
|
|
69
|
+
expect(phoneCountryOptions.find(({ value }) => value === "MX")).to.include({
|
|
70
|
+
compactLabel: "🇲🇽 +52",
|
|
71
|
+
label: "🇲🇽 +52 – Mexico",
|
|
72
|
+
});
|
|
73
|
+
expect(
|
|
74
|
+
new Set(phoneCountryOptions.map(({ value }) => value)).size
|
|
75
|
+
).to.equal(phoneCountryOptions.length);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("falls back to the US for missing or unsupported building countries", () => {
|
|
79
|
+
expect(getPhoneCountry(null)).to.equal("US");
|
|
80
|
+
expect(getPhoneCountry("unknown")).to.equal("US");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not change the separate leasing-office display formatter", () => {
|
|
84
|
+
expect(formatPhoneNumber("+12025550123")).to.equal("+1 (202) 555-0123");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("keeps Text Us limited to its existing ten-digit US format", () => {
|
|
88
|
+
expect(formatToUsPhoneInput("2025550123")).to.equal("(202) 555-0123");
|
|
89
|
+
expect(formatToUsPhoneInput("+44 20 7946 0958")).to.equal("(442) 079-4609");
|
|
90
|
+
});
|
|
91
|
+
});
|