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
@@ -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" });
@@ -109,11 +112,43 @@ this.updateMobileSelectedState();
109
112
  this._childObserver.observe(this, { childList: true });
110
113
  this.ensureOptionRoles();
111
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
+ }
112
120
  }
113
121
  _syncCustomFlavor() {
114
122
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.getProperty("flavor"), ({ base }) => selectCustomFlavorCss(base));
115
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
+ }
116
145
  onDisconnect() {
146
+ if (this._state.open) {
147
+ if (this._state.mode === "mobile")
148
+ this.closeMobileModal();
149
+ else
150
+ this.closeDropdown();
151
+ }
117
152
  const shadow = this.shadowRoot;
118
153
  const dialogs = shadow.querySelectorAll("dialog");
119
154
  dialogs.forEach((dialog) => {
@@ -122,7 +157,7 @@ dialog.close();
122
157
  }
123
158
  });
124
159
  if (this._keyboardHandler) {
125
- document.removeEventListener("keydown", this._keyboardHandler);
160
+ document.removeEventListener("keydown", this._keyboardHandler, true);
126
161
  this._keyboardHandler = null;
127
162
  }
128
163
  if (this._searchDebounceTimer !== null) {
@@ -151,8 +186,15 @@ case "name":
151
186
  this._name = newValue || "";
152
187
  break;
153
188
  case "flavor":
189
+ if (newValue !== "danger") {
190
+ this.removeAttribute(ERROR_FLAVOR);
191
+ this.syncErrorFlavor();
192
+ }
154
193
  this._syncCustomFlavor();
155
194
  break;
195
+ case "error":
196
+ this.syncErrorFlavor();
197
+ break;
156
198
  case "multiple":
157
199
  this._multiple = newValue;
158
200
  if (!newValue && this._state.selectedValues.length > 1) {
@@ -256,6 +298,8 @@ return "";
256
298
  return null;
257
299
  }
258
300
  focus(options) {
301
+ if (this._disabled)
302
+ return;
259
303
  const trigger = this.querySelector(':scope > [slot="trigger"]');
260
304
  const stub = this.shadowRoot?.querySelector(".select-stub");
261
305
  const target = trigger ?? stub;
@@ -345,7 +389,7 @@ if (!tag.hasAttribute("aria-selected"))
345
389
  tag.setAttribute("aria-selected", "false");
346
390
  });
347
391
  }
348
- updateComponentValue(newValues, dispatchChange = false, action = "set", item = null) {
392
+ updateComponentValue(newValues, dispatchChange = false, action = "set", item = null, reveal = dispatchChange) {
349
393
  const oldValues = this.getSelectedValues();
350
394
  const valueStr = newValues.join(",");
351
395
  if (JSON.stringify(newValues.sort()) !== JSON.stringify(oldValues.sort())) {
@@ -359,6 +403,8 @@ items: this.getSelectedItems(newValues),
359
403
  action,
360
404
  item,
361
405
  });
406
+ if (reveal)
407
+ this.revealValidity();
362
408
  }
363
409
  }
364
410
  }
@@ -602,6 +648,7 @@ if (this._disabled || this._readonly) {
602
648
  return;
603
649
  }
604
650
  this.clear();
651
+ this.revealValidity();
605
652
  }
