@block-auth.io/angular 0.2.10 → 0.3.0
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/.turbo/turbo-build.log +40 -37
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +102 -0
- package/.turbo/turbo-typecheck.log +19 -0
- package/CHANGELOG.md +113 -0
- package/package.json +3 -3
- package/src/lib/components/txns/internals/permissions-required-modal.component.test.ts +9 -0
- package/src/lib/components/txns/internals/permissions-required-modal.component.ts +3 -3
- package/src/lib/components/txns/internals/phone-input.component.test.ts +119 -0
- package/src/lib/components/txns/internals/phone-input.component.ts +301 -0
- package/src/lib/components/txns/internals/solicited-data-modal.component.test.ts +561 -4
- package/src/lib/components/txns/internals/solicited-data-modal.component.ts +324 -129
- package/src/lib/components/txns/internals/txn-type.component.ts +2 -2
- package/src/lib/components/txns/internals/txn.component.test.ts +345 -0
- package/src/lib/components/txns/internals/txn.component.ts +157 -74
- package/src/lib/components/txns/txns-overview.component.test.ts +270 -2
- package/src/lib/components/txns/txns-overview.component.ts +153 -23
- package/src/lib/primitives/arrow-icon.component.test.ts +13 -0
- package/src/lib/primitives/arrow-icon.component.ts +8 -1
- package/src/public-api.ts +1 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
HostListener,
|
|
6
|
+
computed,
|
|
7
|
+
effect,
|
|
8
|
+
inject,
|
|
9
|
+
input,
|
|
10
|
+
linkedSignal,
|
|
11
|
+
output,
|
|
12
|
+
signal,
|
|
13
|
+
viewChild,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import {
|
|
16
|
+
$locale,
|
|
17
|
+
getDefaultPhoneCountry,
|
|
18
|
+
getPhoneCountries,
|
|
19
|
+
resolvePhoneDraft,
|
|
20
|
+
searchPhoneCountries,
|
|
21
|
+
t as translate,
|
|
22
|
+
updatePhoneDraft,
|
|
23
|
+
type PhoneCountryCode,
|
|
24
|
+
type PhoneDraft,
|
|
25
|
+
type TextKey,
|
|
26
|
+
} from '@block-auth.io/core';
|
|
27
|
+
import { injectStore } from '../../../inject-store';
|
|
28
|
+
|
|
29
|
+
@Component({
|
|
30
|
+
selector: 'ba-phone-input',
|
|
31
|
+
standalone: true,
|
|
32
|
+
host: { style: 'display: contents' },
|
|
33
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
34
|
+
template: `
|
|
35
|
+
<div class="bal-ba-glass-phone">
|
|
36
|
+
<div class="bal-ba-glass-phone-row">
|
|
37
|
+
<button
|
|
38
|
+
#countryButton
|
|
39
|
+
type="button"
|
|
40
|
+
[id]="inputId() + '-country'"
|
|
41
|
+
aria-haspopup="listbox"
|
|
42
|
+
[attr.aria-expanded]="isOpen() ? 'true' : 'false'"
|
|
43
|
+
[attr.aria-controls]="isOpen() ? listId() : null"
|
|
44
|
+
[attr.aria-label]="buttonLabel()"
|
|
45
|
+
class="bal-ba-glass-phone-country"
|
|
46
|
+
(click)="toggle()"
|
|
47
|
+
(keydown)="onButtonKeyDown($event)"
|
|
48
|
+
>
|
|
49
|
+
<span class="bal-ba-glass-phone-flag" aria-hidden="true">{{
|
|
50
|
+
selected()?.flag
|
|
51
|
+
}}</span>
|
|
52
|
+
<span aria-hidden="true">{{ selected()?.dialCode }}</span>
|
|
53
|
+
<svg
|
|
54
|
+
viewBox="0 0 10 10"
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
stroke-width="1.6"
|
|
58
|
+
stroke-linecap="round"
|
|
59
|
+
stroke-linejoin="round"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
class="bal-ba-glass-phone-chevron"
|
|
62
|
+
>
|
|
63
|
+
<path d="M2 3.5 5 6.5 8 3.5" />
|
|
64
|
+
</svg>
|
|
65
|
+
</button>
|
|
66
|
+
<div class="bal-ba-glass-input-wrap">
|
|
67
|
+
<input
|
|
68
|
+
#nationalInput
|
|
69
|
+
[id]="inputId()"
|
|
70
|
+
type="tel"
|
|
71
|
+
inputmode="tel"
|
|
72
|
+
autocomplete="tel-national"
|
|
73
|
+
[value]="draft().nationalNumber"
|
|
74
|
+
[attr.aria-invalid]="invalid() ? 'true' : 'false'"
|
|
75
|
+
[attr.aria-describedby]="describedBy()"
|
|
76
|
+
class="bal-ba-glass-input"
|
|
77
|
+
(input)="onNationalInput($event)"
|
|
78
|
+
(blur)="blurred.emit()"
|
|
79
|
+
/>
|
|
80
|
+
<ng-content />
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
@if (isOpen()) {
|
|
84
|
+
<div class="bal-ba-glass-phone-menu">
|
|
85
|
+
<input
|
|
86
|
+
#search
|
|
87
|
+
type="text"
|
|
88
|
+
role="combobox"
|
|
89
|
+
autocomplete="off"
|
|
90
|
+
aria-expanded="true"
|
|
91
|
+
aria-autocomplete="list"
|
|
92
|
+
[id]="inputId() + '-country-search'"
|
|
93
|
+
[attr.aria-controls]="listId()"
|
|
94
|
+
[attr.aria-activedescendant]="activeId()"
|
|
95
|
+
[attr.aria-label]="t('searchPhoneCountry')"
|
|
96
|
+
[placeholder]="t('searchPhoneCountry')"
|
|
97
|
+
[value]="query()"
|
|
98
|
+
class="bal-ba-glass-input bal-ba-glass-phone-search"
|
|
99
|
+
(input)="onQueryInput($event)"
|
|
100
|
+
(keydown)="onSearchKeyDown($event)"
|
|
101
|
+
/>
|
|
102
|
+
@if (filtered().length) {
|
|
103
|
+
<ul
|
|
104
|
+
#list
|
|
105
|
+
role="listbox"
|
|
106
|
+
[id]="listId()"
|
|
107
|
+
[attr.aria-label]="t('phoneCountry')"
|
|
108
|
+
class="bal-ba-glass-phone-list"
|
|
109
|
+
>
|
|
110
|
+
@for (country of filtered(); track country.iso2; let index = $index) {
|
|
111
|
+
<li
|
|
112
|
+
role="option"
|
|
113
|
+
[id]="optionId(country.iso2)"
|
|
114
|
+
[attr.aria-selected]="
|
|
115
|
+
country.iso2 === draft().country ? 'true' : 'false'
|
|
116
|
+
"
|
|
117
|
+
[class]="
|
|
118
|
+
'bal-ba-glass-phone-option' +
|
|
119
|
+
(index === activeIndex()
|
|
120
|
+
? ' bal-ba-glass-phone-option-active'
|
|
121
|
+
: '')
|
|
122
|
+
"
|
|
123
|
+
(mousedown)="$event.preventDefault()"
|
|
124
|
+
(mouseenter)="activeIndex.set(index)"
|
|
125
|
+
(click)="select(country.iso2)"
|
|
126
|
+
>
|
|
127
|
+
<span aria-hidden="true">{{ country.flag }}</span>
|
|
128
|
+
<span class="bal-ba-glass-phone-option-name">{{
|
|
129
|
+
country.name
|
|
130
|
+
}}</span>
|
|
131
|
+
<span class="bal-ba-glass-phone-option-dial">{{
|
|
132
|
+
country.dialCode
|
|
133
|
+
}}</span>
|
|
134
|
+
</li>
|
|
135
|
+
}
|
|
136
|
+
</ul>
|
|
137
|
+
} @else {
|
|
138
|
+
<p role="status" class="bal-ba-glass-phone-empty">
|
|
139
|
+
{{ t('noPhoneCountryResults') }}
|
|
140
|
+
</p>
|
|
141
|
+
}
|
|
142
|
+
</div>
|
|
143
|
+
}
|
|
144
|
+
</div>
|
|
145
|
+
`,
|
|
146
|
+
})
|
|
147
|
+
export class PhoneInputComponent {
|
|
148
|
+
inputId = input.required<string>();
|
|
149
|
+
value = input<string>('');
|
|
150
|
+
invalid = input(false);
|
|
151
|
+
describedBy = input<string | null>(null);
|
|
152
|
+
|
|
153
|
+
valueChange = output<string>();
|
|
154
|
+
blurred = output<void>();
|
|
155
|
+
|
|
156
|
+
private readonly locale = injectStore($locale);
|
|
157
|
+
protected t = (key: TextKey): string => translate(key, this.locale());
|
|
158
|
+
|
|
159
|
+
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
160
|
+
private readonly countryButton =
|
|
161
|
+
viewChild.required<ElementRef<HTMLButtonElement>>('countryButton');
|
|
162
|
+
private readonly nationalInput =
|
|
163
|
+
viewChild.required<ElementRef<HTMLInputElement>>('nationalInput');
|
|
164
|
+
private readonly search = viewChild<ElementRef<HTMLInputElement>>('search');
|
|
165
|
+
private readonly list = viewChild<ElementRef<HTMLUListElement>>('list');
|
|
166
|
+
private readonly defaultCountry = getDefaultPhoneCountry();
|
|
167
|
+
|
|
168
|
+
protected readonly draft = linkedSignal<string, PhoneDraft>({
|
|
169
|
+
source: this.value,
|
|
170
|
+
computation: (value, previous) =>
|
|
171
|
+
resolvePhoneDraft(value, previous?.value ?? null, this.defaultCountry),
|
|
172
|
+
});
|
|
173
|
+
protected readonly isOpen = signal(false);
|
|
174
|
+
protected readonly query = signal('');
|
|
175
|
+
protected readonly activeIndex = signal(0);
|
|
176
|
+
protected readonly countries = computed(() =>
|
|
177
|
+
getPhoneCountries(this.locale()),
|
|
178
|
+
);
|
|
179
|
+
protected readonly filtered = computed(() =>
|
|
180
|
+
searchPhoneCountries(this.countries(), this.query()),
|
|
181
|
+
);
|
|
182
|
+
protected readonly selected = computed(() =>
|
|
183
|
+
this.countries().find((country) => country.iso2 === this.draft().country),
|
|
184
|
+
);
|
|
185
|
+
protected readonly listId = computed(() => `${this.inputId()}-countries`);
|
|
186
|
+
protected readonly activeId = computed(() => {
|
|
187
|
+
const active = this.filtered()[this.activeIndex()];
|
|
188
|
+
return active ? this.optionId(active.iso2) : null;
|
|
189
|
+
});
|
|
190
|
+
protected readonly buttonLabel = computed(() =>
|
|
191
|
+
`${this.t('phoneCountry')}: ${
|
|
192
|
+
this.selected()?.name ?? this.draft().country
|
|
193
|
+
} ${this.selected()?.dialCode ?? ''}`.trim(),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
constructor() {
|
|
197
|
+
effect(() => {
|
|
198
|
+
this.search()?.nativeElement.focus();
|
|
199
|
+
});
|
|
200
|
+
effect(() => {
|
|
201
|
+
const id = this.activeId();
|
|
202
|
+
const list = this.list()?.nativeElement;
|
|
203
|
+
if (!id || !list) return;
|
|
204
|
+
list
|
|
205
|
+
.querySelector<HTMLElement>(`#${id}`)
|
|
206
|
+
?.scrollIntoView?.({ block: 'nearest' });
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@HostListener('document:mousedown', ['$event'])
|
|
211
|
+
onDocumentMouseDown(event: MouseEvent): void {
|
|
212
|
+
if (!this.isOpen()) return;
|
|
213
|
+
if (!this.host.nativeElement.contains(event.target as Node)) this.isOpen.set(false);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@HostListener('keydown', ['$event'])
|
|
217
|
+
onKeyDown(event: KeyboardEvent): void {
|
|
218
|
+
if (event.key !== 'Escape' || !this.isOpen()) return;
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
event.stopPropagation();
|
|
221
|
+
this.close();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
focus(): void {
|
|
225
|
+
this.nationalInput().nativeElement.focus();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
protected optionId(iso2: string): string {
|
|
229
|
+
return `${this.inputId()}-country-${iso2}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
protected toggle(): void {
|
|
233
|
+
if (this.isOpen()) this.close();
|
|
234
|
+
else this.open();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
protected select(iso2: PhoneCountryCode): void {
|
|
238
|
+
this.commit(updatePhoneDraft(iso2, this.draft().nationalNumber));
|
|
239
|
+
this.isOpen.set(false);
|
|
240
|
+
this.focus();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
protected onNationalInput(event: Event): void {
|
|
244
|
+
this.commit(
|
|
245
|
+
updatePhoneDraft(
|
|
246
|
+
this.draft().country,
|
|
247
|
+
(event.target as HTMLInputElement).value,
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
protected onQueryInput(event: Event): void {
|
|
253
|
+
this.query.set((event.target as HTMLInputElement).value);
|
|
254
|
+
this.activeIndex.set(0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
protected onSearchKeyDown(event: KeyboardEvent): void {
|
|
258
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
const total = this.filtered().length;
|
|
261
|
+
if (!total) return;
|
|
262
|
+
const delta = event.key === 'ArrowDown' ? 1 : -1;
|
|
263
|
+
this.activeIndex.update((index) => (index + delta + total) % total);
|
|
264
|
+
} else if (event.key === 'Enter') {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
const active = this.filtered()[this.activeIndex()];
|
|
267
|
+
if (active) this.select(active.iso2);
|
|
268
|
+
} else if (event.key === 'Tab') {
|
|
269
|
+
this.isOpen.set(false);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
protected onButtonKeyDown(event: KeyboardEvent): void {
|
|
274
|
+
if (event.key !== 'ArrowDown' || this.isOpen()) return;
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
this.open();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private commit(next: PhoneDraft): void {
|
|
280
|
+
this.draft.set(next);
|
|
281
|
+
if (next.value !== this.value()) this.valueChange.emit(next.value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private open(): void {
|
|
285
|
+
this.query.set('');
|
|
286
|
+
this.activeIndex.set(
|
|
287
|
+
Math.max(
|
|
288
|
+
0,
|
|
289
|
+
this.countries().findIndex(
|
|
290
|
+
(country) => country.iso2 === this.draft().country,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
this.isOpen.set(true);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private close(): void {
|
|
298
|
+
this.isOpen.set(false);
|
|
299
|
+
this.countryButton().nativeElement.focus();
|
|
300
|
+
}
|
|
301
|
+
}
|