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.
Files changed (65) hide show
  1. package/css/tyrell-colors-static.css +14 -0
  2. package/css/tyrell-theme.css +1 -0
  3. package/css/tyrell.css +8 -6
  4. package/dist/tyrell-colors-static.css +1 -1
  5. package/dist/tyrell-theme.css +1 -1
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.d.ts +8 -12
  8. package/lib/base/ty-component.js +38 -9
  9. package/lib/components/button.d.ts +10 -0
  10. package/lib/components/button.js +22 -1
  11. package/lib/components/calendar-month.d.ts +7 -1
  12. package/lib/components/calendar-month.js +32 -18
  13. package/lib/components/calendar-navigation.js +19 -6
  14. package/lib/components/calendar.d.ts +24 -5
  15. package/lib/components/calendar.js +83 -37
  16. package/lib/components/checkbox.d.ts +5 -0
  17. package/lib/components/checkbox.js +18 -5
  18. package/lib/components/copy.d.ts +2 -0
  19. package/lib/components/copy.js +15 -31
  20. package/lib/components/date-picker.d.ts +7 -26
  21. package/lib/components/date-picker.js +14 -84
  22. package/lib/components/file-upload.js +6 -2
  23. package/lib/components/input.d.ts +0 -9
  24. package/lib/components/input.js +17 -40
  25. package/lib/components/modal.js +59 -18
  26. package/lib/components/popup.js +56 -5
  27. package/lib/components/radio.d.ts +6 -0
  28. package/lib/components/radio.js +12 -28
  29. package/lib/components/select.d.ts +29 -11
  30. package/lib/components/select.js +43 -57
  31. package/lib/components/switch.d.ts +4 -0
  32. package/lib/components/switch.js +18 -5
  33. package/lib/components/tabs.js +1 -1
  34. package/lib/components/textarea.d.ts +1 -0
  35. package/lib/components/textarea.js +21 -8
  36. package/lib/index.d.ts +1 -0
  37. package/lib/index.js +1 -0
  38. package/lib/styles/button.js +1 -0
  39. package/lib/styles/calendar-month.js +1 -1
  40. package/lib/styles/calendar.js +3 -0
  41. package/lib/styles/checkbox.js +11 -5
  42. package/lib/styles/date-picker.js +5 -10
  43. package/lib/styles/input.js +5 -9
  44. package/lib/styles/option.d.ts +1 -1
  45. package/lib/styles/option.js +4 -0
  46. package/lib/styles/popup.d.ts +1 -1
  47. package/lib/styles/popup.js +2 -0
  48. package/lib/styles/radio.js +10 -4
  49. package/lib/styles/select-base.js +5 -10
  50. package/lib/styles/select.d.ts +1 -1
  51. package/lib/styles/select.js +4 -0
  52. package/lib/styles/switch.js +10 -5
  53. package/lib/styles/tag.js +2 -1
  54. package/lib/utils/calendar-utils.d.ts +49 -0
  55. package/lib/utils/calendar-utils.js +63 -6
  56. package/lib/utils/scroll-lock.d.ts +10 -4
  57. package/lib/utils/scroll-lock.js +102 -2
  58. package/lib/utils/validity.d.ts +27 -1
  59. package/lib/utils/validity.js +68 -16
  60. package/lib/version.d.ts +1 -1
  61. package/lib/version.js +1 -1
  62. package/package.json +1 -1
  63. package/dist/tyrell.js.map +0 -1
  64. package/lib/utils/error-flavor.d.ts +0 -8
  65. package/lib/utils/error-flavor.js +0 -1
@@ -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) {
@@ -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. An `error` the author set is never overwritten or removed;
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.
@@ -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
@@ -4,4 +4,4 @@
4
4
  * This is automatically synced with package.json during build.
5
5
  * Use `npm version` to bump versions.
6
6
  */
7
- export declare const VERSION = "1.0.0-TC58";
7
+ export declare const VERSION = "1.0.0-TC59";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.0-TC58';
1
+ export const VERSION = '1.0.0-TC59';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyrell-components",
3
- "version": "1.0.0-TC58",
3
+ "version": "1.0.0-TC59",
4
4
  "description": "Tyrell Components - Framework-agnostic web components with semantic design system",
5
5
  "type": "module",
6
6
  "sideEffects": [