tyrell-components 1.0.0-TC57 → 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 (200) hide show
  1. package/dist/tyrell.js +1 -1
  2. package/dist/tyrell.js.map +1 -0
  3. package/lib/base/ty-component.d.ts +12 -3
  4. package/lib/base/ty-component.js +21 -17
  5. package/lib/components/button.d.ts +4 -0
  6. package/lib/components/button.js +33 -6
  7. package/lib/components/calendar-month.js +3 -10
  8. package/lib/components/calendar.d.ts +3 -1
  9. package/lib/components/calendar.js +19 -34
  10. package/lib/components/checkbox.d.ts +1 -1
  11. package/lib/components/checkbox.js +2 -1
  12. package/lib/components/date-picker.d.ts +60 -8
  13. package/lib/components/date-picker.js +268 -134
  14. package/lib/components/file-upload.d.ts +2 -1
  15. package/lib/components/file-upload.js +2 -1
  16. package/lib/components/input.d.ts +13 -6
  17. package/lib/components/input.js +58 -60
  18. package/lib/components/modal.js +37 -1
  19. package/lib/components/option.js +2 -5
  20. package/lib/components/popup.js +43 -6
  21. package/lib/components/radio.d.ts +2 -1
  22. package/lib/components/radio.js +2 -1
  23. package/lib/components/resize-observer.js +2 -0
  24. package/lib/components/scroll-container.js +2 -0
  25. package/lib/components/select.d.ts +32 -3
  26. package/lib/components/select.js +123 -11
  27. package/lib/components/switch.d.ts +3 -1
  28. package/lib/components/switch.js +2 -1
  29. package/lib/components/textarea.d.ts +44 -4
  30. package/lib/components/textarea.js +54 -10
  31. package/lib/styles/button.js +20 -19
  32. package/lib/styles/date-picker.js +17 -0
  33. package/lib/styles/select-base.js +26 -0
  34. package/lib/styles/textarea.d.ts +1 -1
  35. package/lib/styles/textarea.js +7 -0
  36. package/lib/utils/error-flavor.d.ts +8 -0
  37. package/lib/utils/error-flavor.js +1 -0
  38. package/lib/utils/upgrade-properties.d.ts +21 -0
  39. package/lib/utils/upgrade-properties.js +47 -0
  40. package/lib/utils/validity.d.ts +63 -5
  41. package/lib/utils/validity.js +145 -24
  42. package/lib/version.d.ts +1 -1
  43. package/lib/version.js +1 -1
  44. package/package.json +2 -1
  45. package/lib/base/ty-component.d.ts.map +0 -1
  46. package/lib/base/ty-component.js.map +0 -1
  47. package/lib/components/button.d.ts.map +0 -1
  48. package/lib/components/button.js.map +0 -1
  49. package/lib/components/calendar-month.d.ts.map +0 -1
  50. package/lib/components/calendar-month.js.map +0 -1
  51. package/lib/components/calendar-navigation.d.ts.map +0 -1
  52. package/lib/components/calendar-navigation.js.map +0 -1
  53. package/lib/components/calendar.d.ts.map +0 -1
  54. package/lib/components/calendar.js.map +0 -1
  55. package/lib/components/checkbox.d.ts.map +0 -1
  56. package/lib/components/checkbox.js.map +0 -1
  57. package/lib/components/copy.d.ts.map +0 -1
  58. package/lib/components/copy.js.map +0 -1
  59. package/lib/components/date-picker.d.ts.map +0 -1
  60. package/lib/components/date-picker.js.map +0 -1
  61. package/lib/components/dropdown.d.ts +0 -403
  62. package/lib/components/dropdown.d.ts.map +0 -1
  63. package/lib/components/dropdown.js +0 -1315
  64. package/lib/components/dropdown.js.map +0 -1
  65. package/lib/components/icon.d.ts.map +0 -1
  66. package/lib/components/icon.js.map +0 -1
  67. package/lib/components/input.d.ts.map +0 -1
  68. package/lib/components/input.js.map +0 -1
  69. package/lib/components/modal.d.ts.map +0 -1
  70. package/lib/components/modal.js.map +0 -1
  71. package/lib/components/multiselect.d.ts +0 -389
  72. package/lib/components/multiselect.d.ts.map +0 -1
  73. package/lib/components/multiselect.js +0 -1301
  74. package/lib/components/multiselect.js.map +0 -1
  75. package/lib/components/option.d.ts.map +0 -1
  76. package/lib/components/option.js.map +0 -1
  77. package/lib/components/popup.d.ts.map +0 -1
  78. package/lib/components/popup.js.map +0 -1
  79. package/lib/components/resize-observer.d.ts.map +0 -1
  80. package/lib/components/resize-observer.js.map +0 -1
  81. package/lib/components/scroll-container.d.ts.map +0 -1
  82. package/lib/components/scroll-container.js.map +0 -1
  83. package/lib/components/step.d.ts.map +0 -1
  84. package/lib/components/step.js.map +0 -1
  85. package/lib/components/tab.d.ts.map +0 -1
  86. package/lib/components/tab.js.map +0 -1
  87. package/lib/components/tabs.d.ts.map +0 -1
  88. package/lib/components/tabs.js.map +0 -1
  89. package/lib/components/tag.d.ts.map +0 -1
  90. package/lib/components/tag.js.map +0 -1
  91. package/lib/components/textarea.d.ts.map +0 -1
  92. package/lib/components/textarea.js.map +0 -1
  93. package/lib/components/tooltip.d.ts.map +0 -1
  94. package/lib/components/tooltip.js.map +0 -1
  95. package/lib/components/wizard.d.ts.map +0 -1
  96. package/lib/components/wizard.js.map +0 -1
  97. package/lib/icons/fontawesome/brands.d.ts.map +0 -1
  98. package/lib/icons/fontawesome/brands.js.map +0 -1
  99. package/lib/icons/fontawesome/regular.d.ts.map +0 -1
  100. package/lib/icons/fontawesome/regular.js.map +0 -1
  101. package/lib/icons/fontawesome/solid.d.ts.map +0 -1
  102. package/lib/icons/fontawesome/solid.js.map +0 -1
  103. package/lib/icons/heroicons/micro.d.ts.map +0 -1
  104. package/lib/icons/heroicons/micro.js.map +0 -1
  105. package/lib/icons/heroicons/mini.d.ts.map +0 -1
  106. package/lib/icons/heroicons/mini.js.map +0 -1
  107. package/lib/icons/heroicons/outline.d.ts.map +0 -1
  108. package/lib/icons/heroicons/outline.js.map +0 -1
  109. package/lib/icons/heroicons/solid.d.ts.map +0 -1
  110. package/lib/icons/heroicons/solid.js.map +0 -1
  111. package/lib/icons/lucide.d.ts.map +0 -1
  112. package/lib/icons/lucide.js.map +0 -1
  113. package/lib/icons/material/filled.d.ts.map +0 -1
  114. package/lib/icons/material/filled.js.map +0 -1
  115. package/lib/icons/material/outlined.d.ts.map +0 -1
  116. package/lib/icons/material/outlined.js.map +0 -1
  117. package/lib/icons/material/round.d.ts.map +0 -1
  118. package/lib/icons/material/round.js.map +0 -1
  119. package/lib/icons/material/sharp.d.ts.map +0 -1
  120. package/lib/icons/material/sharp.js.map +0 -1
  121. package/lib/icons/material/two-tone.d.ts.map +0 -1
  122. package/lib/icons/material/two-tone.js.map +0 -1
  123. package/lib/index.d.ts.map +0 -1
  124. package/lib/index.js.map +0 -1
  125. package/lib/styles/button.d.ts.map +0 -1
  126. package/lib/styles/button.js.map +0 -1
  127. package/lib/styles/calendar-month.d.ts.map +0 -1
  128. package/lib/styles/calendar-month.js.map +0 -1
  129. package/lib/styles/calendar-navigation.d.ts.map +0 -1
  130. package/lib/styles/calendar-navigation.js.map +0 -1
  131. package/lib/styles/calendar.d.ts.map +0 -1
  132. package/lib/styles/calendar.js.map +0 -1
  133. package/lib/styles/copy.d.ts.map +0 -1
  134. package/lib/styles/copy.js.map +0 -1
  135. package/lib/styles/date-picker.d.ts.map +0 -1
  136. package/lib/styles/date-picker.js.map +0 -1
  137. package/lib/styles/dropdown.d.ts +0 -12
  138. package/lib/styles/dropdown.d.ts.map +0 -1
  139. package/lib/styles/dropdown.js +0 -725
  140. package/lib/styles/dropdown.js.map +0 -1
  141. package/lib/styles/icon.d.ts.map +0 -1
  142. package/lib/styles/icon.js.map +0 -1
  143. package/lib/styles/input.d.ts.map +0 -1
  144. package/lib/styles/input.js.map +0 -1
  145. package/lib/styles/modal.d.ts.map +0 -1
  146. package/lib/styles/modal.js.map +0 -1
  147. package/lib/styles/multiselect.d.ts +0 -6
  148. package/lib/styles/multiselect.d.ts.map +0 -1
  149. package/lib/styles/multiselect.js +0 -568
  150. package/lib/styles/multiselect.js.map +0 -1
  151. package/lib/styles/option.d.ts.map +0 -1
  152. package/lib/styles/option.js.map +0 -1
  153. package/lib/styles/popup.d.ts.map +0 -1
  154. package/lib/styles/popup.js.map +0 -1
  155. package/lib/styles/resize-observer.d.ts.map +0 -1
  156. package/lib/styles/resize-observer.js.map +0 -1
  157. package/lib/styles/scroll-container.d.ts.map +0 -1
  158. package/lib/styles/scroll-container.js.map +0 -1
  159. package/lib/styles/step.d.ts.map +0 -1
  160. package/lib/styles/step.js.map +0 -1
  161. package/lib/styles/tab.d.ts.map +0 -1
  162. package/lib/styles/tab.js.map +0 -1
  163. package/lib/styles/tabs.d.ts.map +0 -1
  164. package/lib/styles/tabs.js.map +0 -1
  165. package/lib/styles/tag.d.ts.map +0 -1
  166. package/lib/styles/tag.js.map +0 -1
  167. package/lib/styles/textarea.d.ts.map +0 -1
  168. package/lib/styles/textarea.js.map +0 -1
  169. package/lib/styles/tooltip.d.ts.map +0 -1
  170. package/lib/styles/tooltip.js.map +0 -1
  171. package/lib/styles/wizard.d.ts.map +0 -1
  172. package/lib/styles/wizard.js.map +0 -1
  173. package/lib/types/common.d.ts.map +0 -1
  174. package/lib/types/common.js.map +0 -1
  175. package/lib/utils/calendar-utils.d.ts.map +0 -1
  176. package/lib/utils/calendar-utils.js.map +0 -1
  177. package/lib/utils/icon-registry.d.ts.map +0 -1
  178. package/lib/utils/icon-registry.js.map +0 -1
  179. package/lib/utils/locale.d.ts.map +0 -1
  180. package/lib/utils/locale.js.map +0 -1
  181. package/lib/utils/number-format.d.ts.map +0 -1
  182. package/lib/utils/number-format.js.map +0 -1
  183. package/lib/utils/parse-boolean.d.ts +0 -39
  184. package/lib/utils/parse-boolean.d.ts.map +0 -1
  185. package/lib/utils/parse-boolean.js +0 -23
  186. package/lib/utils/parse-boolean.js.map +0 -1
  187. package/lib/utils/positioning.d.ts.map +0 -1
  188. package/lib/utils/positioning.js.map +0 -1
  189. package/lib/utils/property-capture.d.ts.map +0 -1
  190. package/lib/utils/property-capture.js.map +0 -1
  191. package/lib/utils/property-manager.d.ts.map +0 -1
  192. package/lib/utils/property-manager.js.map +0 -1
  193. package/lib/utils/resize-observer.d.ts.map +0 -1
  194. package/lib/utils/resize-observer.js.map +0 -1
  195. package/lib/utils/scroll-lock.d.ts.map +0 -1
  196. package/lib/utils/scroll-lock.js.map +0 -1
  197. package/lib/utils/styles.d.ts.map +0 -1
  198. package/lib/utils/styles.js.map +0 -1
  199. package/lib/version.d.ts.map +0 -1
  200. package/lib/version.js.map +0 -1
