tyrell-components 1.0.0-TC57 → 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 (226) 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 +20 -15
  8. package/lib/base/ty-component.js +58 -25
  9. package/lib/components/button.d.ts +14 -0
  10. package/lib/components/button.js +55 -7
  11. package/lib/components/calendar-month.d.ts +7 -1
  12. package/lib/components/calendar-month.js +35 -28
  13. package/lib/components/calendar-navigation.js +19 -6
  14. package/lib/components/calendar.d.ts +25 -4
  15. package/lib/components/calendar.js +98 -67
  16. package/lib/components/checkbox.d.ts +6 -1
  17. package/lib/components/checkbox.js +20 -6
  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 +52 -19
  21. package/lib/components/date-picker.js +211 -147
  22. package/lib/components/file-upload.d.ts +2 -1
  23. package/lib/components/file-upload.js +8 -3
  24. package/lib/components/input.d.ts +4 -6
  25. package/lib/components/input.js +37 -62
  26. package/lib/components/modal.js +86 -9
  27. package/lib/components/option.js +2 -5
  28. package/lib/components/popup.js +99 -11
  29. package/lib/components/radio.d.ts +8 -1
  30. package/lib/components/radio.js +14 -29
  31. package/lib/components/resize-observer.js +2 -0
  32. package/lib/components/scroll-container.js +2 -0
  33. package/lib/components/select.d.ts +53 -6
  34. package/lib/components/select.js +127 -29
  35. package/lib/components/switch.d.ts +7 -1
  36. package/lib/components/switch.js +20 -6
  37. package/lib/components/tabs.js +1 -1
  38. package/lib/components/textarea.d.ts +45 -4
  39. package/lib/components/textarea.js +75 -18
  40. package/lib/index.d.ts +1 -0
  41. package/lib/index.js +1 -0
  42. package/lib/styles/button.js +21 -19
  43. package/lib/styles/calendar-month.js +1 -1
  44. package/lib/styles/calendar.js +3 -0
  45. package/lib/styles/checkbox.js +11 -5
  46. package/lib/styles/date-picker.js +15 -3
  47. package/lib/styles/input.js +5 -9
  48. package/lib/styles/option.d.ts +1 -1
  49. package/lib/styles/option.js +4 -0
  50. package/lib/styles/popup.d.ts +1 -1
  51. package/lib/styles/popup.js +2 -0
  52. package/lib/styles/radio.js +10 -4
  53. package/lib/styles/select-base.js +24 -3
  54. package/lib/styles/select.d.ts +1 -1
  55. package/lib/styles/select.js +4 -0
  56. package/lib/styles/switch.js +10 -5
  57. package/lib/styles/tag.js +2 -1
  58. package/lib/styles/textarea.d.ts +1 -1
  59. package/lib/styles/textarea.js +7 -0
  60. package/lib/utils/calendar-utils.d.ts +49 -0
  61. package/lib/utils/calendar-utils.js +63 -6
  62. package/lib/utils/scroll-lock.d.ts +10 -4
  63. package/lib/utils/scroll-lock.js +102 -2
  64. package/lib/utils/upgrade-properties.d.ts +21 -0
  65. package/lib/utils/upgrade-properties.js +47 -0
  66. package/lib/utils/validity.d.ts +89 -5
  67. package/lib/utils/validity.js +198 -25
  68. package/lib/version.d.ts +1 -1
  69. package/lib/version.js +1 -1
  70. package/package.json +2 -1
  71. package/lib/base/ty-component.d.ts.map +0 -1
  72. package/lib/base/ty-component.js.map +0 -1
  73. package/lib/components/button.d.ts.map +0 -1
  74. package/lib/components/button.js.map +0 -1
  75. package/lib/components/calendar-month.d.ts.map +0 -1
  76. package/lib/components/calendar-month.js.map +0 -1
  77. package/lib/components/calendar-navigation.d.ts.map +0 -1
  78. package/lib/components/calendar-navigation.js.map +0 -1
  79. package/lib/components/calendar.d.ts.map +0 -1
  80. package/lib/components/calendar.js.map +0 -1
  81. package/lib/components/checkbox.d.ts.map +0 -1
  82. package/lib/components/checkbox.js.map +0 -1
  83. package/lib/components/copy.d.ts.map +0 -1
  84. package/lib/components/copy.js.map +0 -1
  85. package/lib/components/date-picker.d.ts.map +0 -1
  86. package/lib/components/date-picker.js.map +0 -1
  87. package/lib/components/dropdown.d.ts +0 -403
  88. package/lib/components/dropdown.d.ts.map +0 -1
  89. package/lib/components/dropdown.js +0 -1315
  90. package/lib/components/dropdown.js.map +0 -1
  91. package/lib/components/icon.d.ts.map +0 -1
  92. package/lib/components/icon.js.map +0 -1
  93. package/lib/components/input.d.ts.map +0 -1
  94. package/lib/components/input.js.map +0 -1
  95. package/lib/components/modal.d.ts.map +0 -1
  96. package/lib/components/modal.js.map +0 -1
  97. package/lib/components/multiselect.d.ts +0 -389
  98. package/lib/components/multiselect.d.ts.map +0 -1
  99. package/lib/components/multiselect.js +0 -1301
  100. package/lib/components/multiselect.js.map +0 -1
  101. package/lib/components/option.d.ts.map +0 -1
  102. package/lib/components/option.js.map +0 -1
  103. package/lib/components/popup.d.ts.map +0 -1
  104. package/lib/components/popup.js.map +0 -1
  105. package/lib/components/resize-observer.d.ts.map +0 -1
  106. package/lib/components/resize-observer.js.map +0 -1
  107. package/lib/components/scroll-container.d.ts.map +0 -1
  108. package/lib/components/scroll-container.js.map +0 -1
  109. package/lib/components/step.d.ts.map +0 -1
  110. package/lib/components/step.js.map +0 -1
  111. package/lib/components/tab.d.ts.map +0 -1
  112. package/lib/components/tab.js.map +0 -1
  113. package/lib/components/tabs.d.ts.map +0 -1
  114. package/lib/components/tabs.js.map +0 -1
  115. package/lib/components/tag.d.ts.map +0 -1
  116. package/lib/components/tag.js.map +0 -1
  117. package/lib/components/textarea.d.ts.map +0 -1
  118. package/lib/components/textarea.js.map +0 -1
  119. package/lib/components/tooltip.d.ts.map +0 -1
  120. package/lib/components/tooltip.js.map +0 -1
  121. package/lib/components/wizard.d.ts.map +0 -1
  122. package/lib/components/wizard.js.map +0 -1
  123. package/lib/icons/fontawesome/brands.d.ts.map +0 -1
  124. package/lib/icons/fontawesome/brands.js.map +0 -1
  125. package/lib/icons/fontawesome/regular.d.ts.map +0 -1
  126. package/lib/icons/fontawesome/regular.js.map +0 -1
  127. package/lib/icons/fontawesome/solid.d.ts.map +0 -1
  128. package/lib/icons/fontawesome/solid.js.map +0 -1
  129. package/lib/icons/heroicons/micro.d.ts.map +0 -1
  130. package/lib/icons/heroicons/micro.js.map +0 -1
  131. package/lib/icons/heroicons/mini.d.ts.map +0 -1
  132. package/lib/icons/heroicons/mini.js.map +0 -1
  133. package/lib/icons/heroicons/outline.d.ts.map +0 -1
  134. package/lib/icons/heroicons/outline.js.map +0 -1
  135. package/lib/icons/heroicons/solid.d.ts.map +0 -1
  136. package/lib/icons/heroicons/solid.js.map +0 -1
  137. package/lib/icons/lucide.d.ts.map +0 -1
  138. package/lib/icons/lucide.js.map +0 -1
  139. package/lib/icons/material/filled.d.ts.map +0 -1
  140. package/lib/icons/material/filled.js.map +0 -1
  141. package/lib/icons/material/outlined.d.ts.map +0 -1
  142. package/lib/icons/material/outlined.js.map +0 -1
  143. package/lib/icons/material/round.d.ts.map +0 -1
  144. package/lib/icons/material/round.js.map +0 -1
  145. package/lib/icons/material/sharp.d.ts.map +0 -1
  146. package/lib/icons/material/sharp.js.map +0 -1
  147. package/lib/icons/material/two-tone.d.ts.map +0 -1
  148. package/lib/icons/material/two-tone.js.map +0 -1
  149. package/lib/index.d.ts.map +0 -1
  150. package/lib/index.js.map +0 -1
  151. package/lib/styles/button.d.ts.map +0 -1
  152. package/lib/styles/button.js.map +0 -1
  153. package/lib/styles/calendar-month.d.ts.map +0 -1
  154. package/lib/styles/calendar-month.js.map +0 -1
  155. package/lib/styles/calendar-navigation.d.ts.map +0 -1
  156. package/lib/styles/calendar-navigation.js.map +0 -1
  157. package/lib/styles/calendar.d.ts.map +0 -1
  158. package/lib/styles/calendar.js.map +0 -1
  159. package/lib/styles/copy.d.ts.map +0 -1
  160. package/lib/styles/copy.js.map +0 -1
  161. package/lib/styles/date-picker.d.ts.map +0 -1
  162. package/lib/styles/date-picker.js.map +0 -1
  163. package/lib/styles/dropdown.d.ts +0 -12
  164. package/lib/styles/dropdown.d.ts.map +0 -1
  165. package/lib/styles/dropdown.js +0 -725
  166. package/lib/styles/dropdown.js.map +0 -1
  167. package/lib/styles/icon.d.ts.map +0 -1
  168. package/lib/styles/icon.js.map +0 -1
  169. package/lib/styles/input.d.ts.map +0 -1
  170. package/lib/styles/input.js.map +0 -1
  171. package/lib/styles/modal.d.ts.map +0 -1
  172. package/lib/styles/modal.js.map +0 -1
  173. package/lib/styles/multiselect.d.ts +0 -6
  174. package/lib/styles/multiselect.d.ts.map +0 -1
  175. package/lib/styles/multiselect.js +0 -568
  176. package/lib/styles/multiselect.js.map +0 -1
  177. package/lib/styles/option.d.ts.map +0 -1
  178. package/lib/styles/option.js.map +0 -1
  179. package/lib/styles/popup.d.ts.map +0 -1
  180. package/lib/styles/popup.js.map +0 -1
  181. package/lib/styles/resize-observer.d.ts.map +0 -1
  182. package/lib/styles/resize-observer.js.map +0 -1
  183. package/lib/styles/scroll-container.d.ts.map +0 -1
  184. package/lib/styles/scroll-container.js.map +0 -1
  185. package/lib/styles/step.d.ts.map +0 -1
  186. package/lib/styles/step.js.map +0 -1
  187. package/lib/styles/tab.d.ts.map +0 -1
  188. package/lib/styles/tab.js.map +0 -1
  189. package/lib/styles/tabs.d.ts.map +0 -1
  190. package/lib/styles/tabs.js.map +0 -1
  191. package/lib/styles/tag.d.ts.map +0 -1
  192. package/lib/styles/tag.js.map +0 -1
  193. package/lib/styles/textarea.d.ts.map +0 -1
  194. package/lib/styles/textarea.js.map +0 -1
  195. package/lib/styles/tooltip.d.ts.map +0 -1
  196. package/lib/styles/tooltip.js.map +0 -1
  197. package/lib/styles/wizard.d.ts.map +0 -1
  198. package/lib/styles/wizard.js.map +0 -1
  199. package/lib/types/common.d.ts.map +0 -1
  200. package/lib/types/common.js.map +0 -1
  201. package/lib/utils/calendar-utils.d.ts.map +0 -1
  202. package/lib/utils/calendar-utils.js.map +0 -1
  203. package/lib/utils/icon-registry.d.ts.map +0 -1
  204. package/lib/utils/icon-registry.js.map +0 -1
  205. package/lib/utils/locale.d.ts.map +0 -1
  206. package/lib/utils/locale.js.map +0 -1
  207. package/lib/utils/number-format.d.ts.map +0 -1
  208. package/lib/utils/number-format.js.map +0 -1
  209. package/lib/utils/parse-boolean.d.ts +0 -39
  210. package/lib/utils/parse-boolean.d.ts.map +0 -1
  211. package/lib/utils/parse-boolean.js +0 -23
  212. package/lib/utils/parse-boolean.js.map +0 -1
  213. package/lib/utils/positioning.d.ts.map +0 -1
  214. package/lib/utils/positioning.js.map +0 -1
  215. package/lib/utils/property-capture.d.ts.map +0 -1
  216. package/lib/utils/property-capture.js.map +0 -1
  217. package/lib/utils/property-manager.d.ts.map +0 -1
  218. package/lib/utils/property-manager.js.map +0 -1
  219. package/lib/utils/resize-observer.d.ts.map +0 -1
  220. package/lib/utils/resize-observer.js.map +0 -1
  221. package/lib/utils/scroll-lock.d.ts.map +0 -1
  222. package/lib/utils/scroll-lock.js.map +0 -1
  223. package/lib/utils/styles.d.ts.map +0 -1
  224. package/lib/utils/styles.js.map +0 -1
  225. package/lib/version.d.ts.map +0 -1
  226. package/lib/version.js.map +0 -1
