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
package/lib/utils/scroll-lock.js
CHANGED
|
@@ -3,6 +3,7 @@ locked: false,
|
|
|
3
3
|
scrollY: 0,
|
|
4
4
|
activeLocks: new Set(),
|
|
5
5
|
};
|
|
6
|
+
let savedStyle = { overflow: '', paddingRight: '' };
|
|
6
7
|
function getBodyEl() {
|
|
7
8
|
return document.body;
|
|
8
9
|
}
|
|
@@ -15,6 +16,102 @@ return window.scrollY || getDocEl().scrollTop || 0;
|
|
|
15
16
|
function getScrollbarWidth() {
|
|
16
17
|
return window.innerWidth - getDocEl().clientWidth;
|
|
17
18
|
}
|
|
19
|
+
const KEY_SCROLL = {
|
|
20
|
+
ArrowDown: ['y', 1], ArrowUp: ['y', -1], PageDown: ['y', 1], PageUp: ['y', -1],
|
|
21
|
+
End: ['y', 1], Home: ['y', -1], ' ': ['y', 1], ArrowRight: ['x', 1], ArrowLeft: ['x', -1],
|
|
22
|
+
};
|
|
23
|
+
const TEXT_INPUT = /^(?:text|search|email|url|tel|password|number|date|datetime-local|month|week|time|range|color)$/;
|
|
24
|
+
function keyBelongsTo(el, key) {
|
|
25
|
+
if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)
|
|
26
|
+
return true;
|
|
27
|
+
if (el instanceof HTMLElement && el.isContentEditable)
|
|
28
|
+
return true;
|
|
29
|
+
if (el instanceof HTMLInputElement) {
|
|
30
|
+
if (TEXT_INPUT.test(el.type))
|
|
31
|
+
return true;
|
|
32
|
+
if (el.type === 'radio' && key.startsWith('Arrow'))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (key === ' ' && el.matches('button, input, summary, a[href], [role]'))
|
|
36
|
+
return true;
|
|
37
|
+
if (key !== ' ' && el.matches('[role]:not([role=dialog]):not([role=document]):not([role=presentation]):not([role=none])'))
|
|
38
|
+
return true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function canScroll(el, axis, dir) {
|
|
42
|
+
const style = getComputedStyle(el);
|
|
43
|
+
const overflow = axis === 'y' ? style.overflowY : style.overflowX;
|
|
44
|
+
if (!/^(?:auto|scroll|overlay)$/.test(overflow) && !(el instanceof HTMLTextAreaElement))
|
|
45
|
+
return false;
|
|
46
|
+
const pos = axis === 'y' ? el.scrollTop : Math.abs(el.scrollLeft);
|
|
47
|
+
const max = axis === 'y' ? el.scrollHeight - el.clientHeight : el.scrollWidth - el.clientWidth;
|
|
48
|
+
if (max < 1)
|
|
49
|
+
return false;
|
|
50
|
+
return dir > 0 ? pos < max - 1 : pos > 0;
|
|
51
|
+
}
|
|
52
|
+
const HAS_POPOVER = typeof HTMLElement !== 'undefined' && 'popover' in HTMLElement.prototype;
|
|
53
|
+
function isOpenPopover(el) {
|
|
54
|
+
return HAS_POPOVER && el.hasAttribute('popover') && el.matches(':popover-open');
|
|
55
|
+
}
|
|
56
|
+
function scrollsInside(event, axis, dir) {
|
|
57
|
+
if (!dir)
|
|
58
|
+
return true;
|
|
59
|
+
const path = event.composedPath().filter((n) => n instanceof Element);
|
|
60
|
+
const root = path.findIndex((n) => (n instanceof HTMLDialogElement && n.open) || isOpenPopover(n));
|
|
61
|
+
return root >= 0 && path.slice(0, root + 1).some((n) => canScroll(n, axis, dir));
|
|
62
|
+
}
|
|
63
|
+
function guardWheel(e) {
|
|
64
|
+
if (e.ctrlKey)
|
|
65
|
+
return;
|
|
66
|
+
const axis = Math.abs(e.deltaX) > Math.abs(e.deltaY) || e.shiftKey ? 'x' : 'y';
|
|
67
|
+
const delta = axis === 'y' && !e.shiftKey ? e.deltaY : e.deltaX || e.deltaY;
|
|
68
|
+
if (e.cancelable && !scrollsInside(e, axis, Math.sign(delta)))
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
}
|
|
71
|
+
let touchX = 0;
|
|
72
|
+
let touchY = 0;
|
|
73
|
+
function guardTouchStart(e) {
|
|
74
|
+
if (e.touches.length !== 1)
|
|
75
|
+
return;
|
|
76
|
+
touchX = e.touches[0].clientX;
|
|
77
|
+
touchY = e.touches[0].clientY;
|
|
78
|
+
}
|
|
79
|
+
function guardTouchMove(e) {
|
|
80
|
+
if (e.touches.length !== 1 || !e.cancelable)
|
|
81
|
+
return;
|
|
82
|
+
const { clientX, clientY } = e.touches[0];
|
|
83
|
+
const dx = touchX - clientX;
|
|
84
|
+
const dy = touchY - clientY;
|
|
85
|
+
touchX = clientX;
|
|
86
|
+
touchY = clientY;
|
|
87
|
+
const axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
|
|
88
|
+
if (!scrollsInside(e, axis, Math.sign(axis === 'y' ? dy : dx)))
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
}
|
|
91
|
+
function guardKey(e) {
|
|
92
|
+
const scroll = KEY_SCROLL[e.key];
|
|
93
|
+
if (!scroll || e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey)
|
|
94
|
+
return;
|
|
95
|
+
const target = e.composedPath()[0];
|
|
96
|
+
if (target instanceof Element && keyBelongsTo(target, e.key))
|
|
97
|
+
return;
|
|
98
|
+
const [axis, dir] = scroll;
|
|
99
|
+
if (!scrollsInside(e, axis, e.key === ' ' && e.shiftKey ? -dir : dir))
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
}
|
|
102
|
+
const WHEEL_OPTS = { capture: true, passive: false };
|
|
103
|
+
function addGuard() {
|
|
104
|
+
window.addEventListener('wheel', guardWheel, WHEEL_OPTS);
|
|
105
|
+
window.addEventListener('touchstart', guardTouchStart, { capture: true, passive: true });
|
|
106
|
+
window.addEventListener('touchmove', guardTouchMove, WHEEL_OPTS);
|
|
107
|
+
window.addEventListener('keydown', guardKey, true);
|
|
108
|
+
}
|
|
109
|
+
function removeGuard() {
|
|
110
|
+
window.removeEventListener('wheel', guardWheel, WHEEL_OPTS);
|
|
111
|
+
window.removeEventListener('touchstart', guardTouchStart, { capture: true });
|
|
112
|
+
window.removeEventListener('touchmove', guardTouchMove, WHEEL_OPTS);
|
|
113
|
+
window.removeEventListener('keydown', guardKey, true);
|
|
114
|
+
}
|
|
18
115
|
function lockBodyFixed() {
|
|
19
116
|
if (lockState.locked)
|
|
20
117
|
return;
|
|
@@ -25,11 +122,13 @@ console.log('🔒 Locking body scroll:', { scrollbarWidth });
|
|
|
25
122
|
}
|
|
26
123
|
lockState.scrollY = getScrollY();
|
|
27
124
|
lockState.locked = true;
|
|
125
|
+
savedStyle = { overflow: docEl.style.overflow, paddingRight: docEl.style.paddingRight };
|
|
28
126
|
docEl.style.overflow = 'hidden';
|
|
29
127
|
if (scrollbarWidth > 0) {
|
|
30
128
|
docEl.style.paddingRight = `${scrollbarWidth}px`;
|
|
31
129
|
}
|
|
32
130
|
docEl.classList.add('ty-scroll-locked');
|
|
131
|
+
addGuard();
|
|
33
132
|
}
|
|
34
133
|
function unlockBodyFixed() {
|
|
35
134
|
if (!lockState.locked)
|
|
@@ -38,9 +137,10 @@ const docEl = getDocEl();
|
|
|
38
137
|
if (window.tyScrollDebug) {
|
|
39
138
|
console.log('🔓 Unlocking body scroll');
|
|
40
139
|
}
|
|
140
|
+
removeGuard();
|
|
41
141
|
docEl.classList.remove('ty-scroll-locked');
|
|
42
|
-
docEl.style.overflow =
|
|
43
|
-
docEl.style.paddingRight =
|
|
142
|
+
docEl.style.overflow = savedStyle.overflow;
|
|
143
|
+
docEl.style.paddingRight = savedStyle.paddingRight;
|
|
44
144
|
lockState.locked = false;
|
|
45
145
|
}
|
|
46
146
|
export function lockScroll(componentId) {
|
package/lib/utils/validity.d.ts
CHANGED
|
@@ -17,9 +17,17 @@
|
|
|
17
17
|
* single-line field, form.requestSubmit()) and form.reportValidity() also
|
|
18
18
|
* move focus to the first failing field — the first in form order, like a
|
|
19
19
|
* native form, also when a later field shows a bubble. checkValidity() never
|
|
20
|
-
* does.
|
|
20
|
+
* does. For the script-run calls, a form holding a Ty field with an error
|
|
21
|
+
* line gets own requestSubmit / reportValidity wrappers (the prototype and
|
|
22
|
+
* other forms are left alone); they come off when the last such field leaves.
|
|
23
|
+
* An `error` the author set is never overwritten or removed;
|
|
21
24
|
* form.reset() clears every message the controller wrote.
|
|
22
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* Run `fn` as a submit attempt: `invalid` events during it move focus to the
|
|
28
|
+
* first failing field. ty-button runs its form.requestSubmit() in this.
|
|
29
|
+
*/
|
|
30
|
+
export declare function asSubmitAttempt<T>(fn: () => T): T;
|
|
23
31
|
export declare class ValidityController {
|
|
24
32
|
private host;
|
|
25
33
|
private internals;
|
|
@@ -27,7 +35,25 @@ export declare class ValidityController {
|
|
|
27
35
|
private custom;
|
|
28
36
|
/** The message this controller last wrote into `error`. */
|
|
29
37
|
private written;
|
|
38
|
+
/** The anchor last handed to setValidity(). */
|
|
39
|
+
private anchor?;
|
|
40
|
+
/**
|
|
41
|
+
* Anchors a validation bubble was shown on, the last two. WebKit keeps a
|
|
42
|
+
* pointer to that anchor without keeping it alive: once a re-render drops
|
|
43
|
+
* it (ty-calendar rebuilds its grid) and it is garbage-collected, the next
|
|
44
|
+
* submit crashes the page. Held until two later bubbles replaced it.
|
|
45
|
+
*/
|
|
46
|
+
private shown;
|
|
47
|
+
/** The form whose requestSubmit / reportValidity this field keeps wrapped. */
|
|
48
|
+
private form;
|
|
30
49
|
constructor(host: HTMLElement, internals: ElementInternals);
|
|
50
|
+
/**
|
|
51
|
+
* The host connected, disconnected or changed form: keep its form's
|
|
52
|
+
* requestSubmit / reportValidity wrapped while it (or another Ty field with
|
|
53
|
+
* an error line) is in it. Only such a field needs it: a native control or
|
|
54
|
+
* a bubble field is focused by the browser itself.
|
|
55
|
+
*/
|
|
56
|
+
track(): void;
|
|
31
57
|
/**
|
|
32
58
|
* Show the current message, if any, in the error display (on blur after an
|
|
33
59
|
* edit). During a pointer interaction it waits until after the click.
|
package/lib/utils/validity.js
CHANGED
|
@@ -34,22 +34,6 @@ const target = e.composedPath()[0];
|
|
|
34
34
|
if (e.key === 'Enter' && !e.isComposing && target instanceof HTMLInputElement && target.form)
|
|
35
35
|
mark();
|
|
36
36
|
}, true);
|
|
37
|
-
const proto = HTMLFormElement.prototype;
|
|
38
|
-
for (const name of ['requestSubmit', 'reportValidity']) {
|
|
39
|
-
const original = proto[name];
|
|
40
|
-
if (typeof original !== 'function')
|
|
41
|
-
continue;
|
|
42
|
-
proto[name] = function (...args) {
|
|
43
|
-
const was = submitAttempt;
|
|
44
|
-
submitAttempt = true;
|
|
45
|
-
try {
|
|
46
|
-
return original.apply(this, args);
|
|
47
|
-
}
|
|
48
|
-
finally {
|
|
49
|
-
submitAttempt = was;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
37
|
document.addEventListener('pointerdown', () => {
|
|
54
38
|
clearTimeout(touchRelease);
|
|
55
39
|
pointerPressed = true;
|
|
@@ -63,6 +47,50 @@ touchRelease = setTimeout(release, 1000);
|
|
|
63
47
|
document.addEventListener('pointercancel', release, true);
|
|
64
48
|
watchRoot(document);
|
|
65
49
|
}
|
|
50
|
+
export function asSubmitAttempt(fn) {
|
|
51
|
+
const was = submitAttempt;
|
|
52
|
+
submitAttempt = true;
|
|
53
|
+
try {
|
|
54
|
+
return fn();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
submitAttempt = was;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const formMethods = ['requestSubmit', 'reportValidity'];
|
|
61
|
+
const wrappedForms = new WeakMap();
|
|
62
|
+
function wrapForm(form) {
|
|
63
|
+
let entry = wrappedForms.get(form);
|
|
64
|
+
if (entry)
|
|
65
|
+
return entry;
|
|
66
|
+
entry = { fields: new Set(), ours: {} };
|
|
67
|
+
wrappedForms.set(form, entry);
|
|
68
|
+
for (const name of formMethods) {
|
|
69
|
+
if (typeof form[name] !== 'function')
|
|
70
|
+
continue;
|
|
71
|
+
const replaced = Object.getOwnPropertyDescriptor(form, name);
|
|
72
|
+
const previous = replaced ? form[name] : undefined;
|
|
73
|
+
const fn = function (...args) {
|
|
74
|
+
const next = previous ?? Object.getPrototypeOf(form)[name];
|
|
75
|
+
return asSubmitAttempt(() => next.apply(this, args));
|
|
76
|
+
};
|
|
77
|
+
Object.defineProperty(form, name, { value: fn, writable: true, configurable: true, enumerable: false });
|
|
78
|
+
entry.ours[name] = { fn, replaced };
|
|
79
|
+
}
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
function unwrapForm(form, entry) {
|
|
83
|
+
wrappedForms.delete(form);
|
|
84
|
+
for (const name of formMethods) {
|
|
85
|
+
const own = entry.ours[name];
|
|
86
|
+
if (!own || Object.getOwnPropertyDescriptor(form, name)?.value !== own.fn)
|
|
87
|
+
continue;
|
|
88
|
+
if (own.replaced)
|
|
89
|
+
Object.defineProperty(form, name, own.replaced);
|
|
90
|
+
else
|
|
91
|
+
delete form[name];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
66
94
|
const watchedRoots = new WeakSet();
|
|
67
95
|
function watchRoot(root) {
|
|
68
96
|
if (watchedRoots.has(root))
|
|
@@ -102,8 +130,14 @@ this.internals = internals;
|
|
|
102
130
|
this.own = { flags: {} };
|
|
103
131
|
this.custom = '';
|
|
104
132
|
this.written = null;
|
|
133
|
+
this.shown = [];
|
|
134
|
+
this.form = null;
|
|
105
135
|
watchDocument();
|
|
106
136
|
hosts.add(host);
|
|
137
|
+
host.addEventListener('invalid', () => {
|
|
138
|
+
if (this.anchor && !this.shown.includes(this.anchor))
|
|
139
|
+
this.shown = [...this.shown.slice(-1), this.anchor];
|
|
140
|
+
});
|
|
107
141
|
host.addEventListener('invalid', (e) => {
|
|
108
142
|
if (!hasErrorLine(host))
|
|
109
143
|
return;
|
|
@@ -117,6 +151,23 @@ if (!form || firstInvalid(form) === host)
|
|
|
117
151
|
host.focus();
|
|
118
152
|
});
|
|
119
153
|
}
|
|
154
|
+
track() {
|
|
155
|
+
const form = hasErrorLine(this.host) ? this.internals.form : null;
|
|
156
|
+
if (form === this.form)
|
|
157
|
+
return;
|
|
158
|
+
const old = this.form && wrappedForms.get(this.form);
|
|
159
|
+
if (old) {
|
|
160
|
+
old.fields.delete(this);
|
|
161
|
+
for (const c of old.fields)
|
|
162
|
+
if (c.internals.form !== this.form)
|
|
163
|
+
old.fields.delete(c);
|
|
164
|
+
if (old.fields.size === 0)
|
|
165
|
+
unwrapForm(this.form, old);
|
|
166
|
+
}
|
|
167
|
+
this.form = form;
|
|
168
|
+
if (form)
|
|
169
|
+
wrapForm(form).fields.add(this);
|
|
170
|
+
}
|
|
120
171
|
reveal() {
|
|
121
172
|
if (pointerPressed) {
|
|
122
173
|
pendingReveals.add(this);
|
|
@@ -170,6 +221,7 @@ return false;
|
|
|
170
221
|
sync() {
|
|
171
222
|
const { flags, message } = this.own;
|
|
172
223
|
const anchor = this.own.anchor && within(this.host, this.own.anchor) ? this.own.anchor : undefined;
|
|
224
|
+
this.anchor = anchor;
|
|
173
225
|
const invalid = Object.values(flags).some(Boolean);
|
|
174
226
|
if (this.custom) {
|
|
175
227
|
this.internals.setValidity({ ...flags, customError: true }, this.custom, anchor);
|
package/lib/version.d.ts
CHANGED
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.0.0-
|
|
1
|
+
export const VERSION = '1.0.0-TC59';
|