@@ -81,7 +81,8 @@ export declare class TyFileUpload extends TyComponent<FileUploadState> implement
81
81
  protected onFormReset(): void;
82
82
  protected updateFormValue(): void;
83
83
  /** Report `required` to the owning form, like ty-input and ty-textarea
84
- do — without this the asterisk showed but submission went through. */
84
+ do — without this the asterisk showed but submission went through.
85
+ The message is a native file input's. */
85
86
  private updateValidity;
86
87
  protected getFormValue(): FormData | string | null;
87
88
  /** Focus the drop zone: the host itself isn't focusable. */
@@ -1,6 +1,7 @@
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";
4
5
  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
6
  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
7
  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 +53,7 @@ this.updateValidity();
52
53
  updateValidity() {
53
54
  if (this.required && !this.disabled && this._files.length === 0) {
54
55
  const anchor = this.shadowRoot?.querySelector(".drop-zone");
55
- this.applyValidity({ valueMissing: true }, "Please select a file", anchor ?? undefined);
56
+ this.applyValidity({ valueMissing: true }, nativeMessage("input", { type: "file", required: true, multiple: this.multiple }), anchor ?? undefined);
56
57
  }
57
58
  else {
58
59
  this.applyValidity({});
@@ -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;
@@ -154,6 +151,14 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
154
151
  protected onConnect(): void;
155
152
  /** Custom (non-built-in) flavors — see utils/flavor-sheet.ts. */
156
153
  private _syncCustomFlavor;
154
+ /**
155
+ * `error` turns a neutral field danger; clearing it (the author, or
156
+ * validation dropping the message it showed) puts back exactly the flavor
157
+ * attribute the author had, or none, kept meanwhile in ERROR_FLAVOR (same
158
+ * as ty-select and ty-date-picker). Runs once connected: while the element
159
+ * upgrades, `error` may come before the `flavor` written next to it.
160
+ */
161
+ private syncErrorFlavor;
157
162
  protected onDisconnect(): void;
158
163
  /** Handle property changes — called BEFORE render. */
159
164
  protected onPropertiesChanged(changes: PropertyChange[]): void;
@@ -204,6 +209,7 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
204
209
  set size(v: Size);
205
210
  get flavor(): Flavor;
206
211
  set flavor(v: Flavor);
212
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
207
213
  get currency(): string;
208
214
  set currency(v: string);
209
215
  get locale(): string;
@@ -235,10 +241,11 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
235
241
  */
236
242
  private setupPasswordToggle;
237
243
  /**
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
244
+ * pattern, minlength and maxlength go on the inner <input>: pattern for the
245
+ * browser's own patternMismatch check, minlength for its tooShort message
246
+ * after the user types, maxlength so typing stops at the limit like a
240
247
  * native field. Formatted (numeric) types show a display string, so they
241
- * get neither.
248
+ * get none.
242
249
  */
243
250
  private syncConstraintAttributes;
244
251
  /**
@@ -2,10 +2,12 @@ import { DEFAULT_SIZE } from '../types/common.js';
2
2
  import { TyComponent } from '../base/ty-component.js';
3
3
  import { ensureStyles } from '../utils/styles.js';
4
4
  import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
5
+ import { ERROR_FLAVOR } from '../utils/error-flavor.js';
5
6
  import { inputStyles, inputCustomFlavorCss } from '../styles/input.js';
6
7
  import { formatNumber, parseNumericValue, shouldFormat as shouldFormatType } from '../utils/number-format.js';
7
8
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
8
9
  import { REQUIRED_ICON_SVG } from '../utils/icons.js';
10
+ import { nativeMessage, nativeValidity, lengthValidity, toLimit } from '../utils/validity.js';
9
11
  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
12
  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
13
  export class TyInput extends TyComponent {
@@ -13,9 +15,7 @@ constructor() {
13
15
  super();
14
16
  this._shadowValue = null;
15
17
  this._isFocused = false;
16
- this._userEdited = false;
17
18
  this._listenersSetup = false;
18
- this._errorAutoDanger = false;
19
19
  this._customFlavorSheet = null;
20
20
  this._inputHandler = null;
21
21
  this._changeHandler = null;
@@ -60,10 +60,32 @@ this._localeObserver = observeLocaleChanges(this, () => {
60
60
  this.render();
61
61
  });
62
62
  this._syncCustomFlavor();
63
+ this.syncErrorFlavor();
63
64
  }
64
65
  _syncCustomFlavor() {
65
66
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.flavor, ({ base }) => inputCustomFlavorCss(base));
66
67
  }
68
+ syncErrorFlavor() {
69
+ if (!this._connected)
70
+ return;
71
+ const flavor = this.getProperty('flavor');
72
+ const replaced = this.getAttribute(ERROR_FLAVOR);
73
+ if (this.getProperty('error')) {
74
+ if (replaced === null && flavor === 'neutral') {
75
+ this.setAttribute(ERROR_FLAVOR, this.getAttribute('flavor') ?? '');
76
+ this.setProperty('flavor', 'danger');
77
+ }
78
+ }
79
+ else if (replaced !== null) {
80
+ this.removeAttribute(ERROR_FLAVOR);
81
+ if (flavor !== 'danger')
82
+ return;
83
+ if (!replaced)
84
+ this.removeAttribute('flavor');
85
+ else
86
+ this.setProperty('flavor', replaced);
87
+ }
88
+ }
67
89
  onDisconnect() {
68
90
  this.removeEventListeners();
69
91
  if (this._localeObserver) {
@@ -83,6 +105,10 @@ onPropertiesChanged(changes) {
83
105
  for (const { name, newValue } of changes) {
84
106
  switch (name) {
85
107
  case 'flavor':
108
+ if (newValue !== 'danger') {
109
+ this.removeAttribute(ERROR_FLAVOR);
110
+ this.syncErrorFlavor();
111
+ }
86
112
  this._syncCustomFlavor();
87
113
  break;
88
114
  case 'value':
@@ -93,18 +119,7 @@ const currentValue = this.getProperty('value') || '';
93
119
  this._shadowValue = this.parseShadowValue(currentValue);
94
120
  break;
95
121
  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
- }
122
+ this.syncErrorFlavor();
108
123
  break;
109
124
  }
110
125
  }
@@ -211,7 +226,15 @@ set error(v) { this.setProperty('error', v); }
211
226
  get size() { return this.getProperty('size'); }
212
227
  set size(v) { this.setProperty('size', v); }
213
228
  get flavor() { return this.getProperty('flavor'); }
214
- set flavor(v) { this.setProperty('flavor', v); }
229
+ set flavor(v) {
230
+ this.removeAttribute(ERROR_FLAVOR);
231
+ this.setProperty('flavor', v);
232
+ }
233
+ attributeChangedCallback(name, oldValue, newValue) {
234
+ if (name === 'flavor' && newValue === 'danger' && oldValue === newValue)
235
+ this.removeAttribute(ERROR_FLAVOR);
236
+ super.attributeChangedCallback(name, oldValue, newValue);
237
+ }
215
238
  get currency() { return this.getProperty('currency'); }
216
239
  set currency(v) { this.setProperty('currency', v); }
217
240
  get locale() {
@@ -404,6 +427,7 @@ const inputMode = shouldFormatType(this.type) ? 'decimal'
404
427
  const displayValue = this.getDisplayValue();
405
428
  if (existingInput && existingWrapper) {
406
429
  existingInput.type = inputType;
430
+ if (existingInput.value !== displayValue)
407
431
  existingInput.value = displayValue;
408
432
  existingInput.placeholder = this.placeholder;
409
433
  existingInput.name = this.name;
@@ -527,6 +551,7 @@ syncConstraintAttributes(inputEl) {
527
551
  const text = !shouldFormatType(this.type);
528
552
  const set = (name, v) => text && v !== '' ? inputEl.setAttribute(name, v) : inputEl.removeAttribute(name);
529
553
  set('pattern', this.pattern);
554
+ set('minlength', toLimit(this.getProperty('minlength')) != null ? String(this.minLength) : '');
530
555
  set('maxlength', toLimit(this.getProperty('maxlength')) != null ? String(this.maxLength) : '');
531
556
  }
532
557
  updateValidity() {
@@ -538,7 +563,7 @@ return;
538
563
  const current = inputEl ? inputEl.value : String(this.getProperty('value') ?? '');
539
564
  if (current.trim() === '') {
540
565
  if (this.required) {
541
- this.applyValidity({ valueMissing: true }, nativeMessage('text', { required: '' }, ''), inputEl ?? undefined);
566
+ this.applyValidity({ valueMissing: true }, nativeMessage('input', { type: 'text', required: true }), inputEl ?? undefined);
542
567
  }
543
568
  else {
544
569
  this.applyValidity({});
@@ -553,29 +578,29 @@ messages.push(message);
553
578
  };
554
579
  if (shouldFormatType(this.type)) {
555
580
  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');
581
+ if (n !== null && (this.min || this.max)) {
582
+ const { validity, message } = nativeValidity('input', { type: 'number', step: 'any', min: this.min, max: this.max }, String(n));
583
+ if (validity.rangeUnderflow)
584
+ fail('rangeUnderflow', message);
585
+ if (validity.rangeOverflow)
586
+ fail('rangeOverflow', message);
587
+ }
563
588
  }
564
589
  else {
565
590
  if (inputEl?.validity.typeMismatch)
566
591
  fail('typeMismatch', inputEl.validationMessage);
567
592
  if (inputEl?.validity.patternMismatch)
568
593
  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
- }
594
+ const { tooShort, tooLong } = lengthValidity(current, {
595
+ minlength: this.getProperty('minlength'),
596
+ maxlength: this.getProperty('maxlength'),
597
+ tooShortMessage: this.tooShortMessage,
598
+ tooLongMessage: this.tooLongMessage,
599
+ }, inputEl);
600
+ if (tooShort)
601
+ fail('tooShort', tooShort);
602
+ if (tooLong)
603
+ fail('tooLong', tooLong);
579
604
  }
580
605
  this.applyValidity(flags, messages[0], inputEl ?? undefined);
581
606
  }
@@ -737,33 +762,6 @@ return Math.max(0, Math.min(5000, Math.floor(num)));
737
762
  }
738
763
  }
739
764
  };
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
765
  if (!customElements.get('ty-input')) {
768
766
  customElements.define('ty-input', TyInput);
769
767
  }
@@ -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();
@@ -98,7 +99,39 @@ event.preventDefault();
98
99
  closeModal(el, 'backdrop');
99
100
  }
100
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
+ }
101
131
  function handleEscapeKey(el, event) {
132
+ const dialog = event.currentTarget;
133
+ if (fromNestedDialog(dialog, event))
134
+ return;
102
135
  event.stopPropagation();
103
136
  if (event.key === 'Escape') {
104
137
  event.preventDefault();
@@ -155,7 +188,7 @@ dialog.addEventListener('keydown', handler);
155
188
  }
156
189
  else {
157
190
  const handler = (e) => {
158
- if (e.key === 'Escape')
191
+ if (e.key === 'Escape' && !fromNestedDialog(dialog, e))
159
192
  e.preventDefault();
160
193
  };
161
194
  escapeKeyHandlers.set(el, handler);
@@ -253,6 +286,7 @@ dispatchModalEvent(el, 'open', {});
253
286
  else {
254
287
  if (dialog.open) {
255
288
  unlockScroll(modalId);
289
+ closeNestedDialogs(el);
256
290
  dialog.close();
257
291
  syncBackdropZoom();
258
292
  const reason = pendingCloseReasons.get(el) ?? 'programmatic';
@@ -265,6 +299,7 @@ dialog.onclose = (event) => {
265
299
  if (event.target !== dialog)
266
300
  return;
267
301
  unlockScroll(modalId);
302
+ closeNestedDialogs(el);
268
303
  if (el.hasAttribute('open')) {
269
304
  el.removeAttribute('open');
270
305
  dispatchModalEvent(el, 'close', {
@@ -349,6 +384,7 @@ super();
349
384
  this.attachShadow({ mode: 'open' });
350
385
  }
351
386
  connectedCallback() {
387
+ upgradeProperties(this);
352
388
  render(this);
353
389
  this.show = () => openModal(this);
354
390
  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,7 @@ const closeTimers = new WeakMap();
9
9
  const escapeHandlers = new WeakMap();
10
10
  const closeRequestHandlers = new WeakMap();
11
11
  const popupIds = new WeakMap();
12
+ const pointerPresses = new WeakSet();
12
13
  function parseBoolAttr(el, name) {
13
14
  return el.hasAttribute(name);
14
15
  }
@@ -89,19 +90,21 @@ const shadowRoot = el.shadowRoot;
89
90
  const dialog = shadowRoot ? getPopupDialog(shadowRoot) : null;
90
91
  if (!dialog)
91
92
  return;
93
+ const anchor = getAnchorElement(el);
92
94
  if (dialog.open) {
93
95
  const pending = closeTimers.get(el);
94
96
  if (pending !== undefined) {
95
97
  clearTimeout(pending);
96
98
  closeTimers.delete(el);
97
99
  dialog.classList.add('open');
100
+ if (anchor?.hasAttribute('aria-haspopup'))
101
+ anchor.setAttribute('aria-expanded', 'true');
98
102
  }
99
103
  return;
100
104
  }
101
105
  const popupId = getPopupId(el);
102
106
  lockScroll(popupId);
103
107
  dialog.showModal();
104
- const anchor = getAnchorElement(el);
105
108
  if (anchor?.hasAttribute('aria-haspopup'))
106
109
  anchor.setAttribute('aria-expanded', 'true');
107
110
  requestAnimationFrame(() => {
@@ -140,7 +143,19 @@ event.preventDefault();
140
143
  closePopup(el);
141
144
  }
142
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
+ }
143
156
  function handleEscape(el, event) {
157
+ if (fromNestedDialog(el, event))
158
+ return;
144
159
  event.stopPropagation();
145
160
  if (event.key === 'Escape') {
146
161
  const { disableClose } = getPopupAttributes(el);
@@ -172,11 +187,21 @@ const existingKeyHandler = anchorKeyClickHandlers.get(el);
172
187
  if (existingKeyHandler) {
173
188
  anchor.removeEventListener('click', existingKeyHandler);
174
189
  }
175
- const handler = (e) => handleAnchorClick(el, e);
190
+ const handler = (e) => {
191
+ pointerPresses.add(el);
192
+ handleAnchorClick(el, e);
193
+ };
176
194
  anchorClickHandlers.set(el, handler);
177
195
  anchor.addEventListener('pointerdown', handler);
178
196
  const keyHandler = (e) => {
179
- 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;
180
205
  handleAnchorClick(el, e);
181
206
  };
182
207
  anchorKeyClickHandlers.set(el, keyHandler);
@@ -221,17 +246,29 @@ dialog.addEventListener('pointerdown', outsideHandler);
221
246
  const escapeHandler = (e) => handleEscape(el, e);
222
247
  escapeHandlers.set(el, escapeHandler);
223
248
  el.addEventListener('keydown', escapeHandler);
224
- dialog.addEventListener('close', () => {
225
- const popupId = getPopupId(el);
226
- 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));
227
256
  dialog.classList.remove('open');
228
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', () => {
229
263
  el.dispatchEvent(new CustomEvent('close'));
230
264
  });
231
265
  }
232
266
  el.closePopup = () => closePopup(el, true);
233
267
  el.openPopup = () => openPopup(el);
234
268
  el.togglePopup = () => togglePopup(el);
269
+ const existingCloseRequest = closeRequestHandlers.get(el);
270
+ if (existingCloseRequest)
271
+ el.removeEventListener('ty:close-popup', existingCloseRequest);
235
272
  const closeRequestHandler = (e) => handleCloseRequest(el, e);
236
273
  closeRequestHandlers.set(el, closeRequestHandler);
237
274
  el.addEventListener('ty:close-popup', closeRequestHandler);
@@ -165,7 +165,8 @@ export declare class TyRadioGroup extends TyComponent<RadioGroupState> implement
165
165
  private getRadios;
166
166
  private syncChildren;
167
167
  /** Report `required` to the owning form — without this the asterisk
168
- 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. */
169
170
  private updateValidity;
170
171
  private handleRadioSelect;
171
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();
@@ -223,7 +224,7 @@ updateValidity() {
223
224
  if (this.required && !this.disabled && !this.value) {
224
225
  const anchor = this.getRadios().find((r) => !r.disabled);
225
226
  const target = anchor?.shadowRoot?.querySelector(".radio-container");
226
- 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);
227
228
  }
228
229
  else {
229
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;
@@ -369,8 +382,9 @@ export declare class TySelect extends TyComponent<SelectState> {
369
382
  * Reflect constraint validation to the owning <form> via ElementInternals.
370
383
  * Without this `required` is only cosmetic — form.checkValidity() (and a
371
384
  * real submit) never actually blocks on an empty required select. Mirrors
372
- * ty-input's updateValidity(): empty + required => valueMissing; disabled
373
- * 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.
374
388
  */
375
389
  private updateValidity;
376
390
  /**
@@ -383,6 +397,16 @@ export declare class TySelect extends TyComponent<SelectState> {
383
397
  private renderDesktop;
384
398
  private renderMobile;
385
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;
386
410
  private handleMobileStubClick;
387
411
  /** Keyboard-open the (closed) mobile stub — see handleStubKeydown for why this is needed. */
388
412
  private handleMobileStubKeydown;
@@ -444,7 +468,9 @@ export declare class TySelect extends TyComponent<SelectState> {
444
468
  * Clear the entire selection programmatically WITH a change event
445
469
  * (action: 'clear'). No-op if nothing is selected. Exists so slot="trigger"
446
470
  * consumers (custom chrome, built-in clear UI hidden by design) can wire
447
- * 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.
448
474
  */
449
475
  clear(): void;
450
476
  get value(): string;
@@ -469,6 +495,8 @@ export declare class TySelect extends TyComponent<SelectState> {
469
495
  set noClear(value: boolean);
470
496
  get required(): boolean;
471
497
  set required(value: boolean);
498
+ get error(): string;
499
+ set error(value: string);
472
500
  get notSearchable(): boolean;
473
501
  set notSearchable(value: boolean);
474
502
  get externalSearch(): boolean;
@@ -483,6 +511,7 @@ export declare class TySelect extends TyComponent<SelectState> {
483
511
  set placement(value: Placement | "");
484
512
  get flavor(): Flavor;
485
513
  set flavor(value: Flavor);
514
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
486
515
  get form(): HTMLFormElement | null;
487
516
  }
488
517
  export {};