@@ -1,6 +1,8 @@
1
1
  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
+ import { nativeMessage } from "../utils/validity.js";
5
+ const DEFAULT_PLACEHOLDER = "Drop files here or click to browse";
4
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>`;
5
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>`;
6
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>`;
@@ -52,7 +54,7 @@ this.updateValidity();
52
54
  updateValidity() {
53
55
  if (this.required && !this.disabled && this._files.length === 0) {
54
56
  const anchor = this.shadowRoot?.querySelector(".drop-zone");
55
- this.applyValidity({ valueMissing: true }, "Please select a file", anchor ?? undefined);
57
+ this.applyValidity({ valueMissing: true }, nativeMessage("input", { type: "file", required: true, multiple: this.multiple }), anchor ?? undefined);
56
58
  }
57
59
  else {
58
60
  this.applyValidity({});
@@ -380,9 +382,12 @@ const hintEl = container.querySelector(".upload-hint");
380
382
  if (this._files.length > 0) {
381
383
  hintEl.innerHTML = `<strong>${this._files.length} file${this._files.length !== 1 ? "s" : ""} selected</strong>`;
382
384
  }
383
- else {
385
+ else if (!this.placeholder || this.placeholder === DEFAULT_PLACEHOLDER) {
384
386
  hintEl.innerHTML = `Drop files here or <span class="browse-link">click to browse</span>`;
385
387
  }
388
+ else {
389
+ hintEl.textContent = this.placeholder;
390
+ }
386
391
  const subHintEl = container.querySelector(".upload-sub-hint");
387
392
  if (this.accept && this._files.length === 0) {
388
393
  subHintEl.textContent = `Accepted: ${this.accept}`;
@@ -433,7 +438,7 @@ default: "",
433
438
  placeholder: {
434
439
  type: "string",
435
440
  visual: true,
436
- default: "Drop files here or click to browse",
441
+ default: DEFAULT_PLACEHOLDER,
437
442
  },
438
443
  error: {
439
444
  type: "string",
@@ -137,10 +137,7 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
137
137
  };
138
138
  private _shadowValue;
139
139
  private _isFocused;
140
- /** The user typed since the field was created: blur then shows its validation message. */
141
- private _userEdited;
142
140
  private _listenersSetup;
143
- private _errorAutoDanger;
144
141
  private _customFlavorSheet;
145
142
  private _inputHandler;
146
143
  private _changeHandler;
@@ -235,10 +232,11 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
235
232
  */
236
233
  private setupPasswordToggle;
237
234
  /**
238
- * pattern and maxlength go on the inner <input>: pattern for the browser's
239
- * own patternMismatch check, maxlength so typing stops at the limit like a
235
+ * pattern, minlength and maxlength go on the inner <input>: pattern for the
236
+ * browser's own patternMismatch check, minlength for its tooShort message
237
+ * after the user types, maxlength so typing stops at the limit like a
240
238
  * native field. Formatted (numeric) types show a display string, so they
241
- * get neither.
239
+ * get none.
242
240
  */
243
241
  private syncConstraintAttributes;
244
242
  /**
@@ -6,6 +6,7 @@ import { inputStyles, inputCustomFlavorCss } from '../styles/input.js';
6
6
  import { formatNumber, parseNumericValue, shouldFormat as shouldFormatType } from '../utils/number-format.js';
7
7
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
8
8
  import { REQUIRED_ICON_SVG } from '../utils/icons.js';
9
+ import { nativeMessage, nativeValidity, lengthValidity, toLimit } from '../utils/validity.js';
9
10
  const EYE_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>`;
