tyrell-components 1.0.0-TC56 → 1.0.0-TC58

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 (212) hide show
  1. package/css/tyrell.css +3 -3
  2. package/dist/tyrell.js +1 -1
  3. package/dist/tyrell.js.map +1 -0
  4. package/lib/base/ty-component.d.ts +15 -3
  5. package/lib/base/ty-component.js +26 -18
  6. package/lib/components/button.d.ts +4 -0
  7. package/lib/components/button.js +33 -6
  8. package/lib/components/calendar-month.js +3 -10
  9. package/lib/components/calendar.d.ts +5 -1
  10. package/lib/components/calendar.js +28 -34
  11. package/lib/components/checkbox.d.ts +3 -1
  12. package/lib/components/checkbox.js +9 -1
  13. package/lib/components/date-picker.d.ts +61 -7
  14. package/lib/components/date-picker.js +274 -133
  15. package/lib/components/file-upload.d.ts +4 -1
  16. package/lib/components/file-upload.js +17 -3
  17. package/lib/components/input.d.ts +82 -3
  18. package/lib/components/input.js +178 -20
  19. package/lib/components/modal.d.ts +2 -0
  20. package/lib/components/modal.js +49 -10
  21. package/lib/components/option.js +7 -5
  22. package/lib/components/popup.js +59 -10
  23. package/lib/components/radio.d.ts +4 -1
  24. package/lib/components/radio.js +10 -1
  25. package/lib/components/resize-observer.js +2 -0
  26. package/lib/components/scroll-container.js +2 -0
  27. package/lib/components/select.d.ts +34 -3
  28. package/lib/components/select.js +135 -11
  29. package/lib/components/selected-tags.d.ts +7 -1
  30. package/lib/components/selected-tags.js +10 -1
  31. package/lib/components/switch.d.ts +5 -1
  32. package/lib/components/switch.js +9 -1
  33. package/lib/components/textarea.d.ts +46 -2
  34. package/lib/components/textarea.js +60 -9
  35. package/lib/styles/button.js +20 -19
  36. package/lib/styles/date-picker.js +18 -1
  37. package/lib/styles/file-upload.d.ts +1 -1
  38. package/lib/styles/file-upload.js +3 -3
  39. package/lib/styles/icon.d.ts +1 -1
  40. package/lib/styles/icon.js +2 -2
  41. package/lib/styles/input.js +1 -1
  42. package/lib/styles/radio.js +2 -2
  43. package/lib/styles/select-base.js +26 -0
  44. package/lib/styles/textarea.d.ts +1 -1
  45. package/lib/styles/textarea.js +7 -0
  46. package/lib/utils/error-flavor.d.ts +8 -0
  47. package/lib/utils/error-flavor.js +1 -0
  48. package/lib/utils/property-manager.d.ts +6 -0
  49. package/lib/utils/property-manager.js +15 -5
  50. package/lib/utils/upgrade-properties.d.ts +21 -0
  51. package/lib/utils/upgrade-properties.js +47 -0
  52. package/lib/utils/validity.d.ts +72 -2
  53. package/lib/utils/validity.js +192 -8
  54. package/lib/version.d.ts +1 -1
  55. package/lib/version.js +1 -1
  56. package/package.json +3 -2
  57. package/lib/base/ty-component.d.ts.map +0 -1
  58. package/lib/base/ty-component.js.map +0 -1
  59. package/lib/components/button.d.ts.map +0 -1
  60. package/lib/components/button.js.map +0 -1
  61. package/lib/components/calendar-month.d.ts.map +0 -1
  62. package/lib/components/calendar-month.js.map +0 -1
  63. package/lib/components/calendar-navigation.d.ts.map +0 -1
  64. package/lib/components/calendar-navigation.js.map +0 -1
  65. package/lib/components/calendar.d.ts.map +0 -1
  66. package/lib/components/calendar.js.map +0 -1
  67. package/lib/components/checkbox.d.ts.map +0 -1
  68. package/lib/components/checkbox.js.map +0 -1
  69. package/lib/components/copy.d.ts.map +0 -1
  70. package/lib/components/copy.js.map +0 -1
  71. package/lib/components/date-picker.d.ts.map +0 -1
  72. package/lib/components/date-picker.js.map +0 -1
  73. package/lib/components/dropdown.d.ts +0 -403
  74. package/lib/components/dropdown.d.ts.map +0 -1
  75. package/lib/components/dropdown.js +0 -1315
  76. package/lib/components/dropdown.js.map +0 -1
  77. package/lib/components/icon.d.ts.map +0 -1
  78. package/lib/components/icon.js.map +0 -1
  79. package/lib/components/input.d.ts.map +0 -1
  80. package/lib/components/input.js.map +0 -1
  81. package/lib/components/modal.d.ts.map +0 -1
  82. package/lib/components/modal.js.map +0 -1
  83. package/lib/components/multiselect.d.ts +0 -389
  84. package/lib/components/multiselect.d.ts.map +0 -1
  85. package/lib/components/multiselect.js +0 -1301
  86. package/lib/components/multiselect.js.map +0 -1
  87. package/lib/components/option.d.ts.map +0 -1
  88. package/lib/components/option.js.map +0 -1
  89. package/lib/components/popup.d.ts.map +0 -1
  90. package/lib/components/popup.js.map +0 -1
  91. package/lib/components/resize-observer.d.ts.map +0 -1
  92. package/lib/components/resize-observer.js.map +0 -1
  93. package/lib/components/scroll-container.d.ts.map +0 -1
  94. package/lib/components/scroll-container.js.map +0 -1
  95. package/lib/components/step.d.ts.map +0 -1
  96. package/lib/components/step.js.map +0 -1
  97. package/lib/components/tab.d.ts.map +0 -1
  98. package/lib/components/tab.js.map +0 -1
  99. package/lib/components/tabs.d.ts.map +0 -1
  100. package/lib/components/tabs.js.map +0 -1
  101. package/lib/components/tag.d.ts.map +0 -1
  102. package/lib/components/tag.js.map +0 -1
  103. package/lib/components/textarea.d.ts.map +0 -1
  104. package/lib/components/textarea.js.map +0 -1
  105. package/lib/components/tooltip.d.ts.map +0 -1
  106. package/lib/components/tooltip.js.map +0 -1
  107. package/lib/components/wizard.d.ts.map +0 -1
  108. package/lib/components/wizard.js.map +0 -1
  109. package/lib/icons/fontawesome/brands.d.ts.map +0 -1
  110. package/lib/icons/fontawesome/brands.js.map +0 -1
  111. package/lib/icons/fontawesome/regular.d.ts.map +0 -1
  112. package/lib/icons/fontawesome/regular.js.map +0 -1
  113. package/lib/icons/fontawesome/solid.d.ts.map +0 -1
  114. package/lib/icons/fontawesome/solid.js.map +0 -1
  115. package/lib/icons/heroicons/micro.d.ts.map +0 -1
  116. package/lib/icons/heroicons/micro.js.map +0 -1
  117. package/lib/icons/heroicons/mini.d.ts.map +0 -1
  118. package/lib/icons/heroicons/mini.js.map +0 -1
  119. package/lib/icons/heroicons/outline.d.ts.map +0 -1
  120. package/lib/icons/heroicons/outline.js.map +0 -1
  121. package/lib/icons/heroicons/solid.d.ts.map +0 -1
  122. package/lib/icons/heroicons/solid.js.map +0 -1
  123. package/lib/icons/lucide.d.ts.map +0 -1
  124. package/lib/icons/lucide.js.map +0 -1
  125. package/lib/icons/material/filled.d.ts.map +0 -1
  126. package/lib/icons/material/filled.js.map +0 -1
  127. package/lib/icons/material/outlined.d.ts.map +0 -1
  128. package/lib/icons/material/outlined.js.map +0 -1
  129. package/lib/icons/material/round.d.ts.map +0 -1
  130. package/lib/icons/material/round.js.map +0 -1
  131. package/lib/icons/material/sharp.d.ts.map +0 -1
  132. package/lib/icons/material/sharp.js.map +0 -1
  133. package/lib/icons/material/two-tone.d.ts.map +0 -1
  134. package/lib/icons/material/two-tone.js.map +0 -1
  135. package/lib/index.d.ts.map +0 -1
  136. package/lib/index.js.map +0 -1
  137. package/lib/styles/button.d.ts.map +0 -1
  138. package/lib/styles/button.js.map +0 -1
  139. package/lib/styles/calendar-month.d.ts.map +0 -1
  140. package/lib/styles/calendar-month.js.map +0 -1
  141. package/lib/styles/calendar-navigation.d.ts.map +0 -1
  142. package/lib/styles/calendar-navigation.js.map +0 -1
  143. package/lib/styles/calendar.d.ts.map +0 -1
  144. package/lib/styles/calendar.js.map +0 -1
  145. package/lib/styles/copy.d.ts.map +0 -1
  146. package/lib/styles/copy.js.map +0 -1
  147. package/lib/styles/date-picker.d.ts.map +0 -1
  148. package/lib/styles/date-picker.js.map +0 -1
  149. package/lib/styles/dropdown.d.ts +0 -12
  150. package/lib/styles/dropdown.d.ts.map +0 -1
  151. package/lib/styles/dropdown.js +0 -725
  152. package/lib/styles/dropdown.js.map +0 -1
  153. package/lib/styles/icon.d.ts.map +0 -1
  154. package/lib/styles/icon.js.map +0 -1
  155. package/lib/styles/input.d.ts.map +0 -1
  156. package/lib/styles/input.js.map +0 -1
  157. package/lib/styles/modal.d.ts.map +0 -1
  158. package/lib/styles/modal.js.map +0 -1
  159. package/lib/styles/multiselect.d.ts +0 -6
  160. package/lib/styles/multiselect.d.ts.map +0 -1
  161. package/lib/styles/multiselect.js +0 -568
  162. package/lib/styles/multiselect.js.map +0 -1
  163. package/lib/styles/option.d.ts.map +0 -1
  164. package/lib/styles/option.js.map +0 -1
  165. package/lib/styles/popup.d.ts.map +0 -1
  166. package/lib/styles/popup.js.map +0 -1
  167. package/lib/styles/resize-observer.d.ts.map +0 -1
  168. package/lib/styles/resize-observer.js.map +0 -1
  169. package/lib/styles/scroll-container.d.ts.map +0 -1
  170. package/lib/styles/scroll-container.js.map +0 -1
  171. package/lib/styles/step.d.ts.map +0 -1
  172. package/lib/styles/step.js.map +0 -1
  173. package/lib/styles/tab.d.ts.map +0 -1
  174. package/lib/styles/tab.js.map +0 -1
  175. package/lib/styles/tabs.d.ts.map +0 -1
  176. package/lib/styles/tabs.js.map +0 -1
  177. package/lib/styles/tag.d.ts.map +0 -1
  178. package/lib/styles/tag.js.map +0 -1
  179. package/lib/styles/textarea.d.ts.map +0 -1
  180. package/lib/styles/textarea.js.map +0 -1
  181. package/lib/styles/tooltip.d.ts.map +0 -1
  182. package/lib/styles/tooltip.js.map +0 -1
  183. package/lib/styles/wizard.d.ts.map +0 -1
  184. package/lib/styles/wizard.js.map +0 -1
  185. package/lib/types/common.d.ts.map +0 -1
  186. package/lib/types/common.js.map +0 -1
  187. package/lib/utils/calendar-utils.d.ts.map +0 -1
  188. package/lib/utils/calendar-utils.js.map +0 -1
  189. package/lib/utils/icon-registry.d.ts.map +0 -1
  190. package/lib/utils/icon-registry.js.map +0 -1
  191. package/lib/utils/locale.d.ts.map +0 -1
  192. package/lib/utils/locale.js.map +0 -1
  193. package/lib/utils/number-format.d.ts.map +0 -1
  194. package/lib/utils/number-format.js.map +0 -1
  195. package/lib/utils/parse-boolean.d.ts +0 -39
  196. package/lib/utils/parse-boolean.d.ts.map +0 -1
  197. package/lib/utils/parse-boolean.js +0 -23
  198. package/lib/utils/parse-boolean.js.map +0 -1
  199. package/lib/utils/positioning.d.ts.map +0 -1
  200. package/lib/utils/positioning.js.map +0 -1
  201. package/lib/utils/property-capture.d.ts.map +0 -1
  202. package/lib/utils/property-capture.js.map +0 -1
  203. package/lib/utils/property-manager.d.ts.map +0 -1
  204. package/lib/utils/property-manager.js.map +0 -1
  205. package/lib/utils/resize-observer.d.ts.map +0 -1
  206. package/lib/utils/resize-observer.js.map +0 -1
  207. package/lib/utils/scroll-lock.d.ts.map +0 -1
  208. package/lib/utils/scroll-lock.js.map +0 -1
  209. package/lib/utils/styles.d.ts.map +0 -1
  210. package/lib/utils/styles.js.map +0 -1
  211. package/lib/version.d.ts.map +0 -1
  212. package/lib/version.js.map +0 -1
@@ -1,5 +1,7 @@
1
1
  import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
2
+ import { coerceBoolean } from '../utils/property-manager.js';
2
3
  import { ensureStyles } from '../utils/styles.js';
4
+ import { upgradeProperties } from '../utils/upgrade-properties.js';
3
5
  import { modalStyles } from '../styles/modal.js';
4
6
  const backdropClickHandlers = new WeakMap();
5
7
  const escapeKeyHandlers = new WeakMap();
@@ -97,7 +99,39 @@ event.preventDefault();
97
99
  closeModal(el, 'backdrop');
98
100
  }
99
101
  }
102
+ function fromNestedDialog(dialog, event) {
103
+ for (const node of event.composedPath()) {
104
+ if (node === dialog)
105
+ return false;
106
+ if (node instanceof HTMLDialogElement && node.matches(':modal'))
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+ function closeNestedDialogs(root, seen = new WeakSet()) {
112
+ const close = (node) => {
113
+ if (seen.has(node))
114
+ return;
115
+ seen.add(node);
116
+ if (node instanceof TyModal && node.open)
117
+ node.removeAttribute('open');
118
+ if (node instanceof HTMLDialogElement && node.matches(':modal'))
119
+ node.close();
120
+ if (node.shadowRoot)
121
+ closeNestedDialogs(node.shadowRoot, seen);
122
+ if (node instanceof HTMLSlotElement) {
123
+ for (const assigned of node.assignedElements({ flatten: true })) {
124
+ close(assigned);
125
+ closeNestedDialogs(assigned, seen);
126
+ }
127
+ }
128
+ };
129
+ root.querySelectorAll('*').forEach(close);
130
+ }
100
131
  function handleEscapeKey(el, event) {
132
+ const dialog = event.currentTarget;
133
+ if (fromNestedDialog(dialog, event))
134
+ return;
101
135
  event.stopPropagation();
102
136
  if (event.key === 'Escape') {
103
137
  event.preventDefault();
@@ -154,7 +188,7 @@ dialog.addEventListener('keydown', handler);
154
188
  }
155
189
  else {
156
190
  const handler = (e) => {
157
- if (e.key === 'Escape')
191
+ if (e.key === 'Escape' && !fromNestedDialog(dialog, e))
158
192
  e.preventDefault();
159
193
  };
160
194
  escapeKeyHandlers.set(el, handler);
@@ -252,6 +286,7 @@ dispatchModalEvent(el, 'open', {});
252
286
  else {
253
287
  if (dialog.open) {
254
288
  unlockScroll(modalId);
289
+ closeNestedDialogs(el);
255
290
  dialog.close();
256
291
  syncBackdropZoom();
257
292
  const reason = pendingCloseReasons.get(el) ?? 'programmatic';
@@ -259,10 +294,12 @@ pendingCloseReasons.delete(el);
259
294
  dispatchModalEvent(el, 'close', { reason });
260
295
  }
261
296
  }
297
+ syncBackdropZoom();
262
298
  dialog.onclose = (event) => {
263
299
  if (event.target !== dialog)
264
300
  return;
265
301
  unlockScroll(modalId);
302
+ closeNestedDialogs(el);
266
303
  if (el.hasAttribute('open')) {
267
304
  el.removeAttribute('open');
268
305
  dispatchModalEvent(el, 'close', {
@@ -274,10 +311,7 @@ syncBackdropZoom();
274
311
  };
275
312
  }
276
313
  function syncBackdropZoom() {
277
- const anyOpen = Array.from(document.querySelectorAll('ty-modal[open][backdrop-zoom], ty-dialog[open][backdrop-zoom]')).some((m) => {
278
- const v = m.getAttribute('backdrop-zoom').toLowerCase().trim();
279
- return v !== 'false' && v !== '0';
280
- });
314
+ const anyOpen = Array.from(document.querySelectorAll('ty-modal[open], ty-dialog[open]')).some((m) => parseBoolAttr(m, 'open') && !parseBoolAttr(m, 'no-zoom'));
281
315
  document.documentElement.classList.toggle('ty-modal-zoom', anyOpen);
282
316
  }
283
317
  function cleanup(el) {
@@ -325,27 +359,32 @@ hoverLeaveHandlers.delete(el);
325
359
  }
326
360
  export class TyModal extends HTMLElement {
327
361
  static get observedAttributes() {
328
- return ['open', 'no-backdrop', 'prevent-outside-click', 'prevent-escape', 'label'];
362
+ return ['open', 'no-backdrop', 'no-zoom', 'prevent-outside-click', 'prevent-escape', 'label'];
329
363
  }
330
364
  get open() { return parseBoolAttr(this, 'open'); }
331
- set open(v) { v ? this.setAttribute('open', 'true') : this.removeAttribute('open'); }
365
+ set open(v) { coerceBoolean(v) ? this.setAttribute('open', 'true') : this.removeAttribute('open'); }
332
366
  get noBackdrop() { return parseBoolAttr(this, 'no-backdrop'); }
333
367
  set noBackdrop(v) {
334
- v ? this.setAttribute('no-backdrop', '') : this.removeAttribute('no-backdrop');
368
+ coerceBoolean(v) ? this.setAttribute('no-backdrop', '') : this.removeAttribute('no-backdrop');
369
+ }
370
+ get noZoom() { return parseBoolAttr(this, 'no-zoom'); }
371
+ set noZoom(v) {
372
+ coerceBoolean(v) ? this.setAttribute('no-zoom', '') : this.removeAttribute('no-zoom');
335
373
  }
336
374
  get preventOutsideClick() { return parseBoolAttr(this, 'prevent-outside-click'); }
337
375
  set preventOutsideClick(v) {
338
- v ? this.setAttribute('prevent-outside-click', '') : this.removeAttribute('prevent-outside-click');
376
+ coerceBoolean(v) ? this.setAttribute('prevent-outside-click', '') : this.removeAttribute('prevent-outside-click');
339
377
  }
340
378
  get preventEscape() { return parseBoolAttr(this, 'prevent-escape'); }
341
379
  set preventEscape(v) {
342
- v ? this.setAttribute('prevent-escape', '') : this.removeAttribute('prevent-escape');
380
+ coerceBoolean(v) ? this.setAttribute('prevent-escape', '') : this.removeAttribute('prevent-escape');
343
381
  }
344
382
  constructor() {
345
383
  super();
346
384
  this.attachShadow({ mode: 'open' });
347
385
  }
348
386
  connectedCallback() {
387
+ upgradeProperties(this);
349
388
  render(this);
350
389
  this.show = () => openModal(this);
351
390
  this.hide = (opts) => closeModal(this, 'programmatic', opts);
@@ -1,4 +1,6 @@
1
1
  import { ensureStyles } from '../utils/styles.js';
2
+ import { coerceBoolean } from '../utils/property-manager.js';
3
+ import { upgradeProperties } from '../utils/upgrade-properties.js';
2
4
  import { optionStyles } from '../styles/option.js';
3
5
  const CHECK_SVG = `
4
6
  <svg class="option-check" viewBox="0 0 24 24" fill="none" stroke="currentColor"
@@ -22,11 +24,7 @@ static get observedAttributes() {
22
24
  return ['value', 'disabled', 'selected', 'highlighted', 'hidden'];
23
25
  }
24
26
  connectedCallback() {
25
- const instanceValue = Object.getOwnPropertyDescriptor(this, 'value');
26
- if (instanceValue) {
27
- this._value = instanceValue.value;
28
- delete this.value;
29
- }
27
+ upgradeProperties(this);
30
28
  this.render();
31
29
  }
32
30
  attributeChangedCallback(name, oldValue, newValue) {
@@ -80,6 +78,7 @@ get selected() {
80
78
  return this._selected;
81
79
  }
82
80
  set selected(value) {
81
+ value = coerceBoolean(value);
83
82
  if (this._selected !== value) {
84
83
  this._selected = value;
85
84
  if (value) {
@@ -95,6 +94,7 @@ get disabled() {
95
94
  return this._disabled;
96
95
  }
97
96
  set disabled(value) {
97
+ value = coerceBoolean(value);
98
98
  if (this._disabled !== value) {
99
99
  this._disabled = value;
100
100
  if (value) {
@@ -110,6 +110,7 @@ get highlighted() {
110
110
  return this._highlighted;
111
111
  }
112
112
  set highlighted(value) {
113
+ value = coerceBoolean(value);
113
114
  if (this._highlighted !== value) {
114
115
  this._highlighted = value;
115
116
  if (value) {
@@ -125,6 +126,7 @@ get hidden() {
125
126
  return this._hidden;
126
127
  }
127
128
  set hidden(value) {
129
+ value = coerceBoolean(value);
128
130
  if (this._hidden !== value) {
129
131
  this._hidden = value;
130
132
  if (value) {
@@ -5,9 +5,11 @@ import { popupStyles } from '../styles/popup.js';
5
5
  const anchorClickHandlers = new WeakMap();
6
6
  const anchorKeyClickHandlers = new WeakMap();
7
7
  const outsideClickHandlers = new WeakMap();
8
+ const closeTimers = new WeakMap();
8
9
  const escapeHandlers = new WeakMap();
9
10
  const closeRequestHandlers = new WeakMap();
10
11
  const popupIds = new WeakMap();
12
+ const pointerPresses = new WeakSet();
11
13
  function parseBoolAttr(el, name) {
12
14
  return el.hasAttribute(name);
13
15
  }
@@ -76,20 +78,33 @@ if (anchor?.hasAttribute('aria-haspopup'))
76
78
  anchor.setAttribute('aria-expanded', 'false');
77
79
  const exitMs = Math.max(0, ...getComputedStyle(dialog).transitionDuration
78
80
  .split(',').map((d) => parseFloat(d) * (d.trim().endsWith('ms') ? 1 : 1000)));
79
- setTimeout(() => {
81
+ clearTimeout(closeTimers.get(el));
82
+ closeTimers.set(el, setTimeout(() => {
83
+ closeTimers.delete(el);
80
84
  dialog.classList.remove('position-calculated');
81
85
  dialog.close();
82
- }, exitMs + 20);
86
+ }, exitMs + 20));
83
87
  }
84
88
  function openPopup(el) {
85
- const popupId = getPopupId(el);
86
- lockScroll(popupId);
87
89
  const shadowRoot = el.shadowRoot;
88
90
  const dialog = shadowRoot ? getPopupDialog(shadowRoot) : null;
89
91
  if (!dialog)
90
92
  return;
91
- dialog.showModal();
92
93
  const anchor = getAnchorElement(el);
94
+ if (dialog.open) {
95
+ const pending = closeTimers.get(el);
96
+ if (pending !== undefined) {
97
+ clearTimeout(pending);
98
+ closeTimers.delete(el);
99
+ dialog.classList.add('open');
100
+ if (anchor?.hasAttribute('aria-haspopup'))
101
+ anchor.setAttribute('aria-expanded', 'true');
102
+ }
103
+ return;
104
+ }
105
+ const popupId = getPopupId(el);
106
+ lockScroll(popupId);
107
+ dialog.showModal();
93
108
  if (anchor?.hasAttribute('aria-haspopup'))
94
109
  anchor.setAttribute('aria-expanded', 'true');
95
110
  requestAnimationFrame(() => {
@@ -128,7 +143,19 @@ event.preventDefault();
128
143
  closePopup(el);
129
144
  }
130
145
  }
146
+ function fromNestedDialog(el, event) {
147
+ const dialog = el.shadowRoot ? getPopupDialog(el.shadowRoot) : null;
148
+ for (const node of event.composedPath()) {
149
+ if (node === dialog)
150
+ return false;
151
+ if (node instanceof HTMLDialogElement && node.matches(':modal'))
152
+ return true;
153
+ }
154
+ return false;
155
+ }
131
156
  function handleEscape(el, event) {
157
+ if (fromNestedDialog(el, event))
158
+ return;
132
159
  event.stopPropagation();
133
160
  if (event.key === 'Escape') {
134
161
  const { disableClose } = getPopupAttributes(el);
@@ -160,11 +187,21 @@ const existingKeyHandler = anchorKeyClickHandlers.get(el);
160
187
  if (existingKeyHandler) {
161
188
  anchor.removeEventListener('click', existingKeyHandler);
162
189
  }
163
- const handler = (e) => handleAnchorClick(el, e);
190
+ const handler = (e) => {
191
+ pointerPresses.add(el);
192
+ handleAnchorClick(el, e);
193
+ };
164
194
  anchorClickHandlers.set(el, handler);
165
195
  anchor.addEventListener('pointerdown', handler);
166
196
  const keyHandler = (e) => {
167
- if (e.detail === 0)
197
+ if (pointerPresses.delete(el))
198
+ return;
199
+ const d = e.detail;
200
+ const click = typeof d === 'number' ? e : d?.originalEvent ?? e;
201
+ if (click.detail !== 0)
202
+ return;
203
+ if (click.composedPath().includes(el))
204
+ return;
168
205
  handleAnchorClick(el, e);
169
206
  };
170
207
  anchorKeyClickHandlers.set(el, keyHandler);
@@ -209,17 +246,29 @@ dialog.addEventListener('pointerdown', outsideHandler);
209
246
  const escapeHandler = (e) => handleEscape(el, e);
210
247
  escapeHandlers.set(el, escapeHandler);
211
248
  el.addEventListener('keydown', escapeHandler);
212
- dialog.addEventListener('close', () => {
213
- const popupId = getPopupId(el);
214
- unlockScroll(popupId);
249
+ new MutationObserver(() => {
250
+ if (dialog.open)
251
+ return;
252
+ clearTimeout(closeTimers.get(el));
253
+ closeTimers.delete(el);
254
+ pointerPresses.delete(el);
255
+ unlockScroll(getPopupId(el));
215
256
  dialog.classList.remove('open');
216
257
  dialog.classList.remove('preparing-animation');
258
+ const anchor = getAnchorElement(el);
259
+ if (anchor?.hasAttribute('aria-haspopup'))
260
+ anchor.setAttribute('aria-expanded', 'false');
261
+ }).observe(dialog, { attributes: true, attributeFilter: ['open'] });
262
+ dialog.addEventListener('close', () => {
217
263
  el.dispatchEvent(new CustomEvent('close'));
218
264
  });
219
265
  }
220
266
  el.closePopup = () => closePopup(el, true);
221
267
  el.openPopup = () => openPopup(el);
222
268
  el.togglePopup = () => togglePopup(el);
269
+ const existingCloseRequest = closeRequestHandlers.get(el);
270
+ if (existingCloseRequest)
271
+ el.removeEventListener('ty:close-popup', existingCloseRequest);
223
272
  const closeRequestHandler = (e) => handleCloseRequest(el, e);
224
273
  closeRequestHandlers.set(el, closeRequestHandler);
225
274
  el.addEventListener('ty:close-popup', closeRequestHandler);
@@ -160,10 +160,13 @@ export declare class TyRadioGroup extends TyComponent<RadioGroupState> implement
160
160
  * the accessors after upgrade. Each radio fires ty-radio-connected once
161
161
  * upgraded, which re-syncs the group.
162
162
  */
163
+ /** Focus the group's tab stop: the checked radio, else the first enabled one. */
164
+ focus(options?: FocusOptions): void;
163
165
  private getRadios;
164
166
  private syncChildren;
165
167
  /** Report `required` to the owning form — without this the asterisk
166
- showed but an empty group submitted anyway. */
168
+ showed but an empty group submitted anyway. The message is a native
169
+ radio's; it needs a name, a nameless radio never reports valueMissing. */
167
170
  private updateValidity;
168
171
  private handleRadioSelect;
169
172
  /**
@@ -4,6 +4,7 @@ import { ensureStyles } from "../utils/styles.js";
4
4
  import { syncCustomFlavorSheet } from "../utils/flavor-sheet.js";
5
5
  import { radioStyles, radioCustomFlavorCss } from "../styles/radio.js";
6
6
  import { REQUIRED_ICON_SVG } from "../utils/icons.js";
7
+ import { nativeMessage } from "../utils/validity.js";
7
8
  export class TyRadio extends TyComponent {
8
9
  constructor() {
9
10
  super();
@@ -184,6 +185,14 @@ this.syncChildren();
184
185
  getFormValue() {
185
186
  return this.value || null;
186
187
  }
188
+ focus(options) {
189
+ const enabled = this.getRadios().filter((r) => !r.disabled);
190
+ const stop = enabled.find((r) => r.checked) ?? enabled[0];
191
+ if (stop)
192
+ stop.focus(options);
193
+ else
194
+ super.focus(options);
195
+ }
187
196
  getRadios() {
188
197
  return Array.from(this.querySelectorAll("ty-radio"))
189
198
  .filter((r) => typeof r.setTabStop === "function");
@@ -215,7 +224,7 @@ updateValidity() {
215
224
  if (this.required && !this.disabled && !this.value) {
216
225
  const anchor = this.getRadios().find((r) => !r.disabled);
217
226
  const target = anchor?.shadowRoot?.querySelector(".radio-container");
218
- this.applyValidity({ valueMissing: true }, "Please select an option", target ?? undefined);
227
+ this.applyValidity({ valueMissing: true }, nativeMessage("input", { type: "radio", name: "ty", required: true }), target ?? undefined);
219
228
  }
220
229
  else {
221
230
  this.applyValidity({});
@@ -1,6 +1,7 @@
1
1
  import { ensureStyles } from '../utils/styles.js';
2
2
  import { resizeObserverStyles } from '../styles/resize-observer.js';
3
3
  import { updateSize, removeSize } from '../utils/resize-observer.js';
4
+ import { upgradeProperties } from '../utils/upgrade-properties.js';
4
5
  export class TyResizeObserver extends HTMLElement {
5
6
  static get observedAttributes() {
6
7
  return ['id', 'debounce'];
@@ -20,6 +21,7 @@ ensureStyles(shadow, { css: resizeObserverStyles, id: 'ty-resize-observer' });
20
21
  shadow.innerHTML = '<slot></slot>';
21
22
  }
22
23
  connectedCallback() {
24
+ upgradeProperties(this);
23
25
  this.setupObserver();
24
26
  }
25
27
  attributeChangedCallback(name, oldValue, newValue) {
@@ -1,6 +1,7 @@
1
1
  import { ensureStyles } from '../utils/styles.js';
2
2
  import { scrollContainerStyles } from '../styles/scroll-container.js';
3
3
  import { CustomScrollbar } from '../utils/custom-scrollbar.js';
4
+ import { upgradeProperties } from '../utils/upgrade-properties.js';
4
5
  export class TyScrollContainer extends HTMLElement {
5
6
  static get observedAttributes() {
6
7
  return ['no-shadow', 'max-height', 'hide-scrollbar', 'custom-scrollbar', 'overflow-x', 'scroll-anchoring'];
@@ -112,6 +113,7 @@ this._shadowLeft = shadow.querySelector('.shadow-left');
112
113
  this._shadowRight = shadow.querySelector('.shadow-right');
113
114
  }
114
115
  connectedCallback() {
116
+ upgradeProperties(this);
115
117
  this._scrollWrapper?.addEventListener('scroll', this._onScroll, { passive: true });
116
118
  this._resizeObserver = new ResizeObserver(() => {
117
119
  this._scrollbar?.update();
@@ -73,6 +73,11 @@ export declare class TySelect extends TyComponent<SelectState> {
73
73
  visual: boolean;
74
74
  default: boolean;
75
75
  };
76
+ error: {
77
+ type: "string";
78
+ visual: boolean;
79
+ default: string;
80
+ };
76
81
  noClear: {
77
82
  type: "boolean";
78
83
  visual: boolean;
@@ -190,11 +195,19 @@ export declare class TySelect extends TyComponent<SelectState> {
190
195
  private _childObserver;
191
196
  private _selectedClone;
192
197
  private _customFlavorSheet;
198
+ private _ariaTrigger;
193
199
  constructor();
194
200
  /** TyComponent handles property capture automatically. */
195
201
  protected onConnect(): void;
196
202
  /** Custom (non-built-in) flavors — see utils/flavor-sheet.ts. */
197
203
  private _syncCustomFlavor;
204
+ /**
205
+ * `error` turns a neutral select danger, like ty-input; clearing it puts
206
+ * back exactly the flavor attribute the author had, or none, kept meanwhile
207
+ * in ERROR_FLAVOR. Runs once connected: while the element upgrades, `error`
208
+ * may come before the `flavor` written next to it.
209
+ */
210
+ private syncErrorFlavor;
198
211
  protected onDisconnect(): void;
199
212
  /** Handle state synchronization BEFORE render. */
200
213
  protected onPropertiesChanged(changes: PropertyChange[]): void;
@@ -207,6 +220,8 @@ export declare class TySelect extends TyComponent<SelectState> {
207
220
  private applyLoadingState;
208
221
  /** Form value: a plain entry for single, FormData with repeated entries for multiple (HTMX standard). */
209
222
  protected getFormValue(): FormDataEntryValue | FormData | null;
223
+ /** Focus the trigger: a slotted one, else the field. The host itself isn't focusable. */
224
+ focus(options?: FocusOptions): void;
210
225
  private parseValue;
211
226
  /** Initialize component state from the value property / pre-selected children. */
212
227
  private initializeState;
@@ -367,8 +382,9 @@ export declare class TySelect extends TyComponent<SelectState> {
367
382
  * Reflect constraint validation to the owning <form> via ElementInternals.
368
383
  * Without this `required` is only cosmetic — form.checkValidity() (and a
369
384
  * real submit) never actually blocks on an empty required select. Mirrors
370
- * ty-input's updateValidity(): empty + required => valueMissing; disabled
371
- * fields are barred from constraint validation.
385
+ * ty-input's updateValidity(): empty + required => valueMissing, with a
386
+ * native <select>'s message; disabled fields are barred from constraint
387
+ * validation.
372
388
  */
373
389
  private updateValidity;
374
390
  /**
@@ -381,6 +397,16 @@ export declare class TySelect extends TyComponent<SelectState> {
381
397
  private renderDesktop;
382
398
  private renderMobile;
383
399
  private setupMobileEventListeners;
400
+ /**
401
+ * The dialog was closed by someone else — a ty-modal around this select
402
+ * closes the dialogs still open inside it. Run our own close too, so the
403
+ * open state, aria-expanded and the scroll lock follow. Watches the `open`
404
+ * attribute rather than the `close` event: that one comes a task later,
405
+ * so right after modal.hide() the stub would still say expanded. Our own
406
+ * close() calls land here with the state already closed, and a dialog
407
+ * reopened since then is open again: both are no-ops.
408
+ */
409
+ private syncOnOutsideClose;
384
410
  private handleMobileStubClick;
385
411
  /** Keyboard-open the (closed) mobile stub — see handleStubKeydown for why this is needed. */
386
412
  private handleMobileStubKeydown;
@@ -442,7 +468,9 @@ export declare class TySelect extends TyComponent<SelectState> {
442
468
  * Clear the entire selection programmatically WITH a change event
443
469
  * (action: 'clear'). No-op if nothing is selected. Exists so slot="trigger"
444
470
  * consumers (custom chrome, built-in clear UI hidden by design) can wire
445
- * their own clear icon's click handler to it.
471
+ * their own clear icon's click handler to it. Shows no validation
472
+ * message (a code reset after a save shouldn't flag the field); the
473
+ * built-in × does.
446
474
  */
447
475
  clear(): void;
448
476
  get value(): string;
@@ -467,6 +495,8 @@ export declare class TySelect extends TyComponent<SelectState> {
467
495
  set noClear(value: boolean);
468
496
  get required(): boolean;
469
497
  set required(value: boolean);
498
+ get error(): string;
499
+ set error(value: string);
470
500
  get notSearchable(): boolean;
471
501
  set notSearchable(value: boolean);
472
502
  get externalSearch(): boolean;
@@ -481,6 +511,7 @@ export declare class TySelect extends TyComponent<SelectState> {
481
511
  set placement(value: Placement | "");
482
512
  get flavor(): Flavor;
483
513
  set flavor(value: Flavor);
514
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
484
515
  get form(): HTMLFormElement | null;
485
516
  }
486
517
  export {};