ngx-mat-input-tel 21.4.2 → 21.4.3

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.
Files changed (61) hide show
  1. package/.angulardoc.json +4 -0
  2. package/.editorconfig +13 -0
  3. package/.github/FUNDING.yml +13 -0
  4. package/.github/instructions/copilot-instructions.md +58 -0
  5. package/.github/workflows/ci.yml +27 -0
  6. package/.github/workflows/publish.yml +41 -0
  7. package/.github/workflows/test.yml +39 -0
  8. package/.husky/commit-msg +4 -0
  9. package/.husky/pre-commit +1 -0
  10. package/.prettierrc +15 -0
  11. package/angular.json +165 -0
  12. package/commitlint.config.ts +3 -0
  13. package/eslint.config.js +43 -0
  14. package/example-1.png +0 -0
  15. package/package.json +83 -55
  16. package/pnpm-workspace.yaml +18 -0
  17. package/projects/demo/eslint.config.js +3 -0
  18. package/projects/demo/karma.conf.js +31 -0
  19. package/projects/demo/src/app/app.html +123 -0
  20. package/projects/demo/src/app/app.scss +16 -0
  21. package/projects/demo/src/app/app.spec.ts +35 -0
  22. package/projects/demo/src/app/app.ts +100 -0
  23. package/projects/demo/src/app/dialog/dialog.html +12 -0
  24. package/projects/demo/src/app/dialog/dialog.ts +31 -0
  25. package/projects/demo/src/environments/environment.prod.ts +3 -0
  26. package/projects/demo/src/environments/environment.ts +3 -0
  27. package/projects/demo/src/favicon.ico +0 -0
  28. package/projects/demo/src/index.html +21 -0
  29. package/projects/demo/src/main.ts +16 -0
  30. package/projects/demo/src/styles.scss +32 -0
  31. package/projects/demo/tsconfig.app.json +9 -0
  32. package/projects/demo/tsconfig.spec.json +9 -0
  33. package/projects/ngx-mat-input-tel/eslint.config.js +3 -0
  34. package/projects/ngx-mat-input-tel/karma.conf.js +31 -0
  35. package/projects/ngx-mat-input-tel/ng-package.json +8 -0
  36. package/projects/ngx-mat-input-tel/package.json +46 -0
  37. package/projects/ngx-mat-input-tel/src/lib/assets/arrow_drop_down_grey600_18dp.png +0 -0
  38. package/projects/ngx-mat-input-tel/src/lib/assets/flags_sprite_2x.png +0 -0
  39. package/projects/ngx-mat-input-tel/src/lib/data/country-code.const.ts +792 -0
  40. package/projects/ngx-mat-input-tel/src/lib/model/country.model.ts +12 -0
  41. package/projects/ngx-mat-input-tel/src/lib/model/phone-number-format.model.ts +1 -0
  42. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel-dialog/ngx-mat-input-tel.dialog.html +82 -0
  43. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel-dialog/ngx-mat-input-tel.dialog.scss +91 -0
  44. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel-dialog/ngx-mat-input-tel.dialog.ts +128 -0
  45. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel-flag/ngx-mat-input-tel-flag.scss +319 -0
  46. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel-flag/ngx-mat-input-tel-flag.ts +72 -0
  47. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel.html +42 -0
  48. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel.scss +122 -0
  49. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel.spec.ts +318 -0
  50. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel.ts +625 -0
  51. package/projects/ngx-mat-input-tel/src/lib/ngx-mat-input-tel.validator.ts +35 -0
  52. package/projects/ngx-mat-input-tel/src/lib/remove-iso.pipe.ts +13 -0
  53. package/projects/ngx-mat-input-tel/src/public-api.ts +7 -0
  54. package/projects/ngx-mat-input-tel/src/test.ts +10 -0
  55. package/projects/ngx-mat-input-tel/tsconfig.lib.json +19 -0
  56. package/projects/ngx-mat-input-tel/tsconfig.lib.prod.json +9 -0
  57. package/projects/ngx-mat-input-tel/tsconfig.spec.json +8 -0
  58. package/tsconfig.json +28 -0
  59. package/fesm2022/ngx-mat-input-tel.mjs +0 -1603
  60. package/fesm2022/ngx-mat-input-tel.mjs.map +0 -1
  61. package/types/ngx-mat-input-tel.d.ts +0 -162