10
11
  const EYE_OFF_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>`;
11
12
  export class TyInput extends TyComponent {
@@ -13,9 +14,7 @@ constructor() {
13
14
  super();
14
15
  this._shadowValue = null;
15
16
  this._isFocused = false;
16
- this._userEdited = false;
17
17
  this._listenersSetup = false;
18
- this._errorAutoDanger = false;
19
18
  this._customFlavorSheet = null;
20
19
  this._inputHandler = null;
21
20
  this._changeHandler = null;
@@ -60,6 +59,10 @@ this._localeObserver = observeLocaleChanges(this, () => {
60
59
  this.render();
61
60
  });
62
61
  this._syncCustomFlavor();
62
+ if (this._rendered) {
63
+ this.render();
64
+ this.setupEventListeners();
65
+ }
63
66
  }
64
67
  _syncCustomFlavor() {
65
68
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.flavor, ({ base }) => inputCustomFlavorCss(base));
@@ -92,20 +95,6 @@ case 'type':
92
95
  const currentValue = this.getProperty('value') || '';
93
96
  this._shadowValue = this.parseShadowValue(currentValue);
94
97
  break;
95
- case 'error':
96
- if (newValue) {
97
- if (this.getProperty('flavor') === 'neutral') {
98
- this.setProperty('flavor', 'danger');
99
- this._errorAutoDanger = true;
100
- }
101
- }
102
- else if (this._errorAutoDanger) {
103
- if (this.getProperty('flavor') === 'danger') {
104
- this.setProperty('flavor', 'neutral');
105
- }
106
- this._errorAutoDanger = false;
107
- }
108
- break;
109
98
  }
110
99
  }
111
100
  }
@@ -393,6 +382,9 @@ const existingWrapper = shadow.querySelector('.input-wrapper');
393
382
  const existingLabel = shadow.querySelector('.ty-field-label');
394
383
  const existingError = shadow.querySelector('.error-message');
395
384
  const classes = this.buildClassList();
385
+ const focused = !!existingInput && shadow.activeElement === existingInput;
386
+ if (!focused)
387
+ this._isFocused = false;
396
388
  const inputType = ['password', 'email', 'tel', 'url'].includes(this.type)
397
389
  ? this.type
398
390
  : 'text';
@@ -403,7 +395,10 @@ const inputMode = shouldFormatType(this.type) ? 'decimal'
403
395
  : undefined;
404
396
  const displayValue = this.getDisplayValue();
405
397
  if (existingInput && existingWrapper) {
406
- existingInput.type = inputType;
398
+ const revealed = inputType === 'password' &&
399
+ shadow.querySelector('.password-toggle')?.getAttribute('aria-pressed') === 'true';
400
+ existingInput.type = revealed ? 'text' : inputType;
401
+ if (existingInput.value !== displayValue)
407
402
  existingInput.value = displayValue;
408
403
  existingInput.placeholder = this.placeholder;
409
404
  existingInput.name = this.name;
@@ -412,6 +407,7 @@ existingInput.inputMode = inputMode;
412
407
  else
413
408
  existingInput.removeAttribute('inputmode');
414
409
  existingWrapper.className = `input-wrapper ${classes}`;
410
+ existingWrapper.classList.toggle('focused', focused);
415
411
  existingInput.disabled = this.disabled;
416
412
  if (this.disabled) {
417
413
  existingInput.setAttribute('disabled', '');
@@ -517,7 +513,12 @@ return;
517
513
  toggle.addEventListener('mousedown', (e) => e.preventDefault());
518
514
  toggle.addEventListener('click', () => {
519
515
  const reveal = inputEl.type === 'password';
516
+ const { selectionStart, selectionEnd, selectionDirection } = inputEl;
520
517
  inputEl.type = reveal ? 'text' : 'password';
518
+ if (selectionStart !== null && shadow.activeElement === inputEl) {
519
+ inputEl.getBoundingClientRect();
520
+ inputEl.setSelectionRange(selectionStart, selectionEnd, selectionDirection ?? undefined);
521
+ }
521
522
  toggle.innerHTML = reveal ? EYE_OFF_ICON_SVG : EYE_ICON_SVG;
522
523
  toggle.setAttribute('aria-label', reveal ? 'Hide password' : 'Show password');
523
524
  toggle.setAttribute('aria-pressed', String(reveal));
@@ -527,6 +528,7 @@ syncConstraintAttributes(inputEl) {
527
528
  const text = !shouldFormatType(this.type);
528
529
  const set = (name, v) => text && v !== '' ? inputEl.setAttribute(name, v) : inputEl.removeAttribute(name);
529
530
  set('pattern', this.pattern);
531
+ set('minlength', toLimit(this.getProperty('minlength')) != null ? String(this.minLength) : '');
530
532
  set('maxlength', toLimit(this.getProperty('maxlength')) != null ? String(this.maxLength) : '');
531
533
  }
532
534
  updateValidity() {
@@ -538,7 +540,7 @@ return;
538
540
  const current = inputEl ? inputEl.value : String(this.getProperty('value') ?? '');
539
541
  if (current.trim() === '') {
540
542
  if (this.required) {
541
- this.applyValidity({ valueMissing: true }, nativeMessage('text', { required: '' }, ''), inputEl ?? undefined);
543
+ this.applyValidity({ valueMissing: true }, nativeMessage('input', { type: 'text', required: true }), inputEl ?? undefined);
542
544
  }
543
545
  else {
544
546
  this.applyValidity({});
@@ -553,29 +555,29 @@ messages.push(message);
553
555
  };
554
556
  if (shouldFormatType(this.type)) {
555
557
  const n = typeof this._shadowValue === 'number' ? this._shadowValue : null;
556
- const min = toNumber(this.min);
557
- const max = toNumber(this.max);
558
- const range = (flag) => fail(flag, nativeMessage('number', { step: 'any', min: this.min, max: this.max }, String(n)));
559
- if (n !== null && min !== null && n < min)
560
- range('rangeUnderflow');
561
- if (n !== null && max !== null && n > max)
562
- range('rangeOverflow');
558
+ if (n !== null && (this.min || this.max)) {
559
+ const { validity, message } = nativeValidity('input', { type: 'number', step: 'any', min: this.min, max: this.max }, String(n));
560
+ if (validity.rangeUnderflow)
561
+ fail('rangeUnderflow', message);
562
+ if (validity.rangeOverflow)
563
+ fail('rangeOverflow', message);
564
+ }
563
565
  }
564
566
  else {
565
567
  if (inputEl?.validity.typeMismatch)
566
568
  fail('typeMismatch', inputEl.validationMessage);
567
569
  if (inputEl?.validity.patternMismatch)
568
570
  fail('patternMismatch', inputEl.validationMessage);
569
- const length = current.length;
570
- const minLength = toLimit(this.getProperty('minlength'));
571
- const maxLength = toLimit(this.getProperty('maxlength'));
572
- const vars = { minlength: minLength, maxlength: maxLength, length };
573
- if (minLength !== null && length < minLength) {
574
- fail('tooShort', fillMessage(this.tooShortMessage || 'Use at least {minlength} characters (now {length})', vars));
575
- }
576
- if (maxLength !== null && length > maxLength) {
577
- fail('tooLong', fillMessage(this.tooLongMessage || 'Use at most {maxlength} characters (now {length})', vars));
578
- }
571
+ const { tooShort, tooLong } = lengthValidity(current, {
572
+ minlength: this.getProperty('minlength'),
573
+ maxlength: this.getProperty('maxlength'),
574
+ tooShortMessage: this.tooShortMessage,
575
+ tooLongMessage: this.tooLongMessage,
576
+ }, inputEl);
577
+ if (tooShort)
578
+ fail('tooShort', tooShort);
579
+ if (tooLong)
580
+ fail('tooLong', tooLong);
579
581
  }
580
582
  this.applyValidity(flags, messages[0], inputEl ?? undefined);
581
583
  }
@@ -737,33 +739,6 @@ return Math.max(0, Math.min(5000, Math.floor(num)));
737
739
  }
738
740
  }
739
741
  };
740
- let probe = null;
741
- function nativeMessage(type, attrs, value) {
742
- probe ?? (probe = document.createElement('input'));
743
- for (const name of Array.from(probe.getAttributeNames()))
744
- probe.removeAttribute(name);
745
- probe.type = type;
746
- for (const [name, v] of Object.entries(attrs))
747
- if (v !== '' || name === 'required')
748
- probe.setAttribute(name, v);
749
- probe.value = value;
750
- return probe.validationMessage;
751
- }
752
- function fillMessage(template, vars) {
753
- return template.replace(/\{(\w+)\}/g, (match, key) => vars[key] != null ? String(vars[key]) : match);
754
- }
755
- function toLimit(v) {
756
- if (v === null || v === undefined || String(v).trim() === '')
757
- return null;
758
- const n = Number(v);
759
- return Number.isInteger(n) && n >= 0 ? n : null;
760
- }
761
- function toNumber(v) {
762
- if (v === null || v === undefined || String(v).trim() === '')
763
- return null;
764
- const n = Number(v);
765
- return Number.isFinite(n) ? n : null;
766
- }
767
742
  if (!customElements.get('ty-input')) {
768
743
  customElements.define('ty-input', TyInput);
769
744
  }
@@ -1,6 +1,7 @@
1
1
  import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
2
2
  import { coerceBoolean } from '../utils/property-manager.js';
3
3
  import { ensureStyles } from '../utils/styles.js';
4
+ import { upgradeProperties } from '../utils/upgrade-properties.js';
4
5
  import { modalStyles } from '../styles/modal.js';
5
6
  const backdropClickHandlers = new WeakMap();
6
7
  const escapeKeyHandlers = new WeakMap();
@@ -9,6 +10,13 @@ const hoverEnterHandlers = new WeakMap();
9
10
  const hoverLeaveHandlers = new WeakMap();
10
11
  const modalIds = new WeakMap();
11
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
+ }
12
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>`;
13
21
  function parseBoolAttr(el, name) {
14
22
  if (!el.hasAttribute(name)) {
@@ -98,13 +106,72 @@ event.preventDefault();
98
106
  closeModal(el, 'backdrop');
99
107
  }
100
108
  }
