tyrell-components 1.0.0-TC58 → 1.0.0-TC59
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/css/tyrell-colors-static.css +14 -0
- package/css/tyrell-theme.css +1 -0
- package/css/tyrell.css +8 -6
- package/dist/tyrell-colors-static.css +1 -1
- package/dist/tyrell-theme.css +1 -1
- package/dist/tyrell.js +1 -1
- package/lib/base/ty-component.d.ts +8 -12
- package/lib/base/ty-component.js +38 -9
- package/lib/components/button.d.ts +10 -0
- package/lib/components/button.js +22 -1
- package/lib/components/calendar-month.d.ts +7 -1
- package/lib/components/calendar-month.js +32 -18
- package/lib/components/calendar-navigation.js +19 -6
- package/lib/components/calendar.d.ts +24 -5
- package/lib/components/calendar.js +83 -37
- package/lib/components/checkbox.d.ts +5 -0
- package/lib/components/checkbox.js +18 -5
- package/lib/components/copy.d.ts +2 -0
- package/lib/components/copy.js +15 -31
- package/lib/components/date-picker.d.ts +7 -26
- package/lib/components/date-picker.js +14 -84
- package/lib/components/file-upload.js +6 -2
- package/lib/components/input.d.ts +0 -9
- package/lib/components/input.js +17 -40
- package/lib/components/modal.js +59 -18
- package/lib/components/popup.js +56 -5
- package/lib/components/radio.d.ts +6 -0
- package/lib/components/radio.js +12 -28
- package/lib/components/select.d.ts +29 -11
- package/lib/components/select.js +43 -57
- package/lib/components/switch.d.ts +4 -0
- package/lib/components/switch.js +18 -5
- package/lib/components/tabs.js +1 -1
- package/lib/components/textarea.d.ts +1 -0
- package/lib/components/textarea.js +21 -8
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/styles/button.js +1 -0
- package/lib/styles/calendar-month.js +1 -1
- package/lib/styles/calendar.js +3 -0
- package/lib/styles/checkbox.js +11 -5
- package/lib/styles/date-picker.js +5 -10
- package/lib/styles/input.js +5 -9
- package/lib/styles/option.d.ts +1 -1
- package/lib/styles/option.js +4 -0
- package/lib/styles/popup.d.ts +1 -1
- package/lib/styles/popup.js +2 -0
- package/lib/styles/radio.js +10 -4
- package/lib/styles/select-base.js +5 -10
- package/lib/styles/select.d.ts +1 -1
- package/lib/styles/select.js +4 -0
- package/lib/styles/switch.js +10 -5
- package/lib/styles/tag.js +2 -1
- package/lib/utils/calendar-utils.d.ts +49 -0
- package/lib/utils/calendar-utils.js +63 -6
- package/lib/utils/scroll-lock.d.ts +10 -4
- package/lib/utils/scroll-lock.js +102 -2
- package/lib/utils/validity.d.ts +27 -1
- package/lib/utils/validity.js +68 -16
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/dist/tyrell.js.map +0 -1
- package/lib/utils/error-flavor.d.ts +0 -8
- package/lib/utils/error-flavor.js +0 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ensureStyles } from '../utils/styles.js';
|
|
2
2
|
import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
|
|
3
|
-
import { ERROR_FLAVOR } from '../utils/error-flavor.js';
|
|
4
3
|
import { datePickerStyles, datePickerCustomFlavorCss } from '../styles/date-picker.js';
|
|
5
4
|
import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
|
|
6
5
|
import { computeAnchoredPosition, placementToAnchored } from '../utils/positioning.js';
|
|
@@ -9,6 +8,7 @@ import { isMobileTouch } from '../utils/mobile.js';
|
|
|
9
8
|
import { TyComponent } from '../base/ty-component.js';
|
|
10
9
|
import { DEFAULT_SIZE } from '../types/common.js';
|
|
11
10
|
import { nativeMessage } from '../utils/validity.js';
|
|
11
|
+
import { parseDateTime, toISODay, wallClockDate } from '../utils/calendar-utils.js';
|
|
12
12
|
const CALENDAR_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='16' height='16' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='4' width='18' height='18' rx='2' ry='2'></rect><line x1='16' y1='2' x2='16' y2='6'></line><line x1='8' y1='2' x2='8' y2='6'></line><line x1='3' y1='10' x2='21' y2='10'></line></svg>`;
|
|
13
13
|
const CLEAR_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='14' height='14' xmlns='http://www.w3.org/2000/svg'><line x1='18' y1='6' x2='6' y2='18'></line><line x1='6' y1='6' x2='18' y2='18'></line></svg>`;
|
|
14
14
|
const SCHEDULE_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='16' height='16' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='10'></circle><polyline points='12,6 12,12 16,14'></polyline></svg>`;
|
|
@@ -22,41 +22,6 @@ elementIds.set(element, id);
|
|
|
22
22
|
}
|
|
23
23
|
return id;
|
|
24
24
|
}
|
|
25
|
-
const LOCAL_VALUE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?$/;
|
|
26
|
-
function wallClockDate(c) {
|
|
27
|
-
const date = new Date(0);
|
|
28
|
-
date.setUTCFullYear(c.year, c.month - 1, c.day);
|
|
29
|
-
date.setUTCHours(c.hour || 0, c.minute || 0, c.second || 0);
|
|
30
|
-
return date;
|
|
31
|
-
}
|
|
32
|
-
function parseValue(value) {
|
|
33
|
-
if (value === null || value === undefined || value === '')
|
|
34
|
-
return null;
|
|
35
|
-
if (typeof value === 'string') {
|
|
36
|
-
const local = LOCAL_VALUE.exec(value);
|
|
37
|
-
if (local) {
|
|
38
|
-
const [year, month, day, hour, minute, second] = local.slice(1, 7).map((g) => Number(g ?? 0));
|
|
39
|
-
const components = { year, month, day, hour, minute, second };
|
|
40
|
-
const check = wallClockDate(components);
|
|
41
|
-
const real = check.getUTCMonth() === month - 1 && check.getUTCDate() === day &&
|
|
42
|
-
hour < 24 && minute < 60 && second < 60;
|
|
43
|
-
return real ? components : null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const dateObj = typeof value === 'number' || typeof value === 'string' ? new Date(value) : null;
|
|
47
|
-
if (!dateObj || isNaN(dateObj.getTime())) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
year: dateObj.getFullYear(),
|
|
52
|
-
month: dateObj.getMonth() + 1,
|
|
53
|
-
day: dateObj.getDate(),
|
|
54
|
-
hour: dateObj.getHours(),
|
|
55
|
-
minute: dateObj.getMinutes(),
|
|
56
|
-
second: dateObj.getSeconds(),
|
|
57
|
-
epoch: dateObj.getTime(),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
25
|
function componentsToDateObject(components) {
|
|
61
26
|
const { year, month, day, hour, minute, second } = components;
|
|
62
27
|
if (!year || !month || !day)
|
|
@@ -248,7 +213,6 @@ if (this._initialized)
|
|
|
248
213
|
this._state.open = false;
|
|
249
214
|
else
|
|
250
215
|
this.initializeState();
|
|
251
|
-
this.syncErrorFlavor();
|
|
252
216
|
this.render();
|
|
253
217
|
this.updateFormValue();
|
|
254
218
|
this._localeObserver = observeLocaleChanges(this, () => {
|
|
@@ -271,10 +235,6 @@ onPropertiesChanged(changes) {
|
|
|
271
235
|
for (const { name, newValue } of changes) {
|
|
272
236
|
switch (name) {
|
|
273
237
|
case 'flavor':
|
|
274
|
-
if (newValue !== 'danger') {
|
|
275
|
-
this.removeAttribute(ERROR_FLAVOR);
|
|
276
|
-
this.syncErrorFlavor();
|
|
277
|
-
}
|
|
278
238
|
this._syncCustomFlavor();
|
|
279
239
|
break;
|
|
280
240
|
case 'size':
|
|
@@ -287,9 +247,7 @@ break;
|
|
|
287
247
|
case 'noClear':
|
|
288
248
|
case 'format':
|
|
289
249
|
case 'locale':
|
|
290
|
-
break;
|
|
291
250
|
case 'error':
|
|
292
|
-
this.syncErrorFlavor();
|
|
293
251
|
break;
|
|
294
252
|
case 'value': {
|
|
295
253
|
if (!this._initialized || this._syncing)
|
|
@@ -297,7 +255,7 @@ break;
|
|
|
297
255
|
this._state = {
|
|
298
256
|
withTime: this._state.withTime,
|
|
299
257
|
open: this._state.open,
|
|
300
|
-
...
|
|
258
|
+
...parseDateTime(newValue),
|
|
301
259
|
};
|
|
302
260
|
this.syncFormValue();
|
|
303
261
|
break;
|
|
@@ -374,7 +332,7 @@ return formatDisplayValue(c, this.getProperty('format') || 'long', getEffectiveL
|
|
|
374
332
|
}
|
|
375
333
|
initializeState() {
|
|
376
334
|
this._state = {
|
|
377
|
-
...
|
|
335
|
+
...parseDateTime(this.getProperty('value')),
|
|
378
336
|
withTime: this.getProperty('with-time'),
|
|
379
337
|
open: false,
|
|
380
338
|
};
|
|
@@ -401,27 +359,6 @@ this._syncing = false;
|
|
|
401
359
|
}
|
|
402
360
|
}
|
|
403
361
|
}
|
|
404
|
-
syncErrorFlavor() {
|
|
405
|
-
if (!this._connected)
|
|
406
|
-
return;
|
|
407
|
-
const flavor = this.getProperty('flavor');
|
|
408
|
-
const replaced = this.getAttribute(ERROR_FLAVOR);
|
|
409
|
-
if (this.getProperty('error')) {
|
|
410
|
-
if (replaced === null && (flavor === 'default' || flavor === 'neutral')) {
|
|
411
|
-
this.setAttribute(ERROR_FLAVOR, this.getAttribute('flavor') ?? '');
|
|
412
|
-
this.setProperty('flavor', 'danger');
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
else if (replaced !== null) {
|
|
416
|
-
this.removeAttribute(ERROR_FLAVOR);
|
|
417
|
-
if (flavor !== 'danger')
|
|
418
|
-
return;
|
|
419
|
-
if (!replaced)
|
|
420
|
-
this.removeAttribute('flavor');
|
|
421
|
-
else
|
|
422
|
-
this.setProperty('flavor', replaced);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
362
|
handleTimeInputChange(hour, minute) {
|
|
426
363
|
if (!this.hasDate())
|
|
427
364
|
return;
|
|
@@ -476,15 +413,7 @@ set size(v) { this.setProperty('size', v); }
|
|
|
476
413
|
get placement() { return this.getProperty('placement'); }
|
|
477
414
|
set placement(v) { this.setProperty('placement', v); }
|
|
478
415
|
get flavor() { return this.getProperty('flavor'); }
|
|
479
|
-
set flavor(v) {
|
|
480
|
-
this.removeAttribute(ERROR_FLAVOR);
|
|
481
|
-
this.setProperty('flavor', v);
|
|
482
|
-
}
|
|
483
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
484
|
-
if (name === 'flavor' && newValue === 'danger' && oldValue === newValue)
|
|
485
|
-
this.removeAttribute(ERROR_FLAVOR);
|
|
486
|
-
super.attributeChangedCallback(name, oldValue, newValue);
|
|
487
|
-
}
|
|
416
|
+
set flavor(v) { this.setProperty('flavor', v); }
|
|
488
417
|
get label() { return this.getProperty('label'); }
|
|
489
418
|
set label(v) { this.setProperty('label', v); }
|
|
490
419
|
get placeholder() { return this.getProperty('placeholder'); }
|
|
@@ -514,7 +443,7 @@ set error(v) { this.setProperty('error', v); }
|
|
|
514
443
|
get value() {
|
|
515
444
|
if (this._initialized)
|
|
516
445
|
return this.getProperty('value') || null;
|
|
517
|
-
const parsed =
|
|
446
|
+
const parsed = parseDateTime(this.getProperty('value'));
|
|
518
447
|
return parsed && componentsToOutputValue(parsed, !!this.getProperty('with-time'), !!this.getProperty('native'));
|
|
519
448
|
}
|
|
520
449
|
set value(val) {
|
|
@@ -658,7 +587,7 @@ const contentWrapper = document.createElement('div');
|
|
|
658
587
|
contentWrapper.className = 'calendar-content';
|
|
659
588
|
const calendar = document.createElement('ty-calendar');
|
|
660
589
|
if (this._state.year && this._state.month && this._state.day) {
|
|
661
|
-
calendar.setAttribute('year', this._state.year.
|
|
590
|
+
calendar.setAttribute('year', String(this._state.year).padStart(4, '0'));
|
|
662
591
|
calendar.setAttribute('month', this._state.month.toString());
|
|
663
592
|
calendar.setAttribute('day', this._state.day.toString());
|
|
664
593
|
}
|
|
@@ -682,14 +611,15 @@ contentWrapper.appendChild(this.renderTimeSection());
|
|
|
682
611
|
}
|
|
683
612
|
dialog.appendChild(contentWrapper);
|
|
684
613
|
new MutationObserver(() => {
|
|
685
|
-
if (this._state.open && !dialog.open && this.isConnected)
|
|
614
|
+
if (this._state.open && !dialog.open && dialog.isConnected && this.isConnected)
|
|
686
615
|
this.closeDropdown();
|
|
687
616
|
}).observe(dialog, { attributes: true, attributeFilter: ['open'] });
|
|
688
617
|
return dialog;
|
|
689
618
|
}
|
|
690
619
|
calendarFlavor() {
|
|
691
|
-
|
|
692
|
-
|
|
620
|
+
if (this.getProperty('error'))
|
|
621
|
+
return 'danger';
|
|
622
|
+
const flavor = this.getProperty('flavor');
|
|
693
623
|
return flavor && flavor !== 'default' ? flavor : null;
|
|
694
624
|
}
|
|
695
625
|
renderNativeInput() {
|
|
@@ -715,7 +645,7 @@ if (!input.value) {
|
|
|
715
645
|
this.clearValue();
|
|
716
646
|
}
|
|
717
647
|
else {
|
|
718
|
-
const parsed =
|
|
648
|
+
const parsed = parseDateTime(input.value);
|
|
719
649
|
if (parsed) {
|
|
720
650
|
const { hour, minute, second } = this._state.withTime ? parsed : this._state;
|
|
721
651
|
const components = { ...parsed, hour, minute, second, epoch: undefined };
|
|
@@ -754,8 +684,8 @@ const withTime = this._state.withTime;
|
|
|
754
684
|
input.type = withTime ? 'datetime-local' : 'date';
|
|
755
685
|
input.disabled = !!this.getProperty('disabled');
|
|
756
686
|
input.required = !!this.getProperty('required');
|
|
757
|
-
const min = this.getProperty('min');
|
|
758
|
-
const max = this.getProperty('max');
|
|
687
|
+
const min = toISODay(this.getProperty('min'));
|
|
688
|
+
const max = toISODay(this.getProperty('max'));
|
|
759
689
|
if (min)
|
|
760
690
|
input.min = withTime ? `${min}T00:00` : min;
|
|
761
691
|
else
|
|
@@ -910,7 +840,7 @@ return;
|
|
|
910
840
|
}
|
|
911
841
|
const calendar = this.shadowRoot.querySelector('ty-calendar');
|
|
912
842
|
if (calendar && hasValue) {
|
|
913
|
-
calendar.setAttribute('year', this._state.year.
|
|
843
|
+
calendar.setAttribute('year', String(this._state.year).padStart(4, '0'));
|
|
914
844
|
calendar.setAttribute('month', this._state.month.toString());
|
|
915
845
|
calendar.setAttribute('day', this._state.day.toString());
|
|
916
846
|
}
|
|
@@ -2,6 +2,7 @@ import { TyComponent } from "../base/ty-component.js";
|
|
|
2
2
|
import { ensureStyles } from "../utils/styles.js";
|
|
3
3
|
import { fileUploadStyles } from "../styles/file-upload.js";
|
|
4
4
|
import { nativeMessage } from "../utils/validity.js";
|
|
5
|
+
const DEFAULT_PLACEHOLDER = "Drop files here or click to browse";
|
|
5
6
|
const UPLOAD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`;
|
|
6
7
|
const FILE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
|
|
7
8
|
const X_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
@@ -381,9 +382,12 @@ const hintEl = container.querySelector(".upload-hint");
|
|
|
381
382
|
if (this._files.length > 0) {
|
|
382
383
|
hintEl.innerHTML = `<strong>${this._files.length} file${this._files.length !== 1 ? "s" : ""} selected</strong>`;
|
|
383
384
|
}
|
|
384
|
-
else {
|
|
385
|
+
else if (!this.placeholder || this.placeholder === DEFAULT_PLACEHOLDER) {
|
|
385
386
|
hintEl.innerHTML = `Drop files here or <span class="browse-link">click to browse</span>`;
|
|
386
387
|
}
|
|
388
|
+
else {
|
|
389
|
+
hintEl.textContent = this.placeholder;
|
|
390
|
+
}
|
|
387
391
|
const subHintEl = container.querySelector(".upload-sub-hint");
|
|
388
392
|
if (this.accept && this._files.length === 0) {
|
|
389
393
|
subHintEl.textContent = `Accepted: ${this.accept}`;
|
|
@@ -434,7 +438,7 @@ default: "",
|
|
|
434
438
|
placeholder: {
|
|
435
439
|
type: "string",
|
|
436
440
|
visual: true,
|
|
437
|
-
default:
|
|
441
|
+
default: DEFAULT_PLACEHOLDER,
|
|
438
442
|
},
|
|
439
443
|
error: {
|
|
440
444
|
type: "string",
|
|
@@ -151,14 +151,6 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
|
|
|
151
151
|
protected onConnect(): void;
|
|
152
152
|
/** Custom (non-built-in) flavors — see utils/flavor-sheet.ts. */
|
|
153
153
|
private _syncCustomFlavor;
|
|
154
|
-
/**
|
|
155
|
-
* `error` turns a neutral field danger; clearing it (the author, or
|
|
156
|
-
* validation dropping the message it showed) puts back exactly the flavor
|
|
157
|
-
* attribute the author had, or none, kept meanwhile in ERROR_FLAVOR (same
|
|
158
|
-
* as ty-select and ty-date-picker). Runs once connected: while the element
|
|
159
|
-
* upgrades, `error` may come before the `flavor` written next to it.
|
|
160
|
-
*/
|
|
161
|
-
private syncErrorFlavor;
|
|
162
154
|
protected onDisconnect(): void;
|
|
163
155
|
/** Handle property changes — called BEFORE render. */
|
|
164
156
|
protected onPropertiesChanged(changes: PropertyChange[]): void;
|
|
@@ -209,7 +201,6 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
|
|
|
209
201
|
set size(v: Size);
|
|
210
202
|
get flavor(): Flavor;
|
|
211
203
|
set flavor(v: Flavor);
|
|
212
|
-
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
213
204
|
get currency(): string;
|
|
214
205
|
set currency(v: string);
|
|
215
206
|
get locale(): string;
|
package/lib/components/input.js
CHANGED
|
@@ -2,7 +2,6 @@ import { DEFAULT_SIZE } from '../types/common.js';
|
|
|
2
2
|
import { TyComponent } from '../base/ty-component.js';
|
|
3
3
|
import { ensureStyles } from '../utils/styles.js';
|
|
4
4
|
import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
|
|
5
|
-
import { ERROR_FLAVOR } from '../utils/error-flavor.js';
|
|
6
5
|
import { inputStyles, inputCustomFlavorCss } from '../styles/input.js';
|
|
7
6
|
import { formatNumber, parseNumericValue, shouldFormat as shouldFormatType } from '../utils/number-format.js';
|
|
8
7
|
import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
|
|
@@ -60,32 +59,14 @@ this._localeObserver = observeLocaleChanges(this, () => {
|
|
|
60
59
|
this.render();
|
|
61
60
|
});
|
|
62
61
|
this._syncCustomFlavor();
|
|
63
|
-
this.
|
|
62
|
+
if (this._rendered) {
|
|
63
|
+
this.render();
|
|
64
|
+
this.setupEventListeners();
|
|
65
|
+
}
|
|
64
66
|
}
|
|
65
67
|
_syncCustomFlavor() {
|
|
66
68
|
this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.flavor, ({ base }) => inputCustomFlavorCss(base));
|
|
67
69
|
}
|
|
68
|
-
syncErrorFlavor() {
|
|
69
|
-
if (!this._connected)
|
|
70
|
-
return;
|
|
71
|
-
const flavor = this.getProperty('flavor');
|
|
72
|
-
const replaced = this.getAttribute(ERROR_FLAVOR);
|
|
73
|
-
if (this.getProperty('error')) {
|
|
74
|
-
if (replaced === null && flavor === 'neutral') {
|
|
75
|
-
this.setAttribute(ERROR_FLAVOR, this.getAttribute('flavor') ?? '');
|
|
76
|
-
this.setProperty('flavor', 'danger');
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else if (replaced !== null) {
|
|
80
|
-
this.removeAttribute(ERROR_FLAVOR);
|
|
81
|
-
if (flavor !== 'danger')
|
|
82
|
-
return;
|
|
83
|
-
if (!replaced)
|
|
84
|
-
this.removeAttribute('flavor');
|
|
85
|
-
else
|
|
86
|
-
this.setProperty('flavor', replaced);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
70
|
onDisconnect() {
|
|
90
71
|
this.removeEventListeners();
|
|
91
72
|
if (this._localeObserver) {
|
|
@@ -105,10 +86,6 @@ onPropertiesChanged(changes) {
|
|
|
105
86
|
for (const { name, newValue } of changes) {
|
|
106
87
|
switch (name) {
|
|
107
88
|
case 'flavor':
|
|
108
|
-
if (newValue !== 'danger') {
|
|
109
|
-
this.removeAttribute(ERROR_FLAVOR);
|
|
110
|
-
this.syncErrorFlavor();
|
|
111
|
-
}
|
|
112
89
|
this._syncCustomFlavor();
|
|
113
90
|
break;
|
|
114
91
|
case 'value':
|
|
@@ -118,9 +95,6 @@ case 'type':
|
|
|
118
95
|
const currentValue = this.getProperty('value') || '';
|
|
119
96
|
this._shadowValue = this.parseShadowValue(currentValue);
|
|
120
97
|
break;
|
|
121
|
-
case 'error':
|
|
122
|
-
this.syncErrorFlavor();
|
|
123
|
-
break;
|
|
124
98
|
}
|
|
125
99
|
}
|
|
126
100
|
}
|
|
@@ -226,15 +200,7 @@ set error(v) { this.setProperty('error', v); }
|
|
|
226
200
|
get size() { return this.getProperty('size'); }
|
|
227
201
|
set size(v) { this.setProperty('size', v); }
|
|
228
202
|
get flavor() { return this.getProperty('flavor'); }
|
|
229
|
-
set flavor(v) {
|
|
230
|
-
this.removeAttribute(ERROR_FLAVOR);
|
|
231
|
-
this.setProperty('flavor', v);
|
|
232
|
-
}
|
|
233
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
234
|
-
if (name === 'flavor' && newValue === 'danger' && oldValue === newValue)
|
|
235
|
-
this.removeAttribute(ERROR_FLAVOR);
|
|
236
|
-
super.attributeChangedCallback(name, oldValue, newValue);
|
|
237
|
-
}
|
|
203
|
+
set flavor(v) { this.setProperty('flavor', v); }
|
|
238
204
|
get currency() { return this.getProperty('currency'); }
|
|
239
205
|
set currency(v) { this.setProperty('currency', v); }
|
|
240
206
|
get locale() {
|
|
@@ -416,6 +382,9 @@ const existingWrapper = shadow.querySelector('.input-wrapper');
|
|
|
416
382
|
const existingLabel = shadow.querySelector('.ty-field-label');
|
|
417
383
|
const existingError = shadow.querySelector('.error-message');
|
|
418
384
|
const classes = this.buildClassList();
|
|
385
|
+
const focused = !!existingInput && shadow.activeElement === existingInput;
|
|
386
|
+
if (!focused)
|
|
387
|
+
this._isFocused = false;
|
|
419
388
|
const inputType = ['password', 'email', 'tel', 'url'].includes(this.type)
|
|
420
389
|
? this.type
|
|
421
390
|
: 'text';
|
|
@@ -426,7 +395,9 @@ const inputMode = shouldFormatType(this.type) ? 'decimal'
|
|
|
426
395
|
: undefined;
|
|
427
396
|
const displayValue = this.getDisplayValue();
|
|
428
397
|
if (existingInput && existingWrapper) {
|
|
429
|
-
|
|
398
|
+
const revealed = inputType === 'password' &&
|
|
399
|
+
shadow.querySelector('.password-toggle')?.getAttribute('aria-pressed') === 'true';
|
|
400
|
+
existingInput.type = revealed ? 'text' : inputType;
|
|
430
401
|
if (existingInput.value !== displayValue)
|
|
431
402
|
existingInput.value = displayValue;
|
|
432
403
|
existingInput.placeholder = this.placeholder;
|
|
@@ -436,6 +407,7 @@ existingInput.inputMode = inputMode;
|
|
|
436
407
|
else
|
|
437
408
|
existingInput.removeAttribute('inputmode');
|
|
438
409
|
existingWrapper.className = `input-wrapper ${classes}`;
|
|
410
|
+
existingWrapper.classList.toggle('focused', focused);
|
|
439
411
|
existingInput.disabled = this.disabled;
|
|
440
412
|
if (this.disabled) {
|
|
441
413
|
existingInput.setAttribute('disabled', '');
|
|
@@ -541,7 +513,12 @@ return;
|
|
|
541
513
|
toggle.addEventListener('mousedown', (e) => e.preventDefault());
|
|
542
514
|
toggle.addEventListener('click', () => {
|
|
543
515
|
const reveal = inputEl.type === 'password';
|
|
516
|
+
const { selectionStart, selectionEnd, selectionDirection } = inputEl;
|
|
544
517
|
inputEl.type = reveal ? 'text' : 'password';
|
|
518
|
+
if (selectionStart !== null && shadow.activeElement === inputEl) {
|
|
519
|
+
inputEl.getBoundingClientRect();
|
|
520
|
+
inputEl.setSelectionRange(selectionStart, selectionEnd, selectionDirection ?? undefined);
|
|
521
|
+
}
|
|
545
522
|
toggle.innerHTML = reveal ? EYE_OFF_ICON_SVG : EYE_ICON_SVG;
|
|
546
523
|
toggle.setAttribute('aria-label', reveal ? 'Hide password' : 'Show password');
|
|
547
524
|
toggle.setAttribute('aria-pressed', String(reveal));
|
package/lib/components/modal.js
CHANGED
|
@@ -10,6 +10,13 @@ const hoverEnterHandlers = new WeakMap();
|
|
|
10
10
|
const hoverLeaveHandlers = new WeakMap();
|
|
11
11
|
const modalIds = new WeakMap();
|
|
12
12
|
const pendingCloseReasons = new WeakMap();
|
|
13
|
+
const detachedOpen = new WeakSet();
|
|
14
|
+
const openStack = [];
|
|
15
|
+
function dropFromStack(el) {
|
|
16
|
+
const i = openStack.indexOf(el);
|
|
17
|
+
if (i >= 0)
|
|
18
|
+
openStack.splice(i, 1);
|
|
19
|
+
}
|
|
13
20
|
const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`;
|
|
14
21
|
function parseBoolAttr(el, name) {
|
|
15
22
|
if (!el.hasAttribute(name)) {
|
|
@@ -108,25 +115,36 @@ return true;
|
|
|
108
115
|
}
|
|
109
116
|
return false;
|
|
110
117
|
}
|
|
111
|
-
function closeNestedDialogs(root
|
|
112
|
-
|
|
113
|
-
if (seen.has(node))
|
|
114
|
-
return;
|
|
115
|
-
seen.add(node);
|
|
118
|
+
function closeNestedDialogs(root) {
|
|
119
|
+
walkNested(root, (node) => {
|
|
116
120
|
if (node instanceof TyModal && node.open)
|
|
117
121
|
node.removeAttribute('open');
|
|
118
122
|
if (node instanceof HTMLDialogElement && node.matches(':modal'))
|
|
119
123
|
node.close();
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
return false;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function hasOpenNestedDialog(root) {
|
|
128
|
+
return walkNested(root, (node) => node instanceof HTMLDialogElement && node.matches(':modal'));
|
|
129
|
+
}
|
|
130
|
+
function walkNested(root, visit, seen = new WeakSet()) {
|
|
131
|
+
const walk = (node) => {
|
|
132
|
+
if (seen.has(node))
|
|
133
|
+
return false;
|
|
134
|
+
seen.add(node);
|
|
135
|
+
if (visit(node))
|
|
136
|
+
return true;
|
|
137
|
+
if (node.shadowRoot && walkNested(node.shadowRoot, visit, seen))
|
|
138
|
+
return true;
|
|
122
139
|
if (node instanceof HTMLSlotElement) {
|
|
123
140
|
for (const assigned of node.assignedElements({ flatten: true })) {
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
if (walk(assigned) || walkNested(assigned, visit, seen))
|
|
142
|
+
return true;
|
|
126
143
|
}
|
|
127
144
|
}
|
|
145
|
+
return false;
|
|
128
146
|
};
|
|
129
|
-
root.querySelectorAll('*').
|
|
147
|
+
return Array.from(root.querySelectorAll('*')).some(walk);
|
|
130
148
|
}
|
|
131
149
|
function handleEscapeKey(el, event) {
|
|
132
150
|
const dialog = event.currentTarget;
|
|
@@ -138,6 +156,22 @@ event.preventDefault();
|
|
|
138
156
|
closeModal(el, 'escape');
|
|
139
157
|
}
|
|
140
158
|
}
|
|
159
|
+
function handleDocumentEscape(event) {
|
|
160
|
+
if (event.key !== 'Escape' || event.defaultPrevented)
|
|
161
|
+
return;
|
|
162
|
+
const top = openStack[openStack.length - 1];
|
|
163
|
+
if (!top)
|
|
164
|
+
return;
|
|
165
|
+
if (event.composedPath().some((n) => n instanceof HTMLDialogElement && n.open))
|
|
166
|
+
return;
|
|
167
|
+
if (hasOpenNestedDialog(top))
|
|
168
|
+
return;
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
if (getModalAttributes(top).closeOnEscape)
|
|
171
|
+
closeModal(top, 'escape');
|
|
172
|
+
}
|
|
173
|
+
if (typeof document !== 'undefined')
|
|
174
|
+
document.addEventListener('keydown', handleDocumentEscape);
|
|
141
175
|
function handleCloseButtonClick(el, event) {
|
|
142
176
|
event.preventDefault();
|
|
143
177
|
event.stopPropagation();
|
|
@@ -273,17 +307,17 @@ setupCloseButtonHover(el, dialog);
|
|
|
273
307
|
if (attributes.open) {
|
|
274
308
|
if (!dialog.open) {
|
|
275
309
|
lockScroll(modalId);
|
|
276
|
-
if (attributes.backdrop) {
|
|
277
310
|
dialog.showModal();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
dialog.show();
|
|
281
|
-
}
|
|
311
|
+
dropFromStack(el);
|
|
312
|
+
openStack.push(el);
|
|
282
313
|
syncBackdropZoom();
|
|
314
|
+
if (!detachedOpen.delete(el))
|
|
283
315
|
dispatchModalEvent(el, 'open', {});
|
|
284
316
|
}
|
|
285
317
|
}
|
|
286
318
|
else {
|
|
319
|
+
detachedOpen.delete(el);
|
|
320
|
+
dropFromStack(el);
|
|
287
321
|
if (dialog.open) {
|
|
288
322
|
unlockScroll(modalId);
|
|
289
323
|
closeNestedDialogs(el);
|
|
@@ -296,9 +330,10 @@ dispatchModalEvent(el, 'close', { reason });
|
|
|
296
330
|
}
|
|
297
331
|
syncBackdropZoom();
|
|
298
332
|
dialog.onclose = (event) => {
|
|
299
|
-
if (event.target !== dialog)
|
|
333
|
+
if (event.target !== dialog || dialog.open || !el.isConnected)
|
|
300
334
|
return;
|
|
301
335
|
unlockScroll(modalId);
|
|
336
|
+
dropFromStack(el);
|
|
302
337
|
closeNestedDialogs(el);
|
|
303
338
|
if (el.hasAttribute('open')) {
|
|
304
339
|
el.removeAttribute('open');
|
|
@@ -311,13 +346,15 @@ syncBackdropZoom();
|
|
|
311
346
|
};
|
|
312
347
|
}
|
|
313
348
|
function syncBackdropZoom() {
|
|
314
|
-
const
|
|
315
|
-
|
|
349
|
+
const zoom = Array.from(document.querySelectorAll('ty-modal[open], ty-dialog[open]'))
|
|
350
|
+
.some((m) => parseBoolAttr(m, 'open') && !parseBoolAttr(m, 'no-backdrop') && !parseBoolAttr(m, 'no-zoom'));
|
|
351
|
+
document.documentElement.classList.toggle('ty-modal-zoom', zoom);
|
|
316
352
|
}
|
|
317
353
|
function cleanup(el) {
|
|
318
354
|
const modalId = modalIds.get(el);
|
|
319
355
|
const shadowRoot = el.shadowRoot;
|
|
320
356
|
const dialog = shadowRoot ? getModalDialog(shadowRoot) : null;
|
|
357
|
+
dropFromStack(el);
|
|
321
358
|
if (modalId) {
|
|
322
359
|
unlockScroll(modalId);
|
|
323
360
|
modalIds.delete(el);
|
|
@@ -325,6 +362,10 @@ modalIds.delete(el);
|
|
|
325
362
|
syncBackdropZoom();
|
|
326
363
|
if (!dialog)
|
|
327
364
|
return;
|
|
365
|
+
if (dialog.open) {
|
|
366
|
+
dialog.close();
|
|
367
|
+
detachedOpen.add(el);
|
|
368
|
+
}
|
|
328
369
|
const backdropHandler = backdropClickHandlers.get(el);
|
|
329
370
|
if (backdropHandler) {
|
|
330
371
|
dialog.removeEventListener('click', backdropHandler);
|
package/lib/components/popup.js
CHANGED
|
@@ -9,6 +9,8 @@ const closeTimers = new WeakMap();
|
|
|
9
9
|
const escapeHandlers = new WeakMap();
|
|
10
10
|
const closeRequestHandlers = new WeakMap();
|
|
11
11
|
const popupIds = new WeakMap();
|
|
12
|
+
const anchors = new WeakMap();
|
|
13
|
+
const escapeGuards = new WeakMap();
|
|
12
14
|
const pointerPresses = new WeakSet();
|
|
13
15
|
function parseBoolAttr(el, name) {
|
|
14
16
|
return el.hasAttribute(name);
|
|
@@ -88,7 +90,7 @@ dialog.close();
|
|
|
88
90
|
function openPopup(el) {
|
|
89
91
|
const shadowRoot = el.shadowRoot;
|
|
90
92
|
const dialog = shadowRoot ? getPopupDialog(shadowRoot) : null;
|
|
91
|
-
if (!dialog)
|
|
93
|
+
if (!dialog || !el.isConnected)
|
|
92
94
|
return;
|
|
93
95
|
const anchor = getAnchorElement(el);
|
|
94
96
|
if (dialog.open) {
|
|
@@ -105,14 +107,28 @@ return;
|
|
|
105
107
|
const popupId = getPopupId(el);
|
|
106
108
|
lockScroll(popupId);
|
|
107
109
|
dialog.showModal();
|
|
110
|
+
guardEscape(el);
|
|
108
111
|
if (anchor?.hasAttribute('aria-haspopup'))
|
|
109
112
|
anchor.setAttribute('aria-expanded', 'true');
|
|
110
113
|
requestAnimationFrame(() => {
|
|
111
114
|
updatePosition(el);
|
|
112
115
|
dialog.classList.add('preparing-animation');
|
|
113
|
-
|
|
116
|
+
dialog.getBoundingClientRect();
|
|
114
117
|
requestAnimationFrame(() => {
|
|
115
118
|
dialog.classList.add('open');
|
|
119
|
+
if (!el.matches(':focus-within')) {
|
|
120
|
+
const target = el.querySelector('[autofocus]');
|
|
121
|
+
target?.focus({ preventScroll: true });
|
|
122
|
+
if (!el.matches(':focus-within')) {
|
|
123
|
+
dialog.focus({ preventScroll: true });
|
|
124
|
+
if (target) {
|
|
125
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
126
|
+
if (dialog.open && shadowRoot.activeElement === dialog)
|
|
127
|
+
target.focus({ preventScroll: true });
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
116
132
|
el.dispatchEvent(new CustomEvent('open'));
|
|
117
133
|
});
|
|
118
134
|
});
|
|
@@ -153,6 +169,23 @@ return true;
|
|
|
153
169
|
}
|
|
154
170
|
return false;
|
|
155
171
|
}
|
|
172
|
+
function guardEscape(el) {
|
|
173
|
+
if (escapeGuards.has(el))
|
|
174
|
+
return;
|
|
175
|
+
const guard = (e) => {
|
|
176
|
+
if (e.key === 'Escape' && getPopupAttributes(el).disableClose && !fromNestedDialog(el, e))
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
};
|
|
179
|
+
escapeGuards.set(el, guard);
|
|
180
|
+
document.addEventListener('keydown', guard, true);
|
|
181
|
+
}
|
|
182
|
+
function unguardEscape(el) {
|
|
183
|
+
const guard = escapeGuards.get(el);
|
|
184
|
+
if (!guard)
|
|
185
|
+
return;
|
|
186
|
+
document.removeEventListener('keydown', guard, true);
|
|
187
|
+
escapeGuards.delete(el);
|
|
188
|
+
}
|
|
156
189
|
function handleEscape(el, event) {
|
|
157
190
|
if (fromNestedDialog(el, event))
|
|
158
191
|
return;
|
|
@@ -179,14 +212,16 @@ return;
|
|
|
179
212
|
anchor.setAttribute('aria-haspopup', 'dialog');
|
|
180
213
|
if (!anchor.hasAttribute('aria-expanded'))
|
|
181
214
|
anchor.setAttribute('aria-expanded', 'false');
|
|
215
|
+
const previous = anchors.get(el) ?? anchor;
|
|
182
216
|
const existingHandler = anchorClickHandlers.get(el);
|
|
183
217
|
if (existingHandler) {
|
|
184
|
-
|
|
218
|
+
previous.removeEventListener('pointerdown', existingHandler);
|
|
185
219
|
}
|
|
186
220
|
const existingKeyHandler = anchorKeyClickHandlers.get(el);
|
|
187
221
|
if (existingKeyHandler) {
|
|
188
|
-
|
|
222
|
+
previous.removeEventListener('click', existingKeyHandler);
|
|
189
223
|
}
|
|
224
|
+
anchors.set(el, anchor);
|
|
190
225
|
const handler = (e) => {
|
|
191
226
|
pointerPresses.add(el);
|
|
192
227
|
handleAnchorClick(el, e);
|
|
@@ -208,7 +243,8 @@ anchorKeyClickHandlers.set(el, keyHandler);
|
|
|
208
243
|
anchor.addEventListener('click', keyHandler);
|
|
209
244
|
}
|
|
210
245
|
function cleanupAnchorEvents(el) {
|
|
211
|
-
const anchor =
|
|
246
|
+
const anchor = anchors.get(el);
|
|
247
|
+
anchors.delete(el);
|
|
212
248
|
const handler = anchorClickHandlers.get(el);
|
|
213
249
|
if (anchor && handler) {
|
|
214
250
|
anchor.removeEventListener('pointerdown', handler);
|
|
@@ -233,6 +269,7 @@ const dialog = document.createElement('dialog');
|
|
|
233
269
|
const container = document.createElement('div');
|
|
234
270
|
const content = document.createElement('slot');
|
|
235
271
|
dialog.className = 'popup-dialog';
|
|
272
|
+
dialog.tabIndex = -1;
|
|
236
273
|
container.className = 'popup-container';
|
|
237
274
|
content.id = 'popup-content';
|
|
238
275
|
shadowRoot.appendChild(dialog);
|
|
@@ -246,12 +283,23 @@ dialog.addEventListener('pointerdown', outsideHandler);
|
|
|
246
283
|
const escapeHandler = (e) => handleEscape(el, e);
|
|
247
284
|
escapeHandlers.set(el, escapeHandler);
|
|
248
285
|
el.addEventListener('keydown', escapeHandler);
|
|
286
|
+
const inertKeys = (e) => {
|
|
287
|
+
if ((e.key === 'Enter' || e.key === ' ') && e.composedPath()[0] === dialog)
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
};
|
|
290
|
+
for (const type of ['keydown', 'keypress', 'keyup'])
|
|
291
|
+
dialog.addEventListener(type, inertKeys);
|
|
292
|
+
dialog.addEventListener('cancel', (e) => {
|
|
293
|
+
if (getPopupAttributes(el).disableClose)
|
|
294
|
+
e.preventDefault();
|
|
295
|
+
});
|
|
249
296
|
new MutationObserver(() => {
|
|
250
297
|
if (dialog.open)
|
|
251
298
|
return;
|
|
252
299
|
clearTimeout(closeTimers.get(el));
|
|
253
300
|
closeTimers.delete(el);
|
|
254
301
|
pointerPresses.delete(el);
|
|
302
|
+
unguardEscape(el);
|
|
255
303
|
unlockScroll(getPopupId(el));
|
|
256
304
|
dialog.classList.remove('open');
|
|
257
305
|
dialog.classList.remove('preparing-animation');
|
|
@@ -289,12 +337,15 @@ if (outsideHandler) {
|
|
|
289
337
|
dialog.removeEventListener('pointerdown', outsideHandler);
|
|
290
338
|
outsideClickHandlers.delete(el);
|
|
291
339
|
}
|
|
340
|
+
if (dialog.open)
|
|
341
|
+
dialog.close();
|
|
292
342
|
}
|
|
293
343
|
const escapeHandler = escapeHandlers.get(el);
|
|
294
344
|
if (escapeHandler) {
|
|
295
345
|
el.removeEventListener('keydown', escapeHandler);
|
|
296
346
|
escapeHandlers.delete(el);
|
|
297
347
|
}
|
|
348
|
+
unguardEscape(el);
|
|
298
349
|
const closeRequestHandler = closeRequestHandlers.get(el);
|
|
299
350
|
if (closeRequestHandler) {
|
|
300
351
|
el.removeEventListener('ty:close-popup', closeRequestHandler);
|