606
653
  handleStubKeydown(e) {
607
654
  if (this._disabled || this._readonly || this._state.open)
@@ -754,6 +801,7 @@ this.closeMobileModal();
754
801
  else {
755
802
  this.closeDropdown();
756
803
  }
804
+ this.focus();
757
805
  break;
758
806
  case "Enter":
759
807
  e.preventDefault();
@@ -915,10 +963,26 @@ label.innerHTML = `${this._label}${this._required ? `<span class="required-icon"
915
963
  else {
916
964
  label?.remove();
917
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
+ }
918
981
  const stub = shadow.querySelector(".select-stub");
919
982
  if (stub) {
920
983
  stub.classList.toggle("disabled", this._disabled);
921
984
  stub.classList.toggle("compact", this._compact);
985
+ stub.classList.toggle("error", !!error);
922
986
  stub.tabIndex = this._disabled ? -1 : 0;
923
987
  stub.toggleAttribute("disabled", this._disabled);
924
988
  if (this._disabled)
@@ -929,7 +993,27 @@ if (this._label)
929
993
  stub.setAttribute("aria-labelledby", labelId);
930
994
  else
931
995
  stub.removeAttribute("aria-labelledby");
996
+ if (error) {
997
+ stub.setAttribute("aria-invalid", "true");
998
+ stub.setAttribute("aria-describedby", errorId);
932
999
  }
1000
+ else {
1001
+ stub.removeAttribute("aria-invalid");
1002
+ stub.removeAttribute("aria-describedby");
1003
+ }
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;
933
1017
  shadow
934
1018
  .querySelectorAll(".dropdown-search-input, .mobile-search-input")
935
1019
  .forEach((input) => (input.disabled = this._disabled));
@@ -941,7 +1025,7 @@ return;
941
1025
  }
942
1026
  if (this.required && this.getSelectedValues().length === 0) {
943
1027
  const stub = this.shadowRoot?.querySelector(".select-stub");
944
- this.applyValidity({ valueMissing: true }, "Please select a value", stub ?? undefined);
1028
+ this.applyValidity({ valueMissing: true }, nativeMessage("select", { required: true, multiple: this._multiple }), stub ?? undefined);
945
1029
  }
946
1030
  else {
947
1031
  this.applyValidity({});
@@ -958,7 +1042,10 @@ const hasTrigger = slot.assignedElements().length > 0;
958
1042
  stub.classList.toggle("custom-trigger", hasTrigger);
959
1043
  this.toggleAttribute("has-trigger", hasTrigger);
960
1044
  };
961
- slot.addEventListener("slotchange", sync);
1045
+ slot.addEventListener("slotchange", () => {
1046
+ sync();
1047
+ this.syncFieldChrome();
1048
+ });
962
1049
  sync();
963
1050
  }
964
1051
  setupEventListeners() {
@@ -991,9 +1078,13 @@ if (e.target === dialog) {
991
1078
  this.closeDropdown();
992
1079
  }
993
1080
  });
1081
+ dialog.addEventListener("cancel", (e) => {
1082
+ e.preventDefault();
1083
+ this.closeDropdown();
1084
+ this.focus();
1085
+ });
1086
+ this.syncOnOutsideClose(dialog, () => this.closeDropdown());
994
1087
  }
995
- this._keyboardHandler = this.handleKeyboard.bind(this);
996
- document.addEventListener("keydown", this._keyboardHandler);
997
1088
  }
998
1089
  buildStubClasses() {
999
1090
  const classes = [];
@@ -1187,10 +1278,6 @@ this.setupMobileEventListeners();
1187
1278
  this.updateSelectionDisplay();
1188
1279
  }
1189
1280
  setupMobileEventListeners() {
1190
- if (!this._keyboardHandler) {
1191
- this._keyboardHandler = this.handleKeyboard.bind(this);
1192
- document.addEventListener("keydown", this._keyboardHandler);
1193
- }
1194
1281
  const shadow = this.shadowRoot;
1195
1282
  const stub = shadow.querySelector(".select-stub");
1196
1283
  const optionsSlot = shadow.querySelector("#options-slot");
@@ -1225,9 +1312,17 @@ this.closeMobileModal();
1225
1312
  dialog.addEventListener("cancel", (e) => {
1226
1313
  e.preventDefault();
1227
1314
  this.closeMobileModal();
1315
+ this.focus();
1228
1316
  });
1317
+ this.syncOnOutsideClose(dialog, () => this.closeMobileModal());
1229
1318
  }
1230
1319
  }
1320
+ syncOnOutsideClose(dialog, close) {
1321
+ new MutationObserver(() => {
1322
+ if (this._state.open && !dialog.open)
1323
+ close();
1324
+ }).observe(dialog, { attributes: true, attributeFilter: ["open"] });
1325
+ }
1231
1326
  handleMobileStubClick(e) {
1232
1327
  e.preventDefault();
1233
1328
  e.stopPropagation();
@@ -1421,7 +1516,7 @@ return;
1421
1516
  this.updateComponentValue(current.filter((v) => v !== value), true, "remove", value);
1422
1517
  }
1423
1518
  clear() {
1424
- this.updateComponentValue([], true, "clear", null);
1519
+ this.updateComponentValue([], true, "clear", null, false);
1425
1520
  }
1426
1521
  get value() {
1427
1522
  return this.getSelectedValues().join(",");
@@ -1489,6 +1584,12 @@ return this.getProperty("required");
1489
1584
  set required(value) {
1490
1585
  this.setProperty("required", value);
1491
1586
  }
1587
+ get error() {
1588
+ return this.getProperty("error");
1589
+ }
1590
+ set error(value) {
1591
+ this.setProperty("error", value);
1592
+ }
1492
1593
  get notSearchable() {
1493
1594
  return this.getProperty("notSearchable");
1494
1595
  }
@@ -1530,8 +1631,14 @@ get flavor() {
1530
1631
  return this.getProperty("flavor");
1531
1632
  }
1532
1633
  set flavor(value) {
1634
+ this.removeAttribute(ERROR_FLAVOR);
1533
1635
  this.setProperty("flavor", value);
1534
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
+ }
1535
1642
  get form() {
1536
1643
  return this._internals.form;
1537
1644
  }
@@ -1592,6 +1699,11 @@ type: "boolean",
1592
1699
  visual: true,
1593
1700
  default: false,
1594
1701
  },
1702
+ error: {
1703
+ type: "string",
1704
+ visual: true,
1705
+ default: "",
1706
+ },
1595
1707
  noClear: {
1596
1708
  type: "boolean",
1597
1709
  visual: true,
@@ -84,7 +84,9 @@ export declare class TySwitch extends TyComponent<SwitchState> implements TySwit
84
84
  protected render(): void;
85
85
  /** Focus the switch itself (the host is not focusable; its role element is). */
86
86
  focus(options?: FocusOptions): void;
87
- /** required + unchecked => valueMissing; otherwise valid. */
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). */
88
90
  private updateValidity;
89
91
  get checked(): boolean;
90
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();
@@ -159,7 +160,7 @@ this.applyValidity({});
159
160
  return;
160
161
  }
161
162
  if (this.required && !this.checked) {
162
- 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);
163
164
  }
164
165
  else {
165
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;
@@ -129,6 +153,14 @@ export declare class TyTextarea extends TyComponent<TextareaState> implements Ty
129
153
  focus(options?: FocusOptions): void;
130
154
  get required(): boolean;
131
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);
132
164
  get error(): string;
133
165
  set error(v: string);
134
166
  get size(): 'xs' | 'sm' | 'md' | 'lg' | 'xl';
@@ -153,8 +185,6 @@ export declare class TyTextarea extends TyComponent<TextareaState> implements Ty
153
185
  private _destroyScrollbar;
154
186
  private emitValueEvent;
155
187
  private handleInputEvent;
156
- /** The user typed since the field was created: blur then shows its validation message. */
157
- private _userEdited;
158
188
  private _valueAtFocus;
159
189
  private handleFocusEvent;
160
190
  private handleBlurEvent;
@@ -162,8 +192,18 @@ export declare class TyTextarea extends TyComponent<TextareaState> implements Ty
162
192
  private applyHeightConstraints;
163
193
  private setupTextareaEvents;
164
194
  protected render(): void;
165
- /** required + empty => valueMissing; otherwise valid. Disabled fields are
166
- * 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
+ */
167
207
  private updateValidity;
168
208
  }
169
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();
@@ -24,7 +25,6 @@ this._internals.setFormValue(this.value || '');
24
25
  this.updateValidity();
25
26
  this.emitValueEvent('input', e);
26
27
  };
27
- this._userEdited = false;
28
28
  this._valueAtFocus = null;
29
29
  this.handleFocusEvent = () => {
30
30
  this._isFocused = true;
@@ -76,6 +76,14 @@ this.shadowRoot?.querySelector('textarea')?.focus(options);
76
76
  }
77
77
  get required() { return this.getProperty('required'); }
78
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 ?? ''); }
79
87
  get error() { return this.getProperty('error'); }
80
88
  set error(v) { this.setProperty('error', v); }
81
89
  get size() {
@@ -257,18 +265,19 @@ const existingError = root.querySelector('.error-message');
257
265
  if (existingContainer && existingTextarea) {
258
266
  if (this.label) {
259
267
  if (existingLabel) {
260
- existingLabel.innerHTML = this.label +
261
- (this.required ? ` <span class="required-icon">${REQUIRED_ICON}</span>` : '');
262
- existingLabel.style.display = 'block';
268
+ existingLabel.innerHTML = `${this.label}${this.required ? `<span class="required-icon">${REQUIRED_ICON_SVG}</span>` : ''}`;
269
+ existingLabel.style.display = 'flex';
263
270
  }
264
271
  }
265
272
  else if (existingLabel) {
266
273
  existingLabel.style.display = 'none';
267
274
  }
275
+ if (existingTextarea.value !== (this.value || ''))
268
276
  existingTextarea.value = this.value || '';
269
277
  existingTextarea.placeholder = this.placeholder || '';
270
278
  existingTextarea.disabled = this.disabled;
271
279
  existingTextarea.required = this.required;
280
+ this.syncConstraintAttributes(existingTextarea);
272
281
  existingTextarea.className = this.buildClassList();
273
282
  if (this.label)
274
283
  existingTextarea.setAttribute('aria-label', this.label);
@@ -308,9 +317,8 @@ container.className = 'textarea-container';
308
317
  const labelEl = document.createElement('label');
309
318
  labelEl.className = 'ty-field-label';
310
319
  if (this.label) {
311
- labelEl.innerHTML = this.label +
312
- (this.required ? ` <span class="required-icon">${REQUIRED_ICON}</span>` : '');
313
- labelEl.style.display = 'block';
320
+ labelEl.innerHTML = `${this.label}${this.required ? `<span class="required-icon">${REQUIRED_ICON_SVG}</span>` : ''}`;
321
+ labelEl.style.display = 'flex';
314
322
  }
315
323
  else {
316
324
  labelEl.style.display = 'none';
@@ -320,6 +328,7 @@ textareaEl.value = this.value || '';
320
328
  textareaEl.placeholder = this.placeholder || '';
321
329
  textareaEl.disabled = this.disabled;
322
330
  textareaEl.required = this.required;
331
+ this.syncConstraintAttributes(textareaEl);
323
332
  textareaEl.className = this.buildClassList();
324
333
  if (this.label)
325
334
  textareaEl.setAttribute('aria-label', this.label);
@@ -371,6 +380,11 @@ this._textareaEl = textareaEl;
371
380
  }
372
381
  this.updateValidity();
373
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
+ }
374
388
  updateValidity() {
375
389
  const ta = this.shadowRoot?.querySelector('textarea');
376
390
  if (this.disabled) {
@@ -378,12 +392,22 @@ this.applyValidity({});
378
392
  return;
379
393
  }
380
394
  const current = ta ? ta.value : String(this.value ?? '');
381
- if (this.required && current.trim() === '') {
382
- 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);
383
398
  }
384
399
  else {
385
400
  this.applyValidity({});
386
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);
387
411
  }
388
412
  }
389
413
  TyTextarea.formAssociated = true;
@@ -419,6 +443,26 @@ type: 'boolean',
419
443
  visual: true,
420
444
  default: false
421
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
+ },
422
466
  error: {
423
467
  type: 'string',
424
468
  visual: true,
@@ -69,11 +69,25 @@ color: var(--ty-button-color, ${ink('-strong')});
69
69
  export const buttonCustomFlavorCss = (base) => solidFlavor(base, 'neutral') + outlinedFlavor(base, 'neutral') + ghostFlavor(base, 'neutral');
70
70
  export const buttonStyles = `
71
71
  :host {
72
- display: inline-block;
72
+ --_rung: var(--ty-button-height);
73
+ display: inline-grid;
74
+ grid-template: minmax(var(--_rung), 1fr) / minmax(min-content, 1fr);
75
+ height: fit-content;
73
76
  font-family: inherit;
74
77
  }
78
+ :host([action]) {
79
+ --_rung: var(--ty-button-action-size);
80
+ }
81
+ :host([action]:not([wide])) {
82
+ width: fit-content;
83
+ aspect-ratio: 1;
84
+ }
75
85
  :host([wide]) {
76
- display: flex;
86
+ display: grid;
87
+ flex-grow: 1;
88
+ grid-template-rows: minmax(var(--_rung), max-content);
89
+ }
90
+ :host([wide]) button {
77
91
  flex-grow: 1;
78
92
  }
79
93
  :host {
@@ -140,6 +154,7 @@ align-items: center;
140
154
  justify-content: center;
141
155
  gap: var(--ty-button-gap);
142
156
  height: var(--ty-button-height);
157
+ min-height: 100%;
143
158
  padding: var(--ty-button-pad-y) var(--ty-button-pad-x);
144
159
  font-size: var(--ty-button-font);
145
160
  line-height: var(--ty-button-leading);
@@ -214,13 +229,11 @@ justify-content: center;
214
229
  ::slotted(ty-icon) {
215
230
  flex-shrink: 0;
216
231
  }
217
- :host([wide]) button {
218
- flex-grow: 1;
219
- }
220
232
  button.action {
221
233
  gap: 0px !important;
222
- height: var(--ty-button-action-size);
223
- width: var(--ty-button-action-size);
234
+ height: auto;
235
+ min-width: var(--ty-button-action-size);
236
+ min-height: var(--ty-button-action-size);
224
237
  padding: 0px !important;
225
238
  }
226
239
  button.action ::slotted(ty-icon) {
@@ -272,18 +285,6 @@ button.pill.xs { padding-left: 1em; padding-right: 1em; }
272
285
  button.pill.sm { padding-left: 1.125em; padding-right: 1.125em; }
273
286
  button.pill.lg { padding-left: 1.5em; padding-right: 1.5em; }
274
287
  button.pill.xl { padding-left: 1.75em; padding-right: 1.75em; }
275
- button.pill:has(ty-icon:only-child),
276
- button.pill:has(slot[name="start"]:only-child),
277
- button.pill:has(slot[name="end"]:only-child) {
278
- padding: 0;
279
- aspect-ratio: 1;
280
- min-width: var(--ty-size-md);
281
- }
282
- button.pill.xs:has(ty-icon:only-child) { min-width: 1.375rem; min-height: 1.375rem; }
283
- button.pill.sm:has(ty-icon:only-child) { min-width: 1.5rem; min-height: 1.5rem; }
284
- button.pill.md:has(ty-icon:only-child) { min-width: 2rem; min-height: 2rem; }
285
- button.pill.lg:has(ty-icon:only-child) { min-width: 2.25rem; min-height: 2.25rem; }
286
- button.pill.xl:has(ty-icon:only-child) { min-width: 2.5rem; min-height: 2.5rem; }
287
288
  button.solid {
288
289
  --_ring: var(--ty-color-neutral);
289
290
  --_solid-bg: var(--ty-button-bg, var(--ty-solid-neutral));
@@ -134,6 +134,23 @@ border-color: var(--date-picker-accent-bold, var(--ty-date-picker-border-focus))
134
134
  box-shadow: 0 0 0 3px var(--date-picker-ring, var(--ty-date-picker-shadow-focus));
135
135
  }
136
136
  ${FLAVORS.filter((f) => f !== "neutral").map((f) => datePickerFlavor(f)).join("")}
137
+ .error-message {
138
+ font-size: var(--ty-font-xs);
139
+ line-height: var(--ty-leading-xs);
140
+ letter-spacing: var(--ty-tracking-xs);
141
+ color: var(--ty-color-danger);
142
+ margin-top: 4px;
143
+ padding-left: var(--ty-field-outer-pad-x);
144
+ }
145
+ .date-picker-stub.error {
146
+ border-color: var(--ty-color-danger);
147
+ background: var(--ty-bg-danger-soft);
148
+ }
149
+ .date-picker-stub.error:focus,
150
+ .date-picker-stub.error.open {
151
+ border-color: var(--ty-color-danger-bold);
152
+ box-shadow: 0 0 0 3px var(--ty-date-picker-shadow-focus);
153
+ }
137
154
  .stub-text {
138
155
  flex: 1;
139
156
  white-space: nowrap;
@@ -239,6 +239,32 @@ background: transparent;
239
239
  border-color: transparent;
240
240
  box-shadow: none;
241
241
  }
242
+ .error-message {
243
+ font-size: var(--ty-font-xs);
244
+ line-height: var(--ty-leading-xs);
245
+ letter-spacing: var(--ty-tracking-xs);
246
+ color: var(--ty-color-danger);
247
+ margin-top: 4px;
248
+ padding-left: var(--ty-field-outer-pad-x);
249
+ contain: inline-size;
250
+ }
251
+ :host([compact]) .error-message,
252
+ :host([has-trigger]) .error-message {
253
+ box-sizing: border-box;
254
+ width: max(100%, 20em);
255
+ }
256
+ :host([has-trigger]) .error-message {
257
+ padding-left: 0;
258
+ }
259
+ .select-stub.error:not(.custom-trigger) {
260
+ border-color: var(--ty-color-danger);
261
+ background: var(--ty-bg-danger-soft);
262
+ }
263
+ .select-stub.error:not(.custom-trigger):focus,
264
+ .select-stub.error:not(.custom-trigger):focus-visible {
265
+ border-color: var(--ty-color-danger-bold);
266
+ box-shadow: 0 0 0 3px var(--input-shadow-focus, var(--ty-input-shadow-focus));
267
+ }
242
268
  .dropdown-mode-mobile .dropdown-wrapper:has(.mobile-dialog[open]) .select-chips {
243
269
  display: none;
244
270
  }