@@ -0,0 +1,42 @@
1
+ <div class="ngx-mat-input-tel-container">
2
+ <button
3
+ type="button"
4
+ matRipple
5
+ (click)="openCountrySelector()"
6
+ (focus)="onDialCodeFocus()"
7
+ (blur)="onDialCodeBlur()"
8
+ class="country-selector"
9
+ [class.separate]="separateDialCode"
10
+ [class.focused]="separateDialCode && isDialCodeFocused"
11
+ [disabled]="disabled"
12
+ [attr.aria-label]="ariaLabel"
13
+ >
14
+ <ngx-mat-input-tel-flag
15
+ class="main-flag"
16
+ [country]="{
17
+ iso2: $selectedCountry().iso2,
18
+ dialCode: $selectedCountry().dialCode,
19
+ }"
20
+ ></ngx-mat-input-tel-flag>
21
+ </button>
22
+
23
+ <input
24
+ class="ngx-mat-input-tel-input"
25
+ matInput
26
+ type="tel"
27
+ inputmode="tel"
28
+ [name]="name || 'tel'"
29
+ [autocomplete]="autocomplete"
30
+ [ngClass]="cssClass"
31
+ (blur)="onPhoneInputBlurAndTouch()"
32
+ (focus)="onPhoneInputFocus()"
33
+ (keypress)="onInputKeyPress($event)"
34
+ [(ngModel)]="phoneNumber"
35
+ (ngModelChange)="onPhoneNumberChange()"
36
+ [errorStateMatcher]="errorStateMatcher"
37
+ [placeholder]="placeholder || ($selectedCountry() | removeIso) || ''"
38
+ [disabled]="disabled"
39
+ #focusable
40
+ [maxlength]="maxLength"
41
+ />
42
+ </div>
@@ -0,0 +1,122 @@
1
+ :host {
2
+ &.ngx-floating {
3
+ .country-selector,
4
+ input.ngx-mat-input-tel-input::placeholder {
5
+ opacity: 1;
6
+ }
7
+ }
8
+
9
+ .ngx-mat-input-tel-container {
10
+ display: flex;
11
+ height: var(--ngxMatInputTel-height, 24px);
12
+ }
13
+
14
+ input {
15
+ border: none;
16
+ background: none;
17
+ outline: none;
18
+ font: inherit;
19
+ width: 100%;
20
+ box-sizing: border-box;
21
+ position: relative;
22
+ z-index: 0;
23
+
24
+ &.ngx-mat-input-tel-input {
25
+ &::placeholder {
26
+ opacity: var(--ngxMatInputTel-placeholder-opacity, var(--ngxMatInputTel-opacity, 0));
27
+ }
28
+ }
29
+ }
30
+
31
+ .mdc-button__label {
32
+ margin-right: auto;
33
+ }
34
+
35
+ .dialCode-text {
36
+ white-space: nowrap;
37
+ }
38
+
39
+ .country-selector {
40
+ display: flex;
41
+ align-items: center;
42
+ border-radius: 0;
43
+ flex-shrink: 0;
44
+ height: initial;
45
+ line-height: unset;
46
+ padding: 1px;
47
+ opacity: var(--ngxMatInputTel-selector-opacity, var(--ngxMatInputTel-opacity, 0));
48
+ transition: opacity 200ms;
49
+ border: unset;
50
+ background-color: transparent;
51
+ font: {
52
+ size: inherit;
53
+ weight: inherit;
54
+ }
55
+ background: {
56
+ image: url(assets/arrow_drop_down_grey600_18dp.png);
57
+ position: right center;
58
+ repeat: no-repeat;
59
+ size: 18px auto;
60
+ }
61
+
62
+ &:disabled {
63
+ color: rgba(#000, 0.38);
64
+ }
65
+
66
+ &.separate {
67
+ position: absolute;
68
+ left: 0;
69
+ width: 114px;
70
+ border-color: var(--mat-form-field-outlined-outline-color, var(--mat-sys-outline));
71
+ border-width: var(--mat-form-field-outlined-outline-width, 1px);
72
+ border-style: solid;
73
+ height: 100%;
74
+ padding-top: var(--mat-form-field-container-vertical-padding, 16px);
75
+ padding-bottom: var(--mat-form-field-container-vertical-padding, 16px);
76
+ min-height: var(--mat-form-field-container-height, 56px);
77
+ top: 0;
78
+ padding-right: max(
79
+ 16px,
80
+ var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))
81
+ );
82
+ padding-left: max(
83
+ 16px,
84
+ var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)) + 4px
85
+ );
86
+ transform: translate(calc(-100% - var(--ngxMatInputTel-gap, 32px)));
87
+ border-radius: var(
88
+ --mat-form-field-outlined-container-shape,
89
+ var(--mat-sys-corner-extra-small)
90
+ );
91
+ outline: none;
92
+
93
+ &.focused {
94
+ border-color: var(--mat-form-field-outlined-focus-outline-color, var(--mat-sys-primary));
95
+ border-width: var(--mat-form-field-outlined-focus-outline-width, 2px);
96
+ }
97
+ }
98
+ }
99
+
100
+ .country-selector-code {
101
+ color: var(--mdc-outlined-text-field-input-text-color);
102
+ padding-right: 18px;
103
+ }
104
+
105
+ .country-list-button {
106
+ color: rgba(#000, 0.87);
107
+ direction: ltr;
108
+ font-size: 16px;
109
+ font-weight: 400;
110
+ height: initial;
111
+ line-height: normal;
112
+ min-height: 48px;
113
+ padding: 14px 24px;
114
+ text-align: left;
115
+ text-transform: none;
116
+ width: 100%;
117
+ }
118
+
119
+ .main-flag {
120
+ padding-right: 20px;
121
+ }
122
+ }
@@ -0,0 +1,318 @@
1
+ /// <reference types="vitest/globals" />
2
+ import { ComponentFixture, TestBed } from "@angular/core/testing";
3
+ import { MatDividerModule } from "@angular/material/divider";
4
+ import { vi } from "vitest";
5
+
6
+ import { CommonModule } from "@angular/common";
7
+ import { FormsModule, ReactiveFormsModule } from "@angular/forms";
8
+ import { MatButtonModule } from "@angular/material/button";
9
+ import { MatDialogModule } from "@angular/material/dialog";
10
+ import { MatInputModule } from "@angular/material/input";
11
+ import { E164Number, NationalNumber } from "libphonenumber-js";
12
+ import { NgxMatInputTelComponent } from "./ngx-mat-input-tel";
13
+
14
+ describe("NgxMatInputTelComponent", () => {
15
+ let component: NgxMatInputTelComponent;
16
+ let fixture: ComponentFixture<NgxMatInputTelComponent>;
17
+
18
+ beforeEach(async () => {
19
+ await TestBed.configureTestingModule({
20
+ imports: [
21
+ CommonModule,
22
+ FormsModule,
23
+ MatInputModule,
24
+ MatDialogModule,
25
+ MatButtonModule,
26
+ MatDividerModule,
27
+ ReactiveFormsModule,
28
+ NgxMatInputTelComponent,
29
+ ],
30
+ }).compileComponents();
31
+ });
32
+
33
+ beforeEach(() => {
34
+ fixture = TestBed.createComponent(NgxMatInputTelComponent);
35
+ component = fixture.componentInstance;
36
+ fixture.detectChanges();
37
+ });
38
+
39
+ it("should create", () => {
40
+ expect(component).toBeTruthy();
41
+ });
42
+
43
+ describe("Separated Dial Code Focus", () => {
44
+ beforeEach(() => {
45
+ component.separateDialCode = true;
46
+ fixture.detectChanges();
47
+ });
48
+
49
+ it("should set isDialCodeFocused to true when dial code button receives focus", () => {
50
+ expect(component.isDialCodeFocused).toBe(false);
51
+ component.onDialCodeFocus();
52
+ expect(component.isDialCodeFocused).toBe(true);
53
+ });
54
+
55
+ it("should set isDialCodeFocused to false when dial code button loses focus", () => {
56
+ component.isDialCodeFocused = true;
57
+ component.onDialCodeBlur();
58
+ expect(component.isDialCodeFocused).toBe(false);
59
+ });
60
+
61
+ it("should set isPhoneInputFocused to true when phone input receives focus", () => {
62
+ expect(component.isPhoneInputFocused).toBe(false);
63
+ component.onPhoneInputFocus();
64
+ expect(component.isPhoneInputFocused).toBe(true);
65
+ });
66
+
67
+ it("should set isPhoneInputFocused to false when phone input loses focus", () => {
68
+ component.isPhoneInputFocused = true;
69
+ component.onPhoneInputBlur();
70
+ expect(component.isPhoneInputFocused).toBe(false);
71
+ });
72
+
73
+ it("should not set component.focused when dial code button gets focus in separated mode", () => {
74
+ component.focused = false;
75
+ component.onDialCodeFocus();
76
+ expect(component.focused).toBe(false);
77
+ });
78
+
79
+ it("should set component.focused when phone input gets focus in separated mode", () => {
80
+ component.focused = false;
81
+ component.onPhoneInputFocus();
82
+ expect(component.focused).toBe(true);
83
+ });
84
+
85
+ it("should verify separateDialCode and focus state in separated mode", () => {
86
+ component.onDialCodeFocus();
87
+ fixture.detectChanges();
88
+
89
+ // Verify component state
90
+ expect(component.separateDialCode).toBe(true);
91
+ expect(component.isDialCodeFocused).toBe(true);
92
+ expect(component.isPhoneInputFocused).toBe(false);
93
+ expect(component.focused).toBe(false); // mat-form-field should not be focused
94
+ });
95
+
96
+ it("should not have focused class on button when phone input has focus", () => {
97
+ component.onPhoneInputFocus();
98
+ fixture.detectChanges();
99
+
100
+ const button = fixture.nativeElement.querySelector(".country-selector");
101
+ expect(button.classList.contains("focused")).toBe(false);
102
+ expect(component.isDialCodeFocused).toBe(false);
103
+ expect(component.isPhoneInputFocused).toBe(true);
104
+ expect(component.focused).toBe(true); // mat-form-field should be focused
105
+ });
106
+
107
+ it("should handle rapid focus switching between elements", () => {
108
+ // Focus button
109
+ component.onDialCodeFocus();
110
+ expect(component.isDialCodeFocused).toBe(true);
111
+ expect(component.focused).toBe(false);
112
+
113
+ // Switch to input without blur event (edge case)
114
+ component.onPhoneInputFocus();
115
+ expect(component.isPhoneInputFocused).toBe(true);
116
+ expect(component.focused).toBe(true);
117
+
118
+ // Now blur button
119
+ component.onDialCodeBlur();
120
+ expect(component.isDialCodeFocused).toBe(false);
121
+ expect(component.focused).toBe(true); // Should stay true because input is focused
122
+ });
123
+ });
124
+
125
+ describe("enablePlaceholder", () => {
126
+ it("should populate selectedCountry placeholder when enablePlaceholder is set before ngOnInit", () => {
127
+ // Create a new fixture so we can set the input before the component initialises
128
+ const localFixture = TestBed.createComponent(NgxMatInputTelComponent);
129
+ const localComponent = localFixture.componentInstance;
130
+
131
+ localComponent.enablePlaceholder = true;
132
+ localComponent.defaultCountry = "US";
133
+
134
+ // detectChanges triggers ngOnInit
135
+ localFixture.detectChanges();
136
+
137
+ expect(localComponent.$selectedCountry().placeholder).toBeTruthy();
138
+ });
139
+
140
+ it("should leave selectedCountry placeholder empty when enablePlaceholder is not set", () => {
141
+ const localFixture = TestBed.createComponent(NgxMatInputTelComponent);
142
+ const localComponent = localFixture.componentInstance;
143
+
144
+ localComponent.defaultCountry = "US";
145
+ localFixture.detectChanges();
146
+
147
+ expect(localComponent.$selectedCountry().placeholder).toBeFalsy();
148
+ });
149
+
150
+ it("should populate placeholders when onlyCountries is set and enablePlaceholder is true", () => {
151
+ const localFixture = TestBed.createComponent(NgxMatInputTelComponent);
152
+ const localComponent = localFixture.componentInstance;
153
+
154
+ localComponent.enablePlaceholder = true;
155
+ localComponent.onlyCountries = ["US", "GB"];
156
+ localComponent.defaultCountry = "US";
157
+ localFixture.detectChanges();
158
+
159
+ expect(localComponent.$selectedCountry().placeholder).toBeTruthy();
160
+ expect(localComponent.$availableCountries().US.placeholder).toBeTruthy();
161
+ expect(localComponent.$availableCountries().GB.placeholder).toBeTruthy();
162
+ });
163
+ });
164
+
165
+ describe("Input and Label Bindings", () => {
166
+ it("should render input element", () => {
167
+ const input = fixture.nativeElement.querySelector("input");
168
+ expect(input).toBeTruthy();
169
+ expect(input.type).toBe("tel");
170
+ });
171
+
172
+ it("should bind placeholder input correctly", () => {
173
+ component.placeholder = "Enter phone number";
174
+ fixture.detectChanges();
175
+
176
+ const input = fixture.nativeElement.querySelector("input");
177
+ // When placeholder is set, it should be used (or country name if empty)
178
+ expect(component.placeholder).toBe("Enter phone number");
179
+ });
180
+
181
+ it("should render country selector button", () => {
182
+ const button = fixture.nativeElement.querySelector(".country-selector");
183
+ expect(button).toBeTruthy();
184
+ });
185
+
186
+ it("should bind aria-label input correctly", () => {
187
+ const customLabel = "Choose your country";
188
+ component.ariaLabel = customLabel;
189
+ fixture.detectChanges();
190
+
191
+ // Verify the property is set on the component
192
+ expect(component.ariaLabel).toBe(customLabel);
193
+ });
194
+ });
195
+
196
+ describe("ControlValueAccessor Implementation", () => {
197
+ it("should implement writeValue to update the phone number", () => {
198
+ const testPhoneNumber = "+33123456789";
199
+ component.writeValue(testPhoneNumber);
200
+ fixture.detectChanges();
201
+
202
+ expect(component.phoneNumber).toBeTruthy();
203
+ expect(component.numberInstance).toBeTruthy();
204
+ expect(component.numberInstance?.country).toBe("FR");
205
+ });
206
+
207
+ it("should update component phoneNumber when writeValue is called", () => {
208
+ const testPhoneNumber = "+33123456789";
209
+ component.writeValue(testPhoneNumber);
210
+ fixture.detectChanges();
211
+
212
+ expect(component.phoneNumber).toBeTruthy();
213
+ expect(component.numberInstance).toBeTruthy();
214
+ });
215
+
216
+ it("should handle empty value in writeValue", () => {
217
+ // First set a value
218
+ component.writeValue("+33123456789");
219
+ fixture.detectChanges();
220
+ expect(component.phoneNumber).toBeTruthy();
221
+ expect(component.value).toBeTruthy();
222
+
223
+ // writeValue with null/undefined/empty triggers onPhoneNumberChange
224
+ // which calls _setCountry. If phoneNumber is cleared manually, value becomes null
225
+ component.phoneNumber = "" as E164Number | NationalNumber;
226
+ component.onPhoneNumberChange();
227
+ fixture.detectChanges();
228
+
229
+ // After clearing phoneNumber and calling onPhoneNumberChange, value should be null
230
+ expect(component.value).toBeNull();
231
+ });
232
+
233
+ it("should register onChange callback", () => {
234
+ const mockCallback = vi.fn();
235
+ component.registerOnChange(mockCallback);
236
+
237
+ component.phoneNumber = "+33123456789" as E164Number | NationalNumber;
238
+ component.onPhoneNumberChange();
239
+
240
+ expect(mockCallback).toHaveBeenCalled();
241
+ });
242
+
243
+ it("should call onChange when user types in input", () => {
244
+ const mockCallback = vi.fn();
245
+ component.registerOnChange(mockCallback);
246
+
247
+ const input = fixture.nativeElement.querySelector("input");
248
+ input.value = "+33123456789";
249
+ input.dispatchEvent(new Event("input"));
250
+ fixture.detectChanges();
251
+
252
+ expect(mockCallback).toHaveBeenCalled();
253
+ });
254
+
255
+ it("should register onTouched callback", () => {
256
+ const mockCallback = vi.fn();
257
+ component.registerOnTouched(mockCallback);
258
+
259
+ expect(component.onTouched).toBe(mockCallback);
260
+ });
261
+ });
262
+
263
+ describe("Country Selection", () => {
264
+ it("should emit countryChanged when country is selected", () => {
265
+ vi.spyOn(component.countryChanged, "emit");
266
+
267
+ const country = component.getCountry("US");
268
+ component.onCountrySelect({
269
+ key: "US",
270
+ value: country,
271
+ });
272
+ fixture.detectChanges();
273
+
274
+ expect(component.countryChanged.emit).toHaveBeenCalledWith(country);
275
+ });
276
+
277
+ it("should update selected country when selecting a different country", () => {
278
+ const usCountry = component.getCountry("US");
279
+ component.onCountrySelect({
280
+ key: "US",
281
+ value: usCountry,
282
+ });
283
+ fixture.detectChanges();
284
+
285
+ expect(component.$selectedCountry().iso2).toBe("US");
286
+ });
287
+ });
288
+
289
+ describe("Disabled State", () => {
290
+ it("should set disabled state when setDisabledState is called with true", () => {
291
+ component.setDisabledState(true);
292
+ fixture.detectChanges();
293
+
294
+ expect(component.disabled).toBe(true);
295
+ });
296
+
297
+ it("should clear disabled state when setDisabledState is called with false", () => {
298
+ component.setDisabledState(true);
299
+ fixture.detectChanges();
300
+ component.setDisabledState(false);
301
+ fixture.detectChanges();
302
+
303
+ expect(component.disabled).toBe(false);
304
+ });
305
+
306
+ it("should respect disabled state for country selector", () => {
307
+ component.setDisabledState(true);
308
+ fixture.detectChanges();
309
+
310
+ expect(component.disabled).toBe(true);
311
+
312
+ // Verify that openCountrySelector respects disabled state
313
+ vi.spyOn(component["_dialog"], "open");
314
+ component.openCountrySelector();
315
+ expect(component["_dialog"].open).not.toHaveBeenCalled();
316
+ });
317
+ });
318
+ });