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,6 +1,7 @@
1
1
  import { DEFAULT_SIZE } from "../types/common.js";
2
2
  import { ensureStyles } from "../utils/styles.js";
3
3
  import { syncCustomFlavorSheet } from "../utils/flavor-sheet.js";
4
+ import { ERROR_FLAVOR } from "../utils/error-flavor.js";
4
5
  import { selectBaseStyles, selectCustomFlavorCss } from "../styles/select-base.js";
5
6
  import { selectStyles } from "../styles/select.js";
6
7
  import { getLoaderSvg } from "../utils/loader-registry.js";
@@ -20,6 +21,7 @@ elementIds.set(element, id);
20
21
  return id;
21
22
  }
22
23
  import { REQUIRED_ICON_SVG } from "../utils/icons.js";
24
+ import { nativeMessage } from "../utils/validity.js";
23
25
  const SEARCH_AUTO_THRESHOLD = 7;
24
26
  const CHEVRON_DOWN_SVG = `<svg viewBox="0 0 20 20" fill="currentColor">
25
27
  <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
@@ -81,6 +83,7 @@ this._optionsScrollbar = null;
81
83
  this._childObserver = null;
82
84
  this._selectedClone = null;
83
85
  this._customFlavorSheet = null;
86
+ this._ariaTrigger = null;
84
87
  const shadow = this.shadowRoot;
85
88
  ensureStyles(shadow, { css: selectBaseStyles, id: "ty-select-base" });
86
89
  ensureStyles(shadow, { css: selectStyles, id: "ty-select" });
@@ -96,6 +99,7 @@ dialog.close();
96
99
  });
97
100
  this.render();
98
101
  this.initializeState();
102
+ this.updateFormValue();
99
103
  this._childObserver = new MutationObserver(() => {
100
104
  this.ensureOptionRoles();
101
105
  if (this._state.selectedValues.length > 0) {
@@ -108,11 +112,43 @@ this.updateMobileSelectedState();
108
112
  this._childObserver.observe(this, { childList: true });
109
113
  this.ensureOptionRoles();
110
114
  this._syncCustomFlavor();
115
+ this.syncErrorFlavor();
116
+ if (!this._keyboardHandler) {
117
+ this._keyboardHandler = this.handleKeyboard.bind(this);
118
+ document.addEventListener("keydown", this._keyboardHandler, true);
119
+ }
111
120
  }
112
121
  _syncCustomFlavor() {
113
122
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.getProperty("flavor"), ({ base }) => selectCustomFlavorCss(base));
114
123
  }
124
+ syncErrorFlavor() {
125
+ if (!this._connected)
126
+ return;
127
+ const flavor = this.getProperty("flavor");
128
+ const replaced = this.getAttribute(ERROR_FLAVOR);
129
+ if (this.getProperty("error")) {
130
+ if (replaced === null && flavor === "neutral") {
131
+ this.setAttribute(ERROR_FLAVOR, this.getAttribute("flavor") ?? "");
132
+ this.setProperty("flavor", "danger");
133
+ }
134
+ }
135
+ else if (replaced !== null) {
136
+ this.removeAttribute(ERROR_FLAVOR);
137
+ if (flavor !== "danger")
138
+ return;
139
+ if (!replaced)
140
+ this.removeAttribute("flavor");
141
+ else
142
+ this.setProperty("flavor", replaced);
143
+ }
144
+ }
115
145
  onDisconnect() {
146
+ if (this._state.open) {
147
+ if (this._state.mode === "mobile")
148
+ this.closeMobileModal();
149
+ else
150
+ this.closeDropdown();
151
+ }
116
152
  const shadow = this.shadowRoot;
117
153
  const dialogs = shadow.querySelectorAll("dialog");
118
154
  dialogs.forEach((dialog) => {
@@ -121,7 +157,7 @@ dialog.close();
121
157
  }
122
158
  });
123
159
  if (this._keyboardHandler) {
124
- document.removeEventListener("keydown", this._keyboardHandler);
160
+ document.removeEventListener("keydown", this._keyboardHandler, true);
125
161
  this._keyboardHandler = null;
126
162
  }
127
163
  if (this._searchDebounceTimer !== null) {
@@ -150,8 +186,15 @@ case "name":
150
186
  this._name = newValue || "";
151
187
  break;
152
188
  case "flavor":
189
+ if (newValue !== "danger") {
190
+ this.removeAttribute(ERROR_FLAVOR);
191
+ this.syncErrorFlavor();
192
+ }
153
193
  this._syncCustomFlavor();
154
194
  break;
195
+ case "error":
196
+ this.syncErrorFlavor();
197
+ break;
155
198
  case "multiple":
156
199
  this._multiple = newValue;
157
200
  if (!newValue && this._state.selectedValues.length > 1) {
@@ -250,8 +293,21 @@ formData.append(this._name, value);
250
293
  });
251
294
  return formData;
252
295
  }
296
+ if (this._name && !this._multiple)
297
+ return "";
253
298
  return null;
254
299
  }
300
+ focus(options) {
301
+ if (this._disabled)
302
+ return;
303
+ const trigger = this.querySelector(':scope > [slot="trigger"]');
304
+ const stub = this.shadowRoot?.querySelector(".select-stub");
305
+ const target = trigger ?? stub;
306
+ if (target)
307
+ target.focus(options);
308
+ else
309
+ super.focus(options);
310
+ }
255
311
  parseValue(value) {
256
312
  if (!value || typeof value !== "string" || value.trim() === "")
257
313
  return [];
@@ -333,7 +389,7 @@ if (!tag.hasAttribute("aria-selected"))
333
389
  tag.setAttribute("aria-selected", "false");
334
390
  });
335
391
  }
336
- updateComponentValue(newValues, dispatchChange = false, action = "set", item = null) {
392
+ updateComponentValue(newValues, dispatchChange = false, action = "set", item = null, reveal = dispatchChange) {
337
393
  const oldValues = this.getSelectedValues();
338
394
  const valueStr = newValues.join(",");
339
395
  if (JSON.stringify(newValues.sort()) !== JSON.stringify(oldValues.sort())) {
@@ -347,6 +403,8 @@ items: this.getSelectedItems(newValues),
347
403
  action,
348
404
  item,
349
405
  });
406
+ if (reveal)
407
+ this.revealValidity();
350
408
  }
351
409
  }
352
410
  }
@@ -590,6 +648,7 @@ if (this._disabled || this._readonly) {
590
648
  return;
591
649
  }
592
650
  this.clear();
651
+ this.revealValidity();
593
652
  }
594
653
  handleStubKeydown(e) {
595
654
  if (this._disabled || this._readonly || this._state.open)
@@ -742,6 +801,7 @@ this.closeMobileModal();
742
801
  else {
743
802
  this.closeDropdown();
744
803
  }
804
+ this.focus();
745
805
  break;
746
806
  case "Enter":
747
807
  e.preventDefault();
@@ -903,10 +963,26 @@ label.innerHTML = `${this._label}${this._required ? `<span class="required-icon"
903
963
  else {
904
964
  label?.remove();
905
965
  }
966
+ const error = this.getProperty("error") || "";
967
+ const errorId = `select-error-${getElementHash(this)}`;
968
+ let errorLine = container.querySelector(":scope > .error-message");
969
+ if (error) {
970
+ if (!errorLine) {
971
+ errorLine = document.createElement("div");
972
+ errorLine.className = "error-message";
973
+ errorLine.id = errorId;
974
+ container.append(errorLine);
975
+ }
976
+ errorLine.textContent = error;
977
+ }
978
+ else {
979
+ errorLine?.remove();
980
+ }
906
981
  const stub = shadow.querySelector(".select-stub");
907
982
  if (stub) {
908
983
  stub.classList.toggle("disabled", this._disabled);
909
984
  stub.classList.toggle("compact", this._compact);
985
+ stub.classList.toggle("error", !!error);
910
986
  stub.tabIndex = this._disabled ? -1 : 0;
911
987
  stub.toggleAttribute("disabled", this._disabled);
912
988
  if (this._disabled)
@@ -917,7 +993,27 @@ if (this._label)
917
993
  stub.setAttribute("aria-labelledby", labelId);
918
994
  else
919
995
  stub.removeAttribute("aria-labelledby");
996
+ if (error) {
997
+ stub.setAttribute("aria-invalid", "true");
998
+ stub.setAttribute("aria-describedby", errorId);
999
+ }
1000
+ else {
1001
+ stub.removeAttribute("aria-invalid");
1002
+ stub.removeAttribute("aria-describedby");
1003
+ }
920
1004
  }
1005
+ const trigger = error
1006
+ ? this.querySelector(':scope > [slot="trigger"]')
1007
+ : null;
1008
+ if (this._ariaTrigger && this._ariaTrigger !== trigger) {
1009
+ this._ariaTrigger.removeAttribute("aria-invalid");
1010
+ this._ariaTrigger.removeAttribute("aria-description");
1011
+ }
1012
+ if (trigger) {
1013
+ trigger.setAttribute("aria-invalid", "true");
1014
+ trigger.setAttribute("aria-description", error);
1015
+ }
1016
+ this._ariaTrigger = trigger;
921
1017
  shadow
922
1018
  .querySelectorAll(".dropdown-search-input, .mobile-search-input")
923
1019
  .forEach((input) => (input.disabled = this._disabled));
@@ -929,7 +1025,7 @@ return;
929
1025
  }
930
1026
  if (this.required && this.getSelectedValues().length === 0) {
931
1027
  const stub = this.shadowRoot?.querySelector(".select-stub");
932
- this.applyValidity({ valueMissing: true }, "Please select a value", stub ?? undefined);
1028
+ this.applyValidity({ valueMissing: true }, nativeMessage("select", { required: true, multiple: this._multiple }), stub ?? undefined);
933
1029
  }
934
1030
  else {
935
1031
  this.applyValidity({});
@@ -946,7 +1042,10 @@ const hasTrigger = slot.assignedElements().length > 0;
946
1042
  stub.classList.toggle("custom-trigger", hasTrigger);
947
1043
  this.toggleAttribute("has-trigger", hasTrigger);
948
1044
  };
949
- slot.addEventListener("slotchange", sync);
1045
+ slot.addEventListener("slotchange", () => {
1046
+ sync();
1047
+ this.syncFieldChrome();
1048
+ });
950
1049
  sync();
951
1050
  }
952
1051
  setupEventListeners() {
@@ -979,9 +1078,13 @@ if (e.target === dialog) {
979
1078
  this.closeDropdown();
980
1079
  }
981
1080
  });
1081
+ dialog.addEventListener("cancel", (e) => {
1082
+ e.preventDefault();
1083
+ this.closeDropdown();
1084
+ this.focus();
1085
+ });
1086
+ this.syncOnOutsideClose(dialog, () => this.closeDropdown());
982
1087
  }
983
- this._keyboardHandler = this.handleKeyboard.bind(this);
984
- document.addEventListener("keydown", this._keyboardHandler);
985
1088
  }
986
1089
  buildStubClasses() {
987
1090
  const classes = [];
@@ -1175,10 +1278,6 @@ this.setupMobileEventListeners();
1175
1278
  this.updateSelectionDisplay();
1176
1279
  }
1177
1280
  setupMobileEventListeners() {
1178
- if (!this._keyboardHandler) {
1179
- this._keyboardHandler = this.handleKeyboard.bind(this);
1180
- document.addEventListener("keydown", this._keyboardHandler);
1181
- }
1182
1281
  const shadow = this.shadowRoot;
1183
1282
  const stub = shadow.querySelector(".select-stub");
1184
1283
  const optionsSlot = shadow.querySelector("#options-slot");
@@ -1213,8 +1312,16 @@ this.closeMobileModal();
1213
1312
  dialog.addEventListener("cancel", (e) => {
1214
1313
  e.preventDefault();
1215
1314
  this.closeMobileModal();
1315
+ this.focus();
1216
1316
  });
1317
+ this.syncOnOutsideClose(dialog, () => this.closeMobileModal());
1318
+ }
1217
1319
  }
1320
+ syncOnOutsideClose(dialog, close) {
1321
+ new MutationObserver(() => {
1322
+ if (this._state.open && !dialog.open)
1323
+ close();
1324
+ }).observe(dialog, { attributes: true, attributeFilter: ["open"] });
1218
1325
  }
1219
1326
  handleMobileStubClick(e) {
1220
1327
  e.preventDefault();
@@ -1409,7 +1516,7 @@ return;
1409
1516
  this.updateComponentValue(current.filter((v) => v !== value), true, "remove", value);
1410
1517
  }
1411
1518
  clear() {
1412
- this.updateComponentValue([], true, "clear", null);
1519
+ this.updateComponentValue([], true, "clear", null, false);
1413
1520
  }
1414
1521
  get value() {
1415
1522
  return this.getSelectedValues().join(",");
@@ -1477,6 +1584,12 @@ return this.getProperty("required");
1477
1584
  set required(value) {
1478
1585
  this.setProperty("required", value);
1479
1586
  }
1587
+ get error() {
1588
+ return this.getProperty("error");
1589
+ }
1590
+ set error(value) {
1591
+ this.setProperty("error", value);
1592
+ }
1480
1593
  get notSearchable() {
1481
1594
  return this.getProperty("notSearchable");
1482
1595
  }
@@ -1518,8 +1631,14 @@ get flavor() {
1518
1631
  return this.getProperty("flavor");
1519
1632
  }
1520
1633
  set flavor(value) {
1634
+ this.removeAttribute(ERROR_FLAVOR);
1521
1635
  this.setProperty("flavor", value);
1522
1636
  }
1637
+ attributeChangedCallback(name, oldValue, newValue) {
1638
+ if (name === "flavor" && newValue === "danger" && oldValue === newValue)
1639
+ this.removeAttribute(ERROR_FLAVOR);
1640
+ super.attributeChangedCallback(name, oldValue, newValue);
1641
+ }
1523
1642
  get form() {
1524
1643
  return this._internals.form;
1525
1644
  }
@@ -1580,6 +1699,11 @@ type: "boolean",
1580
1699
  visual: true,
1581
1700
  default: false,
1582
1701
  },
1702
+ error: {
1703
+ type: "string",
1704
+ visual: true,
1705
+ default: "",
1706
+ },
1583
1707
  noClear: {
1584
1708
  type: "boolean",
1585
1709
  visual: true,
@@ -28,6 +28,8 @@ export declare class TySelectedOptions extends HTMLElement {
28
28
  * Interpolation context for a value: value, label, flavor plus every
29
29
  * data-* attribute of the picker's matching option.
30
30
  */
31
+ /** The picker's option for a value (never its display clone). */
32
+ private option;
31
33
  private context;
32
34
  /**
33
35
  * The template's blueprint fragment. HTML-parsed templates carry children in
@@ -42,7 +44,11 @@ export declare class TySelectedOptions extends HTMLElement {
42
44
  * no flavor on the option).
43
45
  */
44
46
  private stampTemplate;
45
- /** Default chip when no template is provided. */
47
+ /**
48
+ * Default chip when no template is provided: the option's own content,
49
+ * cloned, so a rich option makes a rich chip. An explicit `label` on the
50
+ * option replaces it with that text; a <template> child replaces it entirely.
51
+ */
46
52
  private defaultChip;
47
53
  private renderChips;
48
54
  /** Remove a value from the picker; picker re-emits change → chips re-render. */
@@ -93,8 +93,11 @@ return parseValues(this._picker?.value ?? '');
93
93
  template() {
94
94
  return this.querySelector(':scope > template');
95
95
  }
96
+ option(value) {
97
+ return this._picker?.querySelector(`[value="${CSS.escape(value)}"]:not([cloned])`);
98
+ }
96
99
  context(value) {
97
- const opt = this._picker?.querySelector(`[value="${CSS.escape(value)}"]`);
100
+ const opt = this.option(value);
98
101
  const ctx = {
99
102
  value,
100
103
  label: opt?.getAttribute('label') || opt?.textContent?.trim() || value,
@@ -150,12 +153,18 @@ return clone;
150
153
  }
151
154
  defaultChip(value) {
152
155
  const ctx = this.context(value);
156
+ const opt = this.option(value);
153
157
  const tag = document.createElement('ty-tag');
154
158
  tag.setAttribute('value', value);
155
159
  tag.setAttribute('dismissible', 'true');
156
160
  if (ctx.flavor)
157
161
  tag.setAttribute('flavor', ctx.flavor);
162
+ if (opt && !opt.hasAttribute('label') && opt.hasChildNodes()) {
163
+ opt.childNodes.forEach(n => tag.appendChild(n.cloneNode(true)));
164
+ }
165
+ else {
158
166
  tag.textContent = ctx.label;
167
+ }
159
168
  tag.addEventListener('dismiss', () => this.deselect(value));
160
169
  return tag;
161
170
  }
@@ -82,7 +82,11 @@ export declare class TySwitch extends TyComponent<SwitchState> implements TySwit
82
82
  private removeEventListeners;
83
83
  private setupEventListeners;
84
84
  protected render(): void;
85
- /** required + unchecked => valueMissing; otherwise valid. */
85
+ /** Focus the switch itself (the host is not focusable; its role element is). */
86
+ focus(options?: FocusOptions): void;
87
+ /** required + unchecked => valueMissing; otherwise valid. The message is a
88
+ * native checkbox's, as a switch where the browser has one (Safari's
89
+ * `switch` attribute). */
86
90
  private updateValidity;
87
91
  get checked(): boolean;
88
92
  set checked(value: boolean);
@@ -3,6 +3,7 @@ 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
5
  import { switchStyles, switchCustomFlavorCss } from "../styles/switch.js";
6
+ import { nativeMessage } from "../utils/validity.js";
6
7
  export class TySwitch extends TyComponent {
7
8
  constructor() {
8
9
  super();
@@ -145,6 +146,13 @@ this.setupEventListeners();
145
146
  this.applyLabelName(switchEl);
146
147
  this.updateValidity();
147
148
  }
149
+ focus(options) {
150
+ const el = this.shadowRoot?.querySelector(".switch-container");
151
+ if (el)
152
+ el.focus(options);
153
+ else
154
+ super.focus(options);
155
+ }
148
156
  updateValidity() {
149
157
  const anchor = this.shadowRoot?.querySelector(".switch-container");
150
158
  if (this.disabled) {
@@ -152,7 +160,7 @@ this.applyValidity({});
152
160
  return;
153
161
  }
154
162
  if (this.required && !this.checked) {
155
- this.applyValidity({ valueMissing: true }, "Please turn this on", anchor ?? undefined);
163
+ this.applyValidity({ valueMissing: true }, nativeMessage("input", { type: "checkbox", switch: true, required: true }), anchor ?? undefined);
156
164
  }
157
165
  else {
158
166
  this.applyValidity({});
@@ -21,6 +21,10 @@ export interface TyTextareaElement extends HTMLElement {
21
21
  label?: string;
22
22
  disabled?: boolean;
23
23
  required?: boolean;
24
+ minLength?: number;
25
+ maxLength?: number;
26
+ tooShortMessage?: string;
27
+ tooLongMessage?: string;
24
28
  error?: string;
25
29
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
26
30
  rows?: string | number;
@@ -63,6 +67,26 @@ export declare class TyTextarea extends TyComponent<TextareaState> implements Ty
63
67
  visual: boolean;
64
68
  default: boolean;
65
69
  };
70
+ minlength: {
71
+ type: "string";
72
+ visual: boolean;
73
+ default: string;
74
+ };
75
+ maxlength: {
76
+ type: "string";
77
+ visual: boolean;
78
+ default: string;
79
+ };
80
+ tooShortMessage: {
81
+ type: "string";
82
+ visual: boolean;
83
+ default: string;
84
+ };
85
+ tooLongMessage: {
86
+ type: "string";
87
+ visual: boolean;
88
+ default: string;
89
+ };
66
90
  error: {
67
91
  type: "string";
68
92
  visual: boolean;
@@ -125,8 +149,18 @@ export declare class TyTextarea extends TyComponent<TextareaState> implements Ty
125
149
  set label(v: string);
126
150
  get disabled(): boolean;
127
151
  set disabled(v: boolean);
152
+ /** Focus the inner <textarea>: the host itself isn't focusable. */
153
+ focus(options?: FocusOptions): void;
128
154
  get required(): boolean;
129
155
  set required(v: boolean);
156
+ get minLength(): number;
157
+ set minLength(v: number);
158
+ get maxLength(): number;
159
+ set maxLength(v: number);
160
+ get tooShortMessage(): string;
161
+ set tooShortMessage(v: string);
162
+ get tooLongMessage(): string;
163
+ set tooLongMessage(v: string);
130
164
  get error(): string;
131
165
  set error(v: string);
132
166
  get size(): 'xs' | 'sm' | 'md' | 'lg' | 'xl';
@@ -158,8 +192,18 @@ export declare class TyTextarea extends TyComponent<TextareaState> implements Ty
158
192
  private applyHeightConstraints;
159
193
  private setupTextareaEvents;
160
194
  protected render(): void;
161
- /** required + empty => valueMissing; otherwise valid. Disabled fields are
162
- * barred from constraint validation. */
195
+ /**
196
+ * minlength goes on the inner <textarea> for the browser's own too-short
197
+ * message after the user types, maxlength so typing stops at the limit
198
+ * (like ty-input).
199
+ */
200
+ private syncConstraintAttributes;
201
+ /**
202
+ * required + empty => valueMissing (the browser's own message); otherwise
203
+ * minlength / maxlength, worded like ty-input's (lengthValidity). Empty
204
+ * values only fail `required`, like native. Disabled fields are barred
205
+ * from constraint validation.
206
+ */
163
207
  private updateValidity;
164
208
  }
165
209
  export {};
@@ -4,7 +4,8 @@ import { ensureStyles } from '../utils/styles.js';
4
4
  import { textareaStyles } from '../styles/textarea.js';
5
5
  import { CustomScrollbar, isCustomScrollbarEnabled } from '../utils/custom-scrollbar.js';
6
6
  import { customScrollbarStyles } from '../styles/custom-scrollbar.js';
7
- const REQUIRED_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`;
7
+ import { nativeMessage, lengthValidity, toLimit } from '../utils/validity.js';
8
+ import { REQUIRED_ICON_SVG } from '../utils/icons.js';
8
9
  export class TyTextarea extends TyComponent {
9
10
  constructor() {
10
11
  super();
@@ -13,6 +14,7 @@ this._dummyEl = null;
13
14
  this._scrollbar = null;
14
15
  this._isFocused = false;
15
16
  this.handleInputEvent = (e) => {
17
+ this._userEdited = true;
16
18
  const target = e.target;
17
19
  const newValue = target.value;
18
20
  this.value = newValue;
@@ -30,6 +32,8 @@ this._valueAtFocus = this.value;
30
32
  };
31
33
  this.handleBlurEvent = (e) => {
32
34
  this._isFocused = false;
35
+ if (this._userEdited)
36
+ this.revealValidity();
33
37
  if (this._valueAtFocus !== null && this.value !== this._valueAtFocus) {
34
38
  this.emitValueEvent('change', e);
35
39
  }
@@ -67,8 +71,19 @@ get label() { return this.getProperty('label'); }
67
71
  set label(v) { this.setProperty('label', v); }
68
72
  get disabled() { return this.getProperty('disabled'); }
69
73
  set disabled(v) { this.setProperty('disabled', v); }
74
+ focus(options) {
75
+ this.shadowRoot?.querySelector('textarea')?.focus(options);
76
+ }
70
77
  get required() { return this.getProperty('required'); }
71
78
  set required(v) { this.setProperty('required', v); }
79
+ get minLength() { return toLimit(this.getProperty('minlength')) ?? -1; }
80
+ set minLength(v) { this.setProperty('minlength', v == null || v < 0 ? '' : String(v)); }
81
+ get maxLength() { return toLimit(this.getProperty('maxlength')) ?? -1; }
82
+ set maxLength(v) { this.setProperty('maxlength', v == null || v < 0 ? '' : String(v)); }
83
+ get tooShortMessage() { return this.getProperty('tooShortMessage'); }
84
+ set tooShortMessage(v) { this.setProperty('tooShortMessage', v ?? ''); }
85
+ get tooLongMessage() { return this.getProperty('tooLongMessage'); }
86
+ set tooLongMessage(v) { this.setProperty('tooLongMessage', v ?? ''); }
72
87
  get error() { return this.getProperty('error'); }
73
88
  set error(v) { this.setProperty('error', v); }
74
89
  get size() {
@@ -250,18 +265,19 @@ const existingError = root.querySelector('.error-message');
250
265
  if (existingContainer && existingTextarea) {
251
266
  if (this.label) {
252
267
  if (existingLabel) {
253
- existingLabel.innerHTML = this.label +
254
- (this.required ? ` <span class="required-icon">${REQUIRED_ICON}</span>` : '');
255
- existingLabel.style.display = 'block';
268
+ existingLabel.innerHTML = `${this.label}${this.required ? `<span class="required-icon">${REQUIRED_ICON_SVG}</span>` : ''}`;
269
+ existingLabel.style.display = 'flex';
256
270
  }
257
271
  }
258
272
  else if (existingLabel) {
259
273
  existingLabel.style.display = 'none';
260
274
  }
275
+ if (existingTextarea.value !== (this.value || ''))
261
276
  existingTextarea.value = this.value || '';
262
277
  existingTextarea.placeholder = this.placeholder || '';
263
278
  existingTextarea.disabled = this.disabled;
264
279
  existingTextarea.required = this.required;
280
+ this.syncConstraintAttributes(existingTextarea);
265
281
  existingTextarea.className = this.buildClassList();
266
282
  if (this.label)
267
283
  existingTextarea.setAttribute('aria-label', this.label);
@@ -301,9 +317,8 @@ container.className = 'textarea-container';
301
317
  const labelEl = document.createElement('label');
302
318
  labelEl.className = 'ty-field-label';
303
319
  if (this.label) {
304
- labelEl.innerHTML = this.label +
305
- (this.required ? ` <span class="required-icon">${REQUIRED_ICON}</span>` : '');
306
- labelEl.style.display = 'block';
320
+ labelEl.innerHTML = `${this.label}${this.required ? `<span class="required-icon">${REQUIRED_ICON_SVG}</span>` : ''}`;
321
+ labelEl.style.display = 'flex';
307
322
  }
308
323
  else {
309
324
  labelEl.style.display = 'none';
@@ -313,6 +328,7 @@ textareaEl.value = this.value || '';
313
328
  textareaEl.placeholder = this.placeholder || '';
314
329
  textareaEl.disabled = this.disabled;
315
330
  textareaEl.required = this.required;
331
+ this.syncConstraintAttributes(textareaEl);
316
332
  textareaEl.className = this.buildClassList();
317
333
  if (this.label)
318
334
  textareaEl.setAttribute('aria-label', this.label);
@@ -364,6 +380,11 @@ this._textareaEl = textareaEl;
364
380
  }
365
381
  this.updateValidity();
366
382
  }
383
+ syncConstraintAttributes(ta) {
384
+ const set = (name, limit) => limit !== null ? ta.setAttribute(name, String(limit)) : ta.removeAttribute(name);
385
+ set('minlength', toLimit(this.getProperty('minlength')));
386
+ set('maxlength', toLimit(this.getProperty('maxlength')));
387
+ }
367
388
  updateValidity() {
368
389
  const ta = this.shadowRoot?.querySelector('textarea');
369
390
  if (this.disabled) {
@@ -371,12 +392,22 @@ this.applyValidity({});
371
392
  return;
372
393
  }
373
394
  const current = ta ? ta.value : String(this.value ?? '');
374
- if (this.required && current.trim() === '') {
375
- this.applyValidity({ valueMissing: true }, 'Please fill out this field', ta ?? undefined);
395
+ if (current.trim() === '') {
396
+ if (this.required) {
397
+ this.applyValidity({ valueMissing: true }, nativeMessage('textarea', { required: true }), ta ?? undefined);
376
398
  }
377
399
  else {
378
400
  this.applyValidity({});
379
401
  }
402
+ return;
403
+ }
404
+ const { tooShort, tooLong } = lengthValidity(current, {
405
+ minlength: this.getProperty('minlength'),
406
+ maxlength: this.getProperty('maxlength'),
407
+ tooShortMessage: this.tooShortMessage,
408
+ tooLongMessage: this.tooLongMessage,
409
+ }, ta);
410
+ this.applyValidity({ tooShort: !!tooShort, tooLong: !!tooLong }, tooShort ?? tooLong, ta ?? undefined);
380
411
  }
381
412
  }
382
413
  TyTextarea.formAssociated = true;
@@ -412,6 +443,26 @@ type: 'boolean',
412
443
  visual: true,
413
444
  default: false
414
445
  },
446
+ minlength: {
447
+ type: 'string',
448
+ visual: true,
449
+ default: ''
450
+ },
451
+ maxlength: {
452
+ type: 'string',
453
+ visual: true,
454
+ default: ''
455
+ },
456
+ tooShortMessage: {
457
+ type: 'string',
458
+ visual: true,
459
+ default: ''
460
+ },
461
+ tooLongMessage: {
462
+ type: 'string',
463
+ visual: true,
464
+ default: ''
465
+ },
415
466
  error: {
416
467
  type: 'string',
417
468
  visual: true,