@genesislcap/foundation-ui 14.501.0 → 15.0.0-FUI-2567.2

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.
@@ -0,0 +1,437 @@
1
+ import { __decorate } from "tslib";
2
+ import { attr, DOM, observable } from '@microsoft/fast-element';
3
+ import { FormAssociated, FoundationElement } from '@microsoft/fast-foundation';
4
+ import { foundationVerificationCodeInputStyles as styles } from './verification-code-input.styles';
5
+ import { foundationVerificationCodeInputTemplate as template } from './verification-code-input.template';
6
+ export const foundationVerificationCodeInputShadowOptions = {
7
+ mode: 'open',
8
+ delegatesFocus: true,
9
+ };
10
+ export const defaultVerificationCodeInputConfig = {};
11
+ const NUMERIC_DIGIT_PATTERN = /^\d$/;
12
+ const DEFAULT_DIGITS = 6;
13
+ class _VerificationCodeInput extends FoundationElement {
14
+ }
15
+ /**
16
+ * Multi-digit verification code input for OTP / 2FA flows.
17
+ *
18
+ * Form-associated (FAST `FormAssociated`): host `name` / `required` / `value` participate
19
+ * in native HTML form validation and submission like {@link TextField}.
20
+ *
21
+ * @public
22
+ * @tagname %%prefix%%-verification-code-input
23
+ *
24
+ * @fires change - Fired when any digit changes. Detail: `{ value: string }`.
25
+ * @fires complete - Fired when all digits are filled. Detail: `{ value: string }`.
26
+ */
27
+ export class VerificationCodeInput extends FormAssociated(_VerificationCodeInput) {
28
+ constructor() {
29
+ super();
30
+ /** Per-digit values indexed from `0` to `digits - 1`. */
31
+ this.values = [];
32
+ this.controlListeners = new Map();
33
+ /** Skip digit re-sync when `value` is updated from digit entry (`commitValues`). */
34
+ this.syncingFromDigits = false;
35
+ /**
36
+ * Visual treatment applied to each digit field.
37
+ * @public
38
+ * @remarks
39
+ * HTML Attribute: appearance
40
+ * @defaultValue outline
41
+ */
42
+ this.appearance = 'outline';
43
+ /**
44
+ * Number of single-character digit fields to render.
45
+ * @public
46
+ * @remarks
47
+ * HTML Attribute: digits
48
+ * @defaultValue 6
49
+ */
50
+ this.digits = DEFAULT_DIGITS;
51
+ /**
52
+ * When true, digit fields are non-interactive.
53
+ * @public
54
+ * @remarks
55
+ * HTML Attribute: readonly
56
+ */
57
+ this.readOnly = false;
58
+ /**
59
+ * When true, digit fields are non-interactive.
60
+ * @public
61
+ * @remarks
62
+ * HTML Attribute: disabled
63
+ */
64
+ this.disabled = false;
65
+ /**
66
+ * When true, focuses the first digit on connect (and after SPA route commit when hosted that way).
67
+ * @public
68
+ * @remarks
69
+ * HTML Attribute: autofocus
70
+ */
71
+ this.autofocus = false;
72
+ /**
73
+ * When true, applies the error visual state to each digit field.
74
+ * @public
75
+ * @remarks
76
+ * HTML Attribute: error
77
+ */
78
+ this.error = false;
79
+ this.proxy = document.createElement('input');
80
+ }
81
+ get digitsIndexes() {
82
+ return Array.from({ length: this.digits }, (_, index) => index);
83
+ }
84
+ get controlIdPrefix() {
85
+ return this.id || 'verification-code';
86
+ }
87
+ get labelId() {
88
+ return `${this.controlIdPrefix}-label`;
89
+ }
90
+ get hasLabel() {
91
+ var _a;
92
+ return !!((_a = this.defaultSlottedNodes) === null || _a === void 0 ? void 0 : _a.length);
93
+ }
94
+ getControlId(index) {
95
+ return `${this.controlIdPrefix}-${index}`;
96
+ }
97
+ /** Clears digits, emits `change`, and focuses the first field when connected. */
98
+ clear() {
99
+ this.value = '';
100
+ this.emitChange();
101
+ if (this.$fastController.isConnected) {
102
+ this.focusDigit(0);
103
+ }
104
+ }
105
+ focus() {
106
+ this.focusDigit(0);
107
+ }
108
+ /** @internal */
109
+ valueChanged(previous, next) {
110
+ const cleaned = this.sanitizeCode(next);
111
+ if (!this.syncingFromDigits) {
112
+ this.syncDigitsFromValue(cleaned);
113
+ }
114
+ if (String(next !== null && next !== void 0 ? next : '') !== cleaned) {
115
+ this.value = cleaned;
116
+ return;
117
+ }
118
+ super.valueChanged(previous, cleaned);
119
+ }
120
+ /** @internal */
121
+ validate(anchor) {
122
+ var _a, _b;
123
+ if (!(this.proxy instanceof HTMLElement)) {
124
+ return;
125
+ }
126
+ const validationAnchor = (_a = anchor !== null && anchor !== void 0 ? anchor : this.getControlElement(0)) !== null && _a !== void 0 ? _a : undefined;
127
+ const value = String((_b = this.value) !== null && _b !== void 0 ? _b : '');
128
+ if (value.length > 0 && !this.isComplete()) {
129
+ this.setValidity({ patternMismatch: true }, `Please enter a ${this.digits}-digit code.`, validationAnchor);
130
+ return;
131
+ }
132
+ if (this.required && !this.isComplete()) {
133
+ this.setValidity({ valueMissing: true }, 'Please fill out this field.', validationAnchor);
134
+ return;
135
+ }
136
+ this.setValidity({}, '', validationAnchor);
137
+ }
138
+ connectedCallback() {
139
+ super.connectedCallback();
140
+ this.proxy.type = 'text';
141
+ this.proxy.inputMode = 'numeric';
142
+ DOM.queueUpdate(() => {
143
+ this.syncFieldValues();
144
+ this.bindControlListeners();
145
+ if (this.autofocus && !this.readOnly) {
146
+ this.focus();
147
+ }
148
+ });
149
+ }
150
+ disconnectedCallback() {
151
+ super.disconnectedCallback();
152
+ this.unbindControlListeners();
153
+ }
154
+ digitsChanged() {
155
+ var _a;
156
+ this.syncDigitsFromValue(this.sanitizeCode(this.value));
157
+ const joined = ((_a = this.values) !== null && _a !== void 0 ? _a : []).join('');
158
+ if (this.value !== joined) {
159
+ this.value = joined;
160
+ }
161
+ else {
162
+ this.validate();
163
+ }
164
+ DOM.queueUpdate(() => {
165
+ this.syncFieldValues();
166
+ this.bindControlListeners();
167
+ });
168
+ }
169
+ get digitClassnames() {
170
+ return [this.error && 'error', (this.disabled || this.readOnly) && 'disabled']
171
+ .filter(Boolean)
172
+ .join(' ');
173
+ }
174
+ sanitizeCode(raw) {
175
+ const length = Math.max(1, Number(this.digits) || DEFAULT_DIGITS);
176
+ return String(raw !== null && raw !== void 0 ? raw : '')
177
+ .replace(/\D/g, '')
178
+ .slice(0, length);
179
+ }
180
+ bindControlListeners() {
181
+ this.unbindControlListeners();
182
+ const inputs = this.rootNode.querySelectorAll('[data-digit-index] input.control');
183
+ inputs.forEach((input) => {
184
+ const index = this.getIndexFromInput(input);
185
+ if (index === null) {
186
+ return;
187
+ }
188
+ const listeners = {
189
+ paste: (event) => this.onPaste(event, index),
190
+ keydown: (event) => this.onKeyDown(event, index),
191
+ input: (event) => this.onInput(event, index),
192
+ };
193
+ input.addEventListener('paste', listeners.paste, true);
194
+ input.addEventListener('keydown', listeners.keydown);
195
+ input.addEventListener('input', listeners.input);
196
+ this.controlListeners.set(input, listeners);
197
+ });
198
+ }
199
+ unbindControlListeners() {
200
+ var _a, _b;
201
+ (_a = this.controlListeners) === null || _a === void 0 ? void 0 : _a.forEach((listeners, input) => {
202
+ input.removeEventListener('paste', listeners.paste, true);
203
+ input.removeEventListener('keydown', listeners.keydown);
204
+ input.removeEventListener('input', listeners.input);
205
+ });
206
+ (_b = this.controlListeners) === null || _b === void 0 ? void 0 : _b.clear();
207
+ }
208
+ onKeyDown(event, index) {
209
+ if (this.readOnly || this.disabled) {
210
+ return;
211
+ }
212
+ if (NUMERIC_DIGIT_PATTERN.test(event.key) &&
213
+ !event.ctrlKey &&
214
+ !event.metaKey &&
215
+ !event.altKey) {
216
+ event.preventDefault();
217
+ this.updateDigit(index, event.key);
218
+ this.focusDigit(index + 1);
219
+ return;
220
+ }
221
+ switch (event.key) {
222
+ case 'Backspace':
223
+ event.preventDefault();
224
+ this.handleBackspace(index);
225
+ break;
226
+ case 'Delete':
227
+ event.preventDefault();
228
+ this.handleDelete(index);
229
+ break;
230
+ case 'ArrowLeft':
231
+ event.preventDefault();
232
+ this.focusDigit(index - 1);
233
+ break;
234
+ case 'ArrowRight':
235
+ event.preventDefault();
236
+ this.focusDigit(index + 1);
237
+ break;
238
+ case 'Tab':
239
+ case 'Home':
240
+ case 'End':
241
+ break;
242
+ default:
243
+ if (this.isBlockedCharacter(event)) {
244
+ event.preventDefault();
245
+ }
246
+ break;
247
+ }
248
+ }
249
+ isBlockedCharacter(event) {
250
+ if (event.ctrlKey || event.metaKey || event.altKey) {
251
+ return false;
252
+ }
253
+ return event.key.length === 1 && !NUMERIC_DIGIT_PATTERN.test(event.key);
254
+ }
255
+ onInput(event, index) {
256
+ if (this.readOnly || this.disabled) {
257
+ return;
258
+ }
259
+ const input = event.target;
260
+ const digits = input.value.replace(/\D/g, '');
261
+ if (digits.length > 1) {
262
+ this.applyDigits(digits, index);
263
+ return;
264
+ }
265
+ const digit = digits.slice(-1);
266
+ if (input.value !== digit) {
267
+ input.value = digit;
268
+ }
269
+ if (digit && this.values[index] !== digit) {
270
+ this.updateDigit(index, digit);
271
+ this.focusDigit(index + 1);
272
+ }
273
+ else if (!digit && this.values[index]) {
274
+ this.updateDigit(index, '');
275
+ }
276
+ }
277
+ onPaste(event, index) {
278
+ var _a, _b;
279
+ if (this.readOnly || this.disabled) {
280
+ return;
281
+ }
282
+ event.preventDefault();
283
+ const pasted = (_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text')) !== null && _b !== void 0 ? _b : '';
284
+ if (pasted) {
285
+ this.applyDigits(pasted, index);
286
+ }
287
+ }
288
+ handleBackspace(index) {
289
+ if (this.values[index]) {
290
+ this.updateDigit(index, '');
291
+ return;
292
+ }
293
+ if (index > 0) {
294
+ this.updateDigit(index - 1, '');
295
+ this.focusDigit(index - 1);
296
+ }
297
+ }
298
+ handleDelete(index) {
299
+ if (this.values[index]) {
300
+ this.updateDigit(index, '');
301
+ }
302
+ }
303
+ getIndexFromInput(input) {
304
+ var _a;
305
+ const index = Number((_a = input.closest('[data-digit-index]')) === null || _a === void 0 ? void 0 : _a.getAttribute('data-digit-index'));
306
+ return Number.isNaN(index) ? null : index;
307
+ }
308
+ /** Maps a sanitized code string into per-digit `values` and syncs the DOM when connected. */
309
+ syncDigitsFromValue(cleaned) {
310
+ var _a, _b;
311
+ const length = Math.max(1, Number(this.digits) || DEFAULT_DIGITS);
312
+ const nextValues = Array.from({ length }, (_, index) => { var _a; return (_a = cleaned[index]) !== null && _a !== void 0 ? _a : ''; });
313
+ const current = (_a = this.values) !== null && _a !== void 0 ? _a : [];
314
+ if (nextValues.length === current.length &&
315
+ nextValues.every((digit, index) => digit === current[index])) {
316
+ return;
317
+ }
318
+ this.values = nextValues;
319
+ if ((_b = this.$fastController) === null || _b === void 0 ? void 0 : _b.isConnected) {
320
+ this.syncFieldValues();
321
+ }
322
+ }
323
+ updateDigit(index, digit) {
324
+ if (index < 0 || index >= this.digits) {
325
+ return;
326
+ }
327
+ const nextValues = [...this.values];
328
+ nextValues[index] = digit;
329
+ this.values = nextValues;
330
+ this.syncFieldValue(index);
331
+ this.commitValues(nextValues);
332
+ }
333
+ applyDigits(raw, startIndex) {
334
+ const digits = raw.replace(/\D/g, '').slice(0, this.digits - startIndex);
335
+ if (!digits.length) {
336
+ return;
337
+ }
338
+ const nextValues = [...this.values];
339
+ for (let i = 0; i < digits.length; i += 1) {
340
+ nextValues[startIndex + i] = digits[i];
341
+ }
342
+ this.values = nextValues;
343
+ this.syncFieldValues();
344
+ this.commitValues(nextValues);
345
+ this.focusDigit(this.isComplete() ? this.digits - 1 : startIndex + digits.length);
346
+ }
347
+ /** Pushes digit state into the form-associated `value` and emits change / complete. */
348
+ commitValues(nextValues) {
349
+ const joined = nextValues.join('');
350
+ this.syncingFromDigits = true;
351
+ try {
352
+ if (this.value !== joined) {
353
+ this.value = joined;
354
+ }
355
+ else {
356
+ this.validate();
357
+ }
358
+ }
359
+ finally {
360
+ this.syncingFromDigits = false;
361
+ }
362
+ this.emitChange();
363
+ if (this.isComplete()) {
364
+ this.$emit('complete', { value: this.value });
365
+ }
366
+ }
367
+ syncFieldValues() {
368
+ for (let index = 0; index < this.digits; index += 1) {
369
+ this.syncFieldValue(index);
370
+ }
371
+ }
372
+ syncFieldValue(index) {
373
+ var _a;
374
+ const control = this.getControlElement(index);
375
+ if (control) {
376
+ control.value = (_a = this.values[index]) !== null && _a !== void 0 ? _a : '';
377
+ }
378
+ }
379
+ focusDigit(index) {
380
+ var _a;
381
+ if (index < 0 || index >= this.digits) {
382
+ return;
383
+ }
384
+ (_a = this.getControlElement(index)) === null || _a === void 0 ? void 0 : _a.focus();
385
+ }
386
+ get rootNode() {
387
+ var _a;
388
+ return (_a = this.shadowRoot) !== null && _a !== void 0 ? _a : this;
389
+ }
390
+ getControlElement(index) {
391
+ return this.rootNode.querySelector(`[data-digit-index="${index}"] input.control`);
392
+ }
393
+ isComplete() {
394
+ var _a;
395
+ const values = (_a = this.values) !== null && _a !== void 0 ? _a : [];
396
+ return (values.length === this.digits && values.every((digit) => NUMERIC_DIGIT_PATTERN.test(digit)));
397
+ }
398
+ emitChange() {
399
+ this.$emit('change', { value: this.value });
400
+ }
401
+ }
402
+ __decorate([
403
+ observable
404
+ ], VerificationCodeInput.prototype, "values", void 0);
405
+ __decorate([
406
+ observable
407
+ ], VerificationCodeInput.prototype, "defaultSlottedNodes", void 0);
408
+ __decorate([
409
+ attr
410
+ ], VerificationCodeInput.prototype, "id", void 0);
411
+ __decorate([
412
+ attr
413
+ ], VerificationCodeInput.prototype, "appearance", void 0);
414
+ __decorate([
415
+ attr
416
+ ], VerificationCodeInput.prototype, "digits", void 0);
417
+ __decorate([
418
+ attr({ attribute: 'readonly', mode: 'boolean' })
419
+ ], VerificationCodeInput.prototype, "readOnly", void 0);
420
+ __decorate([
421
+ attr({ mode: 'boolean' })
422
+ ], VerificationCodeInput.prototype, "disabled", void 0);
423
+ __decorate([
424
+ attr({ mode: 'boolean' })
425
+ ], VerificationCodeInput.prototype, "autofocus", void 0);
426
+ __decorate([
427
+ attr({ mode: 'boolean' })
428
+ ], VerificationCodeInput.prototype, "error", void 0);
429
+ /**
430
+ * The Foundation Verification Code Input
431
+ *
432
+ * @public
433
+ * @remarks
434
+ * HTML Element: \<foundation-verification-code-input\>
435
+ */
436
+ export const foundationVerificationCodeInput = VerificationCodeInput.compose(Object.assign({ baseName: 'verification-code-input', template,
437
+ styles, shadowOptions: foundationVerificationCodeInputShadowOptions }, defaultVerificationCodeInputConfig));
@@ -0,0 +1,108 @@
1
+ import { activeColorScheme } from '@genesislcap/foundation-utils';
2
+ import { css } from '@microsoft/fast-element';
3
+ import { sharedFieldStyles } from '../_common';
4
+ export const foundationVerificationCodeInputStyles = css `
5
+ :host {
6
+ display: block;
7
+ font-family: var(--body-font);
8
+ outline: none;
9
+ }
10
+
11
+ .label {
12
+ display: block;
13
+ color: var(--neutral-foreground-rest);
14
+ cursor: pointer;
15
+ font-size: var(--type-ramp-base-font-size);
16
+ line-height: var(--type-ramp-base-line-height);
17
+ margin-bottom: 4px;
18
+ }
19
+
20
+ .label-hidden {
21
+ display: none;
22
+ }
23
+
24
+ :host([readonly]) .label {
25
+ cursor: var(--disabled-cursor);
26
+ }
27
+
28
+ .container {
29
+ display: flex;
30
+ gap: calc(var(--design-unit) * 1px);
31
+ align-items: center;
32
+ }
33
+
34
+ .digit {
35
+ display: inline-block;
36
+ box-sizing: border-box;
37
+ outline: none;
38
+ min-width: calc(
39
+ (((var(--base-height-multiplier) + var(--density)) * var(--design-unit)) - 4) * 1px
40
+ );
41
+ }
42
+
43
+ .digit .root {
44
+ box-sizing: border-box;
45
+ position: relative;
46
+ display: flex;
47
+ flex-direction: row;
48
+ align-items: center;
49
+ color: var(--neutral-foreground-rest);
50
+ background: var(--neutral-fill-input-rest);
51
+ border-radius: calc(var(--control-corner-radius) * 1px);
52
+ border: calc(var(--stroke-width) * 1px) solid var(--accent-fill-rest);
53
+ height: calc(
54
+ (((var(--base-height-multiplier) + var(--density)) * var(--design-unit)) - 4) * 1px
55
+ );
56
+ }
57
+
58
+ .digit .control {
59
+ appearance: none;
60
+ font: inherit;
61
+ background: transparent;
62
+ border: 0;
63
+ color: inherit;
64
+ height: calc(100% - 4px);
65
+ width: 100%;
66
+ margin-top: auto;
67
+ margin-bottom: auto;
68
+ padding: 0 calc(var(--design-unit) * 2px + 1px);
69
+ font-size: var(--type-ramp-base-font-size);
70
+ line-height: var(--type-ramp-base-line-height);
71
+ text-align: center;
72
+ outline: none;
73
+ color-scheme: ${activeColorScheme};
74
+ }
75
+
76
+ .digit[appearance='filled'] .root {
77
+ background: var(--neutral-fill-rest);
78
+ }
79
+
80
+ .digit.error .root {
81
+ border-color: var(--error-fill-rest);
82
+ }
83
+
84
+ .digit.disabled {
85
+ opacity: var(--disabled-opacity);
86
+ cursor: var(--disabled-cursor);
87
+ }
88
+
89
+ .digit.disabled .control {
90
+ cursor: var(--disabled-cursor);
91
+ }
92
+
93
+ .digit:hover:not(.disabled) .root {
94
+ background: var(--neutral-fill-input-hover);
95
+ border-color: var(--accent-fill-hover);
96
+ }
97
+
98
+ .digit:focus-within:not(.disabled) .root {
99
+ border-color: var(--focus-stroke-outer);
100
+ box-shadow: 0 0 0 calc(var(--focus-stroke-width) * 1px) var(--focus-stroke-outer) inset;
101
+ }
102
+
103
+ .digit[appearance='filled']:hover:not(.disabled) .root {
104
+ background: var(--neutral-fill-hover);
105
+ }
106
+
107
+ ${sharedFieldStyles}
108
+ `;
@@ -0,0 +1,55 @@
1
+ import { html, repeat, slotted } from '@microsoft/fast-element';
2
+ import { whitespaceFilter } from '@microsoft/fast-foundation';
3
+ export const foundationVerificationCodeInputTemplate = html `
4
+ <label
5
+ part="label"
6
+ id="${(x) => x.labelId}"
7
+ for="${(x) => x.getControlId(0)}"
8
+ class="${(x) => x.defaultSlottedNodes && x.defaultSlottedNodes.length ? 'label' : 'label label-hidden'}"
9
+ >
10
+ <slot
11
+ ${slotted({
12
+ property: 'defaultSlottedNodes',
13
+ filter: whitespaceFilter,
14
+ })}
15
+ ></slot>
16
+ </label>
17
+ <div
18
+ class="container"
19
+ role="group"
20
+ aria-labelledby="${(x) => (x.hasLabel ? x.labelId : void 0)}"
21
+ aria-label="${(x) => (x.hasLabel ? void 0 : 'Verification code')}"
22
+ >
23
+ ${repeat((x) => x.digitsIndexes, html `
24
+ <div
25
+ class="digit ${(x, c) => c.parent.digitClassnames}"
26
+ data-digit-index="${(x) => x}"
27
+ appearance="${(x, c) => c.parent.appearance}"
28
+ >
29
+ <div class="root" part="root">
30
+ <div class="control-field">
31
+ <input
32
+ class="control"
33
+ part="control"
34
+ type="text"
35
+ inputmode="numeric"
36
+ pattern="[0-9]*"
37
+ name="${(x, c) => c.parent.getControlId(x)}"
38
+ id="${(x, c) => c.parent.getControlId(x)}"
39
+ autocomplete="${(x) => (x === 0 ? 'one-time-code' : 'off')}"
40
+ data-test-id="${(x, c) => c.parent.getControlId(x)}"
41
+ title="Only numbers are allowed"
42
+ maxlength="1"
43
+ ?autofocus="${(x, c) => c.parent.autofocus && x === 0}"
44
+ ?readonly="${(x, c) => c.parent.readOnly}"
45
+ ?disabled="${(x, c) => c.parent.disabled ||
46
+ c.parent.readOnly}"
47
+ ?aria-required="${(x, c) => c.parent.required}"
48
+ ?aria-invalid="${(x, c) => c.parent.error}"
49
+ />
50
+ </div>
51
+ </div>
52
+ </div>
53
+ `)}
54
+ </div>
55
+ `;