109
+ function fromNestedDialog(dialog, event) {
110
+ for (const node of event.composedPath()) {
111
+ if (node === dialog)
112
+ return false;
113
+ if (node instanceof HTMLDialogElement && node.matches(':modal'))
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ function closeNestedDialogs(root) {
119
+ walkNested(root, (node) => {
120
+ if (node instanceof TyModal && node.open)
121
+ node.removeAttribute('open');
122
+ if (node instanceof HTMLDialogElement && node.matches(':modal'))
123
+ node.close();
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;
139
+ if (node instanceof HTMLSlotElement) {
140
+ for (const assigned of node.assignedElements({ flatten: true })) {
141
+ if (walk(assigned) || walkNested(assigned, visit, seen))
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ };
147
+ return Array.from(root.querySelectorAll('*')).some(walk);
148
+ }
101
149
  function handleEscapeKey(el, event) {
150
+ const dialog = event.currentTarget;
151
+ if (fromNestedDialog(dialog, event))
152
+ return;
102
153
  event.stopPropagation();
103
154
  if (event.key === 'Escape') {
104
155
  event.preventDefault();
105
156
  closeModal(el, 'escape');
106
157
  }
107
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);
108
175
  function handleCloseButtonClick(el, event) {
109
176
  event.preventDefault();
110
177
  event.stopPropagation();
@@ -155,7 +222,7 @@ dialog.addEventListener('keydown', handler);
155
222
  }
156
223
  else {
157
224
  const handler = (e) => {
158
- if (e.key === 'Escape')
225
+ if (e.key === 'Escape' && !fromNestedDialog(dialog, e))
159
226
  e.preventDefault();
160
227
  };
161
228
  escapeKeyHandlers.set(el, handler);
@@ -240,19 +307,20 @@ setupCloseButtonHover(el, dialog);
240
307
  if (attributes.open) {
241
308
  if (!dialog.open) {
242
309
  lockScroll(modalId);
243
- if (attributes.backdrop) {
244
310
  dialog.showModal();
245
- }
246
- else {
247
- dialog.show();
248
- }
311
+ dropFromStack(el);
312
+ openStack.push(el);
249
313
  syncBackdropZoom();
314
+ if (!detachedOpen.delete(el))
250
315
  dispatchModalEvent(el, 'open', {});
251
316
  }
252
317
  }
253
318
  else {
319
+ detachedOpen.delete(el);
320
+ dropFromStack(el);
254
321
  if (dialog.open) {
255
322
  unlockScroll(modalId);
323
+ closeNestedDialogs(el);
256
324
  dialog.close();
257
325
  syncBackdropZoom();
258
326
  const reason = pendingCloseReasons.get(el) ?? 'programmatic';
@@ -262,9 +330,11 @@ dispatchModalEvent(el, 'close', { reason });
262
330
  }
263
331
  syncBackdropZoom();
264
332
  dialog.onclose = (event) => {
265
- if (event.target !== dialog)
333
+ if (event.target !== dialog || dialog.open || !el.isConnected)
266
334
  return;
267
335
  unlockScroll(modalId);
336
+ dropFromStack(el);
337
+ closeNestedDialogs(el);
268
338
  if (el.hasAttribute('open')) {
269
339
  el.removeAttribute('open');
270
340
  dispatchModalEvent(el, 'close', {
@@ -276,13 +346,15 @@ syncBackdropZoom();
276
346
  };
277
347
  }
278
348
  function syncBackdropZoom() {
279
- const anyOpen = Array.from(document.querySelectorAll('ty-modal[open], ty-dialog[open]')).some((m) => parseBoolAttr(m, 'open') && !parseBoolAttr(m, 'no-zoom'));
280
- document.documentElement.classList.toggle('ty-modal-zoom', anyOpen);
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);
281
352
  }
282
353
  function cleanup(el) {
283
354
  const modalId = modalIds.get(el);
284
355
  const shadowRoot = el.shadowRoot;
285
356
  const dialog = shadowRoot ? getModalDialog(shadowRoot) : null;
357
+ dropFromStack(el);
286
358
  if (modalId) {
287
359
  unlockScroll(modalId);
288
360
  modalIds.delete(el);
@@ -290,6 +362,10 @@ modalIds.delete(el);
290
362
  syncBackdropZoom();
291
363
  if (!dialog)
292
364
  return;
365
+ if (dialog.open) {
366
+ dialog.close();
367
+ detachedOpen.add(el);
368
+ }
293
369
  const backdropHandler = backdropClickHandlers.get(el);
294
370
  if (backdropHandler) {
295
371
  dialog.removeEventListener('click', backdropHandler);
@@ -349,6 +425,7 @@ super();
349
425
  this.attachShadow({ mode: 'open' });
350
426
  }
351
427
  connectedCallback() {
428
+ upgradeProperties(this);
352
429
  render(this);
353
430
  this.show = () => openModal(this);
354
431
  this.hide = (opts) => closeModal(this, 'programmatic', opts);
@@ -1,5 +1,6 @@
1
1
  import { ensureStyles } from '../utils/styles.js';
2
2
  import { coerceBoolean } from '../utils/property-manager.js';
3
+ import { upgradeProperties } from '../utils/upgrade-properties.js';
3
4
  import { optionStyles } from '../styles/option.js';
4
5
  const CHECK_SVG = `
5
6
  <svg class="option-check" viewBox="0 0 24 24" fill="none" stroke="currentColor"
@@ -23,11 +24,7 @@ static get observedAttributes() {
23
24
  return ['value', 'disabled', 'selected', 'highlighted', 'hidden'];
24
25
  }
25
26
  connectedCallback() {
26
- const instanceValue = Object.getOwnPropertyDescriptor(this, 'value');
27
- if (instanceValue) {
28
- this._value = instanceValue.value;
29
- delete this.value;
30
- }
27
+ upgradeProperties(this);
31
28
  this.render();
32
29
  }
33
30
  attributeChangedCallback(name, oldValue, newValue) {
@@ -9,6 +9,9 @@ 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();
14
+ const pointerPresses = new WeakSet();
12
15
  function parseBoolAttr(el, name) {
13
16
  return el.hasAttribute(name);
14
17
  }
@@ -87,29 +90,45 @@ dialog.close();
87
90
  function openPopup(el) {
88
91
  const shadowRoot = el.shadowRoot;
89
92
  const dialog = shadowRoot ? getPopupDialog(shadowRoot) : null;
90
- if (!dialog)
93
+ if (!dialog || !el.isConnected)
91
94
  return;
95
+ const anchor = getAnchorElement(el);
92
96
  if (dialog.open) {
93
97
  const pending = closeTimers.get(el);
94
98
  if (pending !== undefined) {
95
99
  clearTimeout(pending);
96
100
  closeTimers.delete(el);
97
101
  dialog.classList.add('open');
102
+ if (anchor?.hasAttribute('aria-haspopup'))
103
+ anchor.setAttribute('aria-expanded', 'true');
98
104
  }
99
105
  return;
100
106
  }
101
107
  const popupId = getPopupId(el);
102
108
  lockScroll(popupId);
103
109
  dialog.showModal();
104
- const anchor = getAnchorElement(el);
110
+ guardEscape(el);
105
111
  if (anchor?.hasAttribute('aria-haspopup'))
106
112
  anchor.setAttribute('aria-expanded', 'true');
107
113
  requestAnimationFrame(() => {
108
114
  updatePosition(el);
109
115
  dialog.classList.add('preparing-animation');
110
- void dialog.offsetWidth;
116
+ dialog.getBoundingClientRect();
111
117
  requestAnimationFrame(() => {
112
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
+ }
113
132
  el.dispatchEvent(new CustomEvent('open'));
114
133
  });
115
134
  });
@@ -140,7 +159,36 @@ event.preventDefault();
140
159
  closePopup(el);
141
160
  }
142
161
  }
162
+ function fromNestedDialog(el, event) {
163
+ const dialog = el.shadowRoot ? getPopupDialog(el.shadowRoot) : null;
164
+ for (const node of event.composedPath()) {
165
+ if (node === dialog)
166
+ return false;
167
+ if (node instanceof HTMLDialogElement && node.matches(':modal'))
168
+ return true;
169
+ }
170
+ return false;
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
+ }
143
189
  function handleEscape(el, event) {
190
+ if (fromNestedDialog(el, event))
191
+ return;
144
192
  event.stopPropagation();
145
193
  if (event.key === 'Escape') {
146
194
  const { disableClose } = getPopupAttributes(el);
@@ -164,26 +212,39 @@ return;
164
212
  anchor.setAttribute('aria-haspopup', 'dialog');
165
213
  if (!anchor.hasAttribute('aria-expanded'))
166
214
  anchor.setAttribute('aria-expanded', 'false');
215
+ const previous = anchors.get(el) ?? anchor;
167
216
  const existingHandler = anchorClickHandlers.get(el);
168
217
  if (existingHandler) {
169
- anchor.removeEventListener('pointerdown', existingHandler);
218
+ previous.removeEventListener('pointerdown', existingHandler);
170
219
  }
171
220
  const existingKeyHandler = anchorKeyClickHandlers.get(el);
172
221
  if (existingKeyHandler) {
173
- anchor.removeEventListener('click', existingKeyHandler);
222
+ previous.removeEventListener('click', existingKeyHandler);
174
223
  }
175
- const handler = (e) => handleAnchorClick(el, e);
224
+ anchors.set(el, anchor);
225
+ const handler = (e) => {
226
+ pointerPresses.add(el);
227
+ handleAnchorClick(el, e);
228
+ };
176
229
  anchorClickHandlers.set(el, handler);
177
230
  anchor.addEventListener('pointerdown', handler);
178
231
  const keyHandler = (e) => {
179
- if (e.detail === 0)
232
+ if (pointerPresses.delete(el))
233
+ return;
234
+ const d = e.detail;
235
+ const click = typeof d === 'number' ? e : d?.originalEvent ?? e;
236
+ if (click.detail !== 0)
237
+ return;
238
+ if (click.composedPath().includes(el))
239
+ return;
180
240
  handleAnchorClick(el, e);
181
241
  };
182
242
  anchorKeyClickHandlers.set(el, keyHandler);
183
243
  anchor.addEventListener('click', keyHandler);
184
244
  }
185
245
  function cleanupAnchorEvents(el) {
186
- const anchor = getAnchorElement(el);
246
+ const anchor = anchors.get(el);
247
+ anchors.delete(el);
187
248
  const handler = anchorClickHandlers.get(el);
188
249
  if (anchor && handler) {
189
250
  anchor.removeEventListener('pointerdown', handler);
@@ -208,6 +269,7 @@ const dialog = document.createElement('dialog');
208
269
  const container = document.createElement('div');
209
270
  const content = document.createElement('slot');
210
271
  dialog.className = 'popup-dialog';
272
+ dialog.tabIndex = -1;
211
273
  container.className = 'popup-container';
212
274
  content.id = 'popup-content';
213
275
  shadowRoot.appendChild(dialog);
@@ -221,17 +283,40 @@ dialog.addEventListener('pointerdown', outsideHandler);
221
283
  const escapeHandler = (e) => handleEscape(el, e);
222
284
  escapeHandlers.set(el, escapeHandler);
223
285
  el.addEventListener('keydown', escapeHandler);
224
- dialog.addEventListener('close', () => {
225
- const popupId = getPopupId(el);
226
- unlockScroll(popupId);
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
+ });
296
+ new MutationObserver(() => {
297
+ if (dialog.open)
298
+ return;
299
+ clearTimeout(closeTimers.get(el));
300
+ closeTimers.delete(el);
301
+ pointerPresses.delete(el);
302
+ unguardEscape(el);
303
+ unlockScroll(getPopupId(el));
227
304
  dialog.classList.remove('open');
228
305
  dialog.classList.remove('preparing-animation');
306
+ const anchor = getAnchorElement(el);
307
+ if (anchor?.hasAttribute('aria-haspopup'))
308
+ anchor.setAttribute('aria-expanded', 'false');
309
+ }).observe(dialog, { attributes: true, attributeFilter: ['open'] });
310
+ dialog.addEventListener('close', () => {
229
311
  el.dispatchEvent(new CustomEvent('close'));
230
312
  });
231
313
  }
232
314
  el.closePopup = () => closePopup(el, true);
233
315
  el.openPopup = () => openPopup(el);
234
316
  el.togglePopup = () => togglePopup(el);
317
+ const existingCloseRequest = closeRequestHandlers.get(el);
318
+ if (existingCloseRequest)
319
+ el.removeEventListener('ty:close-popup', existingCloseRequest);
235
320
  const closeRequestHandler = (e) => handleCloseRequest(el, e);
236
321
  closeRequestHandlers.set(el, closeRequestHandler);
237
322
  el.addEventListener('ty:close-popup', closeRequestHandler);
@@ -252,12 +337,15 @@ if (outsideHandler) {
252
337
  dialog.removeEventListener('pointerdown', outsideHandler);
253
338
  outsideClickHandlers.delete(el);
254
339
  }
340
+ if (dialog.open)
341
+ dialog.close();
255
342
  }
256
343
  const escapeHandler = escapeHandlers.get(el);
257
344
  if (escapeHandler) {
258
345
  el.removeEventListener('keydown', escapeHandler);
259
346
  escapeHandlers.delete(el);
260
347
  }
348
+ unguardEscape(el);
261
349
  const closeRequestHandler = closeRequestHandlers.get(el);
262
350
  if (closeRequestHandler) {
263
351
  el.removeEventListener('ty:close-popup', closeRequestHandler);