tyrell-components 1.0.0-TC57 → 1.0.0-TC59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/css/tyrell-colors-static.css +14 -0
  2. package/css/tyrell-theme.css +1 -0
  3. package/css/tyrell.css +8 -6
  4. package/dist/tyrell-colors-static.css +1 -1
  5. package/dist/tyrell-theme.css +1 -1
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.d.ts +20 -15
  8. package/lib/base/ty-component.js +58 -25
  9. package/lib/components/button.d.ts +14 -0
  10. package/lib/components/button.js +55 -7
  11. package/lib/components/calendar-month.d.ts +7 -1
  12. package/lib/components/calendar-month.js +35 -28
  13. package/lib/components/calendar-navigation.js +19 -6
  14. package/lib/components/calendar.d.ts +25 -4
  15. package/lib/components/calendar.js +98 -67
  16. package/lib/components/checkbox.d.ts +6 -1
  17. package/lib/components/checkbox.js +20 -6
  18. package/lib/components/copy.d.ts +2 -0
  19. package/lib/components/copy.js +15 -31
  20. package/lib/components/date-picker.d.ts +52 -19
  21. package/lib/components/date-picker.js +211 -147
  22. package/lib/components/file-upload.d.ts +2 -1
  23. package/lib/components/file-upload.js +8 -3
  24. package/lib/components/input.d.ts +4 -6
  25. package/lib/components/input.js +37 -62
  26. package/lib/components/modal.js +86 -9
  27. package/lib/components/option.js +2 -5
  28. package/lib/components/popup.js +99 -11
  29. package/lib/components/radio.d.ts +8 -1
  30. package/lib/components/radio.js +14 -29
  31. package/lib/components/resize-observer.js +2 -0
  32. package/lib/components/scroll-container.js +2 -0
  33. package/lib/components/select.d.ts +53 -6
  34. package/lib/components/select.js +127 -29
  35. package/lib/components/switch.d.ts +7 -1
  36. package/lib/components/switch.js +20 -6
  37. package/lib/components/tabs.js +1 -1
  38. package/lib/components/textarea.d.ts +45 -4
  39. package/lib/components/textarea.js +75 -18
  40. package/lib/index.d.ts +1 -0
  41. package/lib/index.js +1 -0
  42. package/lib/styles/button.js +21 -19
  43. package/lib/styles/calendar-month.js +1 -1
  44. package/lib/styles/calendar.js +3 -0
  45. package/lib/styles/checkbox.js +11 -5
  46. package/lib/styles/date-picker.js +15 -3
  47. package/lib/styles/input.js +5 -9
  48. package/lib/styles/option.d.ts +1 -1
  49. package/lib/styles/option.js +4 -0
  50. package/lib/styles/popup.d.ts +1 -1
  51. package/lib/styles/popup.js +2 -0
  52. package/lib/styles/radio.js +10 -4
  53. package/lib/styles/select-base.js +24 -3
  54. package/lib/styles/select.d.ts +1 -1
  55. package/lib/styles/select.js +4 -0
  56. package/lib/styles/switch.js +10 -5
  57. package/lib/styles/tag.js +2 -1
  58. package/lib/styles/textarea.d.ts +1 -1
  59. package/lib/styles/textarea.js +7 -0
  60. package/lib/utils/calendar-utils.d.ts +49 -0
  61. package/lib/utils/calendar-utils.js +63 -6
  62. package/lib/utils/scroll-lock.d.ts +10 -4
  63. package/lib/utils/scroll-lock.js +102 -2
  64. package/lib/utils/upgrade-properties.d.ts +21 -0
  65. package/lib/utils/upgrade-properties.js +47 -0
  66. package/lib/utils/validity.d.ts +89 -5
  67. package/lib/utils/validity.js +198 -25
  68. package/lib/version.d.ts +1 -1
  69. package/lib/version.js +1 -1
  70. package/package.json +2 -1
  71. package/lib/base/ty-component.d.ts.map +0 -1
  72. package/lib/base/ty-component.js.map +0 -1
  73. package/lib/components/button.d.ts.map +0 -1
  74. package/lib/components/button.js.map +0 -1
  75. package/lib/components/calendar-month.d.ts.map +0 -1
  76. package/lib/components/calendar-month.js.map +0 -1
  77. package/lib/components/calendar-navigation.d.ts.map +0 -1
  78. package/lib/components/calendar-navigation.js.map +0 -1
  79. package/lib/components/calendar.d.ts.map +0 -1
  80. package/lib/components/calendar.js.map +0 -1
  81. package/lib/components/checkbox.d.ts.map +0 -1
  82. package/lib/components/checkbox.js.map +0 -1
  83. package/lib/components/copy.d.ts.map +0 -1
  84. package/lib/components/copy.js.map +0 -1
  85. package/lib/components/date-picker.d.ts.map +0 -1
  86. package/lib/components/date-picker.js.map +0 -1
  87. package/lib/components/dropdown.d.ts +0 -403
  88. package/lib/components/dropdown.d.ts.map +0 -1
  89. package/lib/components/dropdown.js +0 -1315
  90. package/lib/components/dropdown.js.map +0 -1
  91. package/lib/components/icon.d.ts.map +0 -1
  92. package/lib/components/icon.js.map +0 -1
  93. package/lib/components/input.d.ts.map +0 -1
  94. package/lib/components/input.js.map +0 -1
  95. package/lib/components/modal.d.ts.map +0 -1
  96. package/lib/components/modal.js.map +0 -1
  97. package/lib/components/multiselect.d.ts +0 -389
  98. package/lib/components/multiselect.d.ts.map +0 -1
  99. package/lib/components/multiselect.js +0 -1301
  100. package/lib/components/multiselect.js.map +0 -1
  101. package/lib/components/option.d.ts.map +0 -1
  102. package/lib/components/option.js.map +0 -1
  103. package/lib/components/popup.d.ts.map +0 -1
  104. package/lib/components/popup.js.map +0 -1
  105. package/lib/components/resize-observer.d.ts.map +0 -1
  106. package/lib/components/resize-observer.js.map +0 -1
  107. package/lib/components/scroll-container.d.ts.map +0 -1
  108. package/lib/components/scroll-container.js.map +0 -1
  109. package/lib/components/step.d.ts.map +0 -1
  110. package/lib/components/step.js.map +0 -1
  111. package/lib/components/tab.d.ts.map +0 -1
  112. package/lib/components/tab.js.map +0 -1
  113. package/lib/components/tabs.d.ts.map +0 -1
  114. package/lib/components/tabs.js.map +0 -1
  115. package/lib/components/tag.d.ts.map +0 -1
  116. package/lib/components/tag.js.map +0 -1
  117. package/lib/components/textarea.d.ts.map +0 -1
  118. package/lib/components/textarea.js.map +0 -1
  119. package/lib/components/tooltip.d.ts.map +0 -1
  120. package/lib/components/tooltip.js.map +0 -1
  121. package/lib/components/wizard.d.ts.map +0 -1
  122. package/lib/components/wizard.js.map +0 -1
  123. package/lib/icons/fontawesome/brands.d.ts.map +0 -1
  124. package/lib/icons/fontawesome/brands.js.map +0 -1
  125. package/lib/icons/fontawesome/regular.d.ts.map +0 -1
  126. package/lib/icons/fontawesome/regular.js.map +0 -1
  127. package/lib/icons/fontawesome/solid.d.ts.map +0 -1
  128. package/lib/icons/fontawesome/solid.js.map +0 -1
  129. package/lib/icons/heroicons/micro.d.ts.map +0 -1
  130. package/lib/icons/heroicons/micro.js.map +0 -1
  131. package/lib/icons/heroicons/mini.d.ts.map +0 -1
  132. package/lib/icons/heroicons/mini.js.map +0 -1
  133. package/lib/icons/heroicons/outline.d.ts.map +0 -1
  134. package/lib/icons/heroicons/outline.js.map +0 -1
  135. package/lib/icons/heroicons/solid.d.ts.map +0 -1
  136. package/lib/icons/heroicons/solid.js.map +0 -1
  137. package/lib/icons/lucide.d.ts.map +0 -1
  138. package/lib/icons/lucide.js.map +0 -1
  139. package/lib/icons/material/filled.d.ts.map +0 -1
  140. package/lib/icons/material/filled.js.map +0 -1
  141. package/lib/icons/material/outlined.d.ts.map +0 -1
  142. package/lib/icons/material/outlined.js.map +0 -1
  143. package/lib/icons/material/round.d.ts.map +0 -1
  144. package/lib/icons/material/round.js.map +0 -1
  145. package/lib/icons/material/sharp.d.ts.map +0 -1
  146. package/lib/icons/material/sharp.js.map +0 -1
  147. package/lib/icons/material/two-tone.d.ts.map +0 -1
  148. package/lib/icons/material/two-tone.js.map +0 -1
  149. package/lib/index.d.ts.map +0 -1
  150. package/lib/index.js.map +0 -1
  151. package/lib/styles/button.d.ts.map +0 -1
  152. package/lib/styles/button.js.map +0 -1
  153. package/lib/styles/calendar-month.d.ts.map +0 -1
  154. package/lib/styles/calendar-month.js.map +0 -1
  155. package/lib/styles/calendar-navigation.d.ts.map +0 -1
  156. package/lib/styles/calendar-navigation.js.map +0 -1
  157. package/lib/styles/calendar.d.ts.map +0 -1
  158. package/lib/styles/calendar.js.map +0 -1
  159. package/lib/styles/copy.d.ts.map +0 -1
  160. package/lib/styles/copy.js.map +0 -1
  161. package/lib/styles/date-picker.d.ts.map +0 -1
  162. package/lib/styles/date-picker.js.map +0 -1
  163. package/lib/styles/dropdown.d.ts +0 -12
  164. package/lib/styles/dropdown.d.ts.map +0 -1
  165. package/lib/styles/dropdown.js +0 -725
  166. package/lib/styles/dropdown.js.map +0 -1
  167. package/lib/styles/icon.d.ts.map +0 -1
  168. package/lib/styles/icon.js.map +0 -1
  169. package/lib/styles/input.d.ts.map +0 -1
  170. package/lib/styles/input.js.map +0 -1
  171. package/lib/styles/modal.d.ts.map +0 -1
  172. package/lib/styles/modal.js.map +0 -1
  173. package/lib/styles/multiselect.d.ts +0 -6
  174. package/lib/styles/multiselect.d.ts.map +0 -1
  175. package/lib/styles/multiselect.js +0 -568
  176. package/lib/styles/multiselect.js.map +0 -1
  177. package/lib/styles/option.d.ts.map +0 -1
  178. package/lib/styles/option.js.map +0 -1
  179. package/lib/styles/popup.d.ts.map +0 -1
  180. package/lib/styles/popup.js.map +0 -1
  181. package/lib/styles/resize-observer.d.ts.map +0 -1
  182. package/lib/styles/resize-observer.js.map +0 -1
  183. package/lib/styles/scroll-container.d.ts.map +0 -1
  184. package/lib/styles/scroll-container.js.map +0 -1
  185. package/lib/styles/step.d.ts.map +0 -1
  186. package/lib/styles/step.js.map +0 -1
  187. package/lib/styles/tab.d.ts.map +0 -1
  188. package/lib/styles/tab.js.map +0 -1
  189. package/lib/styles/tabs.d.ts.map +0 -1
  190. package/lib/styles/tabs.js.map +0 -1
  191. package/lib/styles/tag.d.ts.map +0 -1
  192. package/lib/styles/tag.js.map +0 -1
  193. package/lib/styles/textarea.d.ts.map +0 -1
  194. package/lib/styles/textarea.js.map +0 -1
  195. package/lib/styles/tooltip.d.ts.map +0 -1
  196. package/lib/styles/tooltip.js.map +0 -1
  197. package/lib/styles/wizard.d.ts.map +0 -1
  198. package/lib/styles/wizard.js.map +0 -1
  199. package/lib/types/common.d.ts.map +0 -1
  200. package/lib/types/common.js.map +0 -1
  201. package/lib/utils/calendar-utils.d.ts.map +0 -1
  202. package/lib/utils/calendar-utils.js.map +0 -1
  203. package/lib/utils/icon-registry.d.ts.map +0 -1
  204. package/lib/utils/icon-registry.js.map +0 -1
  205. package/lib/utils/locale.d.ts.map +0 -1
  206. package/lib/utils/locale.js.map +0 -1
  207. package/lib/utils/number-format.d.ts.map +0 -1
  208. package/lib/utils/number-format.js.map +0 -1
  209. package/lib/utils/parse-boolean.d.ts +0 -39
  210. package/lib/utils/parse-boolean.d.ts.map +0 -1
  211. package/lib/utils/parse-boolean.js +0 -23
  212. package/lib/utils/parse-boolean.js.map +0 -1
  213. package/lib/utils/positioning.d.ts.map +0 -1
  214. package/lib/utils/positioning.js.map +0 -1
  215. package/lib/utils/property-capture.d.ts.map +0 -1
  216. package/lib/utils/property-capture.js.map +0 -1
  217. package/lib/utils/property-manager.d.ts.map +0 -1
  218. package/lib/utils/property-manager.js.map +0 -1
  219. package/lib/utils/resize-observer.d.ts.map +0 -1
  220. package/lib/utils/resize-observer.js.map +0 -1
  221. package/lib/utils/scroll-lock.d.ts.map +0 -1
  222. package/lib/utils/scroll-lock.js.map +0 -1
  223. package/lib/utils/styles.d.ts.map +0 -1
  224. package/lib/utils/styles.js.map +0 -1
  225. package/lib/version.d.ts.map +0 -1
  226. package/lib/version.js.map +0 -1
@@ -17,6 +17,7 @@ export interface TyRadioElement extends HTMLElement {
17
17
  flavor: Flavor;
18
18
  }
19
19
  export declare class TyRadio extends TyComponent<RadioState> implements TyRadioElement {
20
+ protected static labelClicks: boolean;
20
21
  protected static properties: {
21
22
  value: {
22
23
  type: "string";
@@ -165,9 +166,15 @@ export declare class TyRadioGroup extends TyComponent<RadioGroupState> implement
165
166
  private getRadios;
166
167
  private syncChildren;
167
168
  /** Report `required` to the owning form — without this the asterisk
168
- showed but an empty group submitted anyway. */
169
+ showed but an empty group submitted anyway. The message is a native
170
+ radio's; it needs a name, a nameless radio never reports valueMissing. */
169
171
  private updateValidity;
170
172
  private handleRadioSelect;
173
+ /**
174
+ * Select a value as the user did: input + change, like a native radio,
175
+ * whether by click, arrow keys or Space. Nothing fires when it is already selected.
176
+ */
177
+ private select;
171
178
  /**
172
179
  * Arrow-key navigation within group (W3C radio practice: arrow keys move
173
180
  * focus and selection together).
@@ -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();
@@ -133,6 +134,7 @@ set size(v) { this.setProperty("size", v); }
133
134
  get flavor() { return this.getProperty("flavor"); }
134
135
  set flavor(v) { this.setProperty("flavor", v); }
135
136
  }
137
+ TyRadio.labelClicks = true;
136
138
  TyRadio.properties = {
137
139
  value: { type: "string", default: "" },
138
140
  checked: {
@@ -223,7 +225,7 @@ updateValidity() {
223
225
  if (this.required && !this.disabled && !this.value) {
224
226
  const anchor = this.getRadios().find((r) => !r.disabled);
225
227
  const target = anchor?.shadowRoot?.querySelector(".radio-container");
226
- this.applyValidity({ valueMissing: true }, "Please select an option", target ?? undefined);
228
+ this.applyValidity({ valueMissing: true }, nativeMessage("input", { type: "radio", name: "ty", required: true }), target ?? undefined);
227
229
  }
228
230
  else {
229
231
  this.applyValidity({});
@@ -236,16 +238,17 @@ const detail = e.detail;
236
238
  const newValue = detail?.value;
237
239
  if (typeof newValue !== "string" || newValue === this.value)
238
240
  return;
239
- this.value = newValue;
241
+ this.select(newValue, e);
242
+ }
243
+ select(value, originalEvent) {
244
+ if (value === this.value)
245
+ return;
246
+ this.value = value;
240
247
  this.syncChildren();
248
+ const detail = { value, formValue: value, originalEvent };
241
249
  setTimeout(() => {
242
- const eventDetail = {
243
- value: newValue,
244
- formValue: newValue,
245
- originalEvent: e,
246
- };
247
- this.dispatchEvent(new CustomEvent("input", { detail: eventDetail, bubbles: true, composed: true }));
248
- this.dispatchEvent(new CustomEvent("change", { detail: eventDetail, bubbles: true, composed: true }));
250
+ this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true, composed: true }));
251
+ this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true, composed: true }));
249
252
  }, 0);
250
253
  }
251
254
  handleKeydown(e) {
@@ -270,16 +273,7 @@ nextIndex = current
270
273
  else if (["Space", " ", "Enter"].includes(e.key)) {
271
274
  if (current) {
272
275
  e.preventDefault();
273
- this.value = current.value;
274
- this.syncChildren();
275
- const eventDetail = {
276
- value: current.value,
277
- formValue: current.value,
278
- originalEvent: e,
279
- };
280
- setTimeout(() => {
281
- this.dispatchEvent(new CustomEvent("change", { detail: eventDetail, bubbles: true, composed: true }));
282
- }, 0);
276
+ this.select(current.value, e);
283
277
  }
284
278
  return;
285
279
  }
@@ -289,16 +283,7 @@ return;
289
283
  const next = radios[nextIndex];
290
284
  if (next) {
291
285
  next.focus();
292
- this.value = next.value;
293
- this.syncChildren();
294
- const eventDetail = {
295
- value: next.value,
296
- formValue: next.value,
297
- originalEvent: e,
298
- };
299
- setTimeout(() => {
300
- this.dispatchEvent(new CustomEvent("change", { detail: eventDetail, bubbles: true, composed: true }));
301
- }, 0);
286
+ this.select(next.value, e);
302
287
  }
303
288
  }
304
289
  removeEventListeners() {
@@ -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,6 +195,8 @@ export declare class TySelect extends TyComponent<SelectState> {
190
195
  private _childObserver;
191
196
  private _selectedClone;
192
197
  private _customFlavorSheet;
198
+ private _ariaTrigger;
199
+ private _ariaObserver;
193
200
  constructor();
194
201
  /** TyComponent handles property capture automatically. */
195
202
  protected onConnect(): void;
@@ -207,7 +214,11 @@ export declare class TySelect extends TyComponent<SelectState> {
207
214
  private applyLoadingState;
208
215
  /** Form value: a plain entry for single, FormData with repeated entries for multiple (HTMX standard). */
209
216
  protected getFormValue(): FormDataEntryValue | FormData | null;
210
- /** Focus the trigger: a slotted one, else the field. The host itself isn't focusable. */
217
+ /**
218
+ * Focus the trigger: a slotted one, else the field. The host itself isn't
219
+ * focusable. A slotted trigger that can't take focus (a <span>, a disabled
220
+ * button) leaves it to the field.
221
+ */
211
222
  focus(options?: FocusOptions): void;
212
223
  private parseValue;
213
224
  /** Initialize component state from the value property / pre-selected children. */
@@ -239,6 +250,12 @@ export declare class TySelect extends TyComponent<SelectState> {
239
250
  * Get all option elements from the component (ty-option preferred, ty-tag accepted)
240
251
  */
241
252
  private getTagElements;
253
+ /**
254
+ * The options on offer: all but the ones the author hid (`hidden`, like a
255
+ * native <option hidden>). The search filters these and the keyboard walks
256
+ * them; a hidden option can still be the value.
257
+ */
258
+ private getOfferedTags;
242
259
  private getTagData;
243
260
  /**
244
261
  * Select an option - attribute only. The option stays in the list with
@@ -337,11 +354,17 @@ export declare class TySelect extends TyComponent<SelectState> {
337
354
  /**
338
355
  * Next tag index in `direction` (-1/1) that isn't disabled, wrapping
339
356
  * around — mirrors handleTagClick's disabled check so keyboard nav can
340
- * never land on/select what a click already refuses to. Returns -1 if
341
- * every tag is disabled (or there are none).
357
+ * never land on/select what a click already refuses to. Nor on one out of
358
+ * sight: `tags` is the list as it was offered, and an option can get
359
+ * `hidden` while it's open (a server patch). Returns -1 if every tag is
360
+ * skipped (or there are none).
342
361
  */
343
362
  private nextEnabledTagIndex;
344
363
  private filterTags;
364
+ /**
365
+ * Search filtering marks the options it leaves out `filtered-out`, never
366
+ * `hidden`: that one is the author's (see getOfferedTags).
367
+ */
345
368
  private updateTagVisibility;
346
369
  private updateOptionsVisibility;
347
370
  private clearHighlights;
@@ -365,12 +388,22 @@ export declare class TySelect extends TyComponent<SelectState> {
365
388
  * `compact` after connect never showed up.
366
389
  */
367
390
  private syncFieldChrome;
391
+ /**
392
+ * A slotted trigger is what takes focus (focus()), so it carries the error
393
+ * state too. aria-describedby can't reach into our shadow root from the
394
+ * light DOM: the message goes in aria-description instead. The trigger is
395
+ * the author's markup, so a morph can strip these while the error stays:
396
+ * a watch on them puts them back. Writes only on a real change, or the
397
+ * watch would feed itself.
398
+ */
399
+ private syncTriggerAria;
368
400
  /**
369
401
  * Reflect constraint validation to the owning <form> via ElementInternals.
370
402
  * Without this `required` is only cosmetic — form.checkValidity() (and a
371
403
  * 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.
404
+ * ty-input's updateValidity(): empty + required => valueMissing, with a
405
+ * native <select>'s message; disabled fields are barred from constraint
406
+ * validation.
374
407
  */
375
408
  private updateValidity;
376
409
  /**
@@ -383,6 +416,16 @@ export declare class TySelect extends TyComponent<SelectState> {
383
416
  private renderDesktop;
384
417
  private renderMobile;
385
418
  private setupMobileEventListeners;
419
+ /**
420
+ * The dialog was closed by someone else — a ty-modal around this select
421
+ * closes the dialogs still open inside it. Run our own close too, so the
422
+ * open state, aria-expanded and the scroll lock follow. Watches the `open`
423
+ * attribute rather than the `close` event: that one comes a task later,
424
+ * so right after modal.hide() the stub would still say expanded. Our own
425
+ * close() calls land here with the state already closed, and a dialog
426
+ * reopened since then is open again: both are no-ops.
427
+ */
428
+ private syncOnOutsideClose;
386
429
  private handleMobileStubClick;
387
430
  /** Keyboard-open the (closed) mobile stub — see handleStubKeydown for why this is needed. */
388
431
  private handleMobileStubKeydown;
@@ -444,7 +487,9 @@ export declare class TySelect extends TyComponent<SelectState> {
444
487
  * Clear the entire selection programmatically WITH a change event
445
488
  * (action: 'clear'). No-op if nothing is selected. Exists so slot="trigger"
446
489
  * consumers (custom chrome, built-in clear UI hidden by design) can wire
447
- * their own clear icon's click handler to it.
490
+ * their own clear icon's click handler to it. Shows no validation
491
+ * message (a code reset after a save shouldn't flag the field); the
492
+ * built-in × does.
448
493
  */
449
494
  clear(): void;
450
495
  get value(): string;
@@ -469,6 +514,8 @@ export declare class TySelect extends TyComponent<SelectState> {
469
514
  set noClear(value: boolean);
470
515
  get required(): boolean;
471
516
  set required(value: boolean);
517
+ get error(): string;
518
+ set error(value: string);
472
519
  get notSearchable(): boolean;
473
520
  set notSearchable(value: boolean);
474
521
  get externalSearch(): boolean;
@@ -20,6 +20,7 @@ elementIds.set(element, id);
20
20
  return id;
21
21
  }
22
22
  import { REQUIRED_ICON_SVG } from "../utils/icons.js";
23
+ import { nativeMessage } from "../utils/validity.js";
23
24
  const SEARCH_AUTO_THRESHOLD = 7;
24
25
  const CHEVRON_DOWN_SVG = `<svg viewBox="0 0 20 20" fill="currentColor">
25
26
  <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 +82,8 @@ this._optionsScrollbar = null;
81
82
  this._childObserver = null;
82
83
  this._selectedClone = null;
83
84
  this._customFlavorSheet = null;
85
+ this._ariaTrigger = null;
86
+ this._ariaObserver = 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,21 @@ this.updateMobileSelectedState();
109
112
  this._childObserver.observe(this, { childList: true });
110
113
  this.ensureOptionRoles();
111
114
  this._syncCustomFlavor();
115
+ if (!this._keyboardHandler) {
116
+ this._keyboardHandler = this.handleKeyboard.bind(this);
117
+ document.addEventListener("keydown", this._keyboardHandler, true);
118
+ }
112
119
  }
113
120
  _syncCustomFlavor() {
114
121
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.getProperty("flavor"), ({ base }) => selectCustomFlavorCss(base));
115
122
  }
116
123
  onDisconnect() {
124
+ if (this._state.open) {
125
+ if (this._state.mode === "mobile")
126
+ this.closeMobileModal();
127
+ else
128
+ this.closeDropdown();
129
+ }
117
130
  const shadow = this.shadowRoot;
118
131
  const dialogs = shadow.querySelectorAll("dialog");
119
132
  dialogs.forEach((dialog) => {
@@ -122,7 +135,7 @@ dialog.close();
122
135
  }
123
136
  });
124
137
  if (this._keyboardHandler) {
125
- document.removeEventListener("keydown", this._keyboardHandler);
138
+ document.removeEventListener("keydown", this._keyboardHandler, true);
126
139
  this._keyboardHandler = null;
127
140
  }
128
141
  if (this._searchDebounceTimer !== null) {
@@ -256,11 +269,18 @@ return "";
256
269
  return null;
257
270
  }
258
271
  focus(options) {
272
+ if (this._disabled)
273
+ return;
259
274
  const trigger = this.querySelector(':scope > [slot="trigger"]');
275
+ if (trigger) {
276
+ trigger.focus(options);
277
+ const active = trigger.getRootNode().activeElement;
278
+ if (active && trigger.contains(active))
279
+ return;
280
+ }
260
281
  const stub = this.shadowRoot?.querySelector(".select-stub");
261
- const target = trigger ?? stub;
262
- if (target)
263
- target.focus(options);
282
+ if (stub)
283
+ stub.focus(options);
264
284
  else
265
285
  super.focus(options);
266
286
  }
@@ -304,6 +324,11 @@ this.resolveDeclarativeSelection(); }, { once: true });
304
324
  getTagElements() {
305
325
  return Array.from(this.querySelectorAll("ty-option:not([cloned]), ty-tag:not([cloned])"));
306
326
  }
327
+ getOfferedTags() {
328
+ return this.getTagElements()
329
+ .filter((el) => !el.hasAttribute("hidden"))
330
+ .map((el) => this.getTagData(el));
331
+ }
307
332
  getTagData(element) {
308
333
  const value = element.value ||
309
334
  element.getAttribute("value") ||
@@ -345,7 +370,7 @@ if (!tag.hasAttribute("aria-selected"))
345
370
  tag.setAttribute("aria-selected", "false");
346
371
  });
347
372
  }
348
- updateComponentValue(newValues, dispatchChange = false, action = "set", item = null) {
373
+ updateComponentValue(newValues, dispatchChange = false, action = "set", item = null, reveal = dispatchChange) {
349
374
  const oldValues = this.getSelectedValues();
350
375
  const valueStr = newValues.join(",");
351
376
  if (JSON.stringify(newValues.sort()) !== JSON.stringify(oldValues.sort())) {
@@ -359,6 +384,8 @@ items: this.getSelectedItems(newValues),
359
384
  action,
360
385
  item,
361
386
  });
387
+ if (reveal)
388
+ this.revealValidity();
362
389
  }
363
390
  }
364
391
  }
@@ -468,8 +495,7 @@ chevron.classList.add("open");
468
495
  const searchChevron = shadow.querySelector(".dropdown-search-chevron");
469
496
  if (searchChevron)
470
497
  searchChevron.classList.add("open");
471
- const tags = this.getTagElements().map((el) => this.getTagData(el));
472
- this._state.filteredTags = tags;
498
+ this._state.filteredTags = this.getOfferedTags();
473
499
  this._state.highlightedIndex = -1;
474
500
  this.updateOptionsVisibility(true);
475
501
  this._setupOptionsScrollbar();
@@ -521,7 +547,7 @@ this.fireSearchEvent("");
521
547
  }
522
548
  else {
523
549
  const allTags = this.getTagElements().map((el) => this.getTagData(el));
524
- this._state.filteredTags = allTags;
550
+ this._state.filteredTags = this.getOfferedTags();
525
551
  this.updateTagVisibility(allTags, allTags);
526
552
  this.clearHighlights(allTags);
527
553
  }
@@ -543,7 +569,7 @@ dialog.getBoundingClientRect();
543
569
  dialog.classList.add("open");
544
570
  this._state.open = true;
545
571
  const tags = this.getTagElements().map((el) => this.getTagData(el));
546
- this._state.filteredTags = tags;
572
+ this._state.filteredTags = this.getOfferedTags();
547
573
  this.updateTagVisibility(tags, tags);
548
574
  requestAnimationFrame(() => {
549
575
  this.updateMobileSelectedState();
@@ -583,7 +609,7 @@ this.fireSearchEvent("");
583
609
  }
584
610
  else {
585
611
  const allTags = this.getTagElements();
586
- allTags.forEach((el) => el.removeAttribute("hidden"));
612
+ allTags.forEach((el) => el.removeAttribute("filtered-out"));
587
613
  }
588
614
  this.fireCloseEvent();
589
615
  }
@@ -602,6 +628,7 @@ if (this._disabled || this._readonly) {
602
628
  return;
603
629
  }
604
630
  this.clear();
631
+ this.revealValidity();
605
632
  }
606
633
  handleStubKeydown(e) {
607
634
  if (this._disabled || this._readonly || this._state.open)
@@ -702,7 +729,7 @@ desktopInput.value = "";
702
729
  if (mobileInput)
703
730
  mobileInput.value = "";
704
731
  const allTags = this.getTagElements().map((el) => this.getTagData(el));
705
- this._state.filteredTags = allTags;
732
+ this._state.filteredTags = this.getOfferedTags();
706
733
  this.updateTagVisibility(allTags, allTags);
707
734
  this.updateOptionsVisibility(true);
708
735
  this.clearHighlights(allTags);
@@ -723,7 +750,7 @@ if (this._externalSearch) {
723
750
  this.dispatchSearchEvent(query);
724
751
  return;
725
752
  }
726
- const allTags = this.getTagElements().map((el) => this.getTagData(el));
753
+ const allTags = this.getOfferedTags();
727
754
  const filtered = this.filterTags(allTags, query);
728
755
  this._state.filteredTags = filtered;
729
756
  this._state.highlightedIndex = -1;
@@ -754,6 +781,7 @@ this.closeMobileModal();
754
781
  else {
755
782
  this.closeDropdown();
756
783
  }
784
+ this.focus();
757
785
  break;
758
786
  case "Enter":
759
787
  e.preventDefault();
@@ -790,7 +818,8 @@ let index = fromIndex === -1
790
818
  ? (direction === 1 ? 0 : count - 1)
791
819
  : (fromIndex + direction + count) % count;
792
820
  for (let i = 0; i < count; i++) {
793
- if (!tags[index].element.hasAttribute("disabled"))
821
+ const el = tags[index].element;
822
+ if (!el.hasAttribute("disabled") && !el.hasAttribute("hidden") && !el.hasAttribute("filtered-out"))
794
823
  return index;
795
824
  index = (index + direction + count) % count;
796
825
  }
@@ -806,12 +835,7 @@ return tags.filter(({ text }) => text.toLowerCase().includes(searchLower));
806
835
  updateTagVisibility(filteredTags, allTags) {
807
836
  const visibleValues = new Set(filteredTags.map((tag) => tag.value));
808
837
  allTags.forEach(({ value, element }) => {
809
- if (visibleValues.has(value)) {
810
- element.removeAttribute("hidden");
811
- }
812
- else {
813
- element.setAttribute("hidden", "");
814
- }
838
+ element.toggleAttribute("filtered-out", !visibleValues.has(value));
815
839
  });
816
840
  }
817
841
  updateOptionsVisibility(hasOptions) {
@@ -915,10 +939,26 @@ label.innerHTML = `${this._label}${this._required ? `<span class="required-icon"
915
939
  else {
916
940
  label?.remove();
917
941
  }
942
+ const error = this.getProperty("error") || "";
943
+ const errorId = `select-error-${getElementHash(this)}`;
944
+ let errorLine = container.querySelector(":scope > .error-message");
945
+ if (error) {
946
+ if (!errorLine) {
947
+ errorLine = document.createElement("div");
948
+ errorLine.className = "error-message";
949
+ errorLine.id = errorId;
950
+ container.append(errorLine);
951
+ }
952
+ errorLine.textContent = error;
953
+ }
954
+ else {
955
+ errorLine?.remove();
956
+ }
918
957
  const stub = shadow.querySelector(".select-stub");
919
958
  if (stub) {
920
959
  stub.classList.toggle("disabled", this._disabled);
921
960
  stub.classList.toggle("compact", this._compact);
961
+ stub.classList.toggle("error", !!error);
922
962
  stub.tabIndex = this._disabled ? -1 : 0;
923
963
  stub.toggleAttribute("disabled", this._disabled);
924
964
  if (this._disabled)
@@ -929,11 +969,45 @@ if (this._label)
929
969
  stub.setAttribute("aria-labelledby", labelId);
930
970
  else
931
971
  stub.removeAttribute("aria-labelledby");
972
+ if (error) {
973
+ stub.setAttribute("aria-invalid", "true");
974
+ stub.setAttribute("aria-describedby", errorId);
975
+ }
976
+ else {
977
+ stub.removeAttribute("aria-invalid");
978
+ stub.removeAttribute("aria-describedby");
979
+ }
932
980
  }
981
+ this.syncTriggerAria();
933
982
  shadow
934
983
  .querySelectorAll(".dropdown-search-input, .mobile-search-input")
935
984
  .forEach((input) => (input.disabled = this._disabled));
936
985
  }
986
+ syncTriggerAria() {
987
+ const error = this.getProperty("error") || "";
988
+ const trigger = error
989
+ ? this.querySelector(':scope > [slot="trigger"]')
990
+ : null;
991
+ if (this._ariaTrigger && this._ariaTrigger !== trigger) {
992
+ this._ariaTrigger.removeAttribute("aria-invalid");
993
+ this._ariaTrigger.removeAttribute("aria-description");
994
+ }
995
+ if (trigger) {
996
+ if (trigger.getAttribute("aria-invalid") !== "true")
997
+ trigger.setAttribute("aria-invalid", "true");
998
+ if (trigger.getAttribute("aria-description") !== error)
999
+ trigger.setAttribute("aria-description", error);
1000
+ }
1001
+ this._ariaTrigger = trigger;
1002
+ this._ariaObserver ?? (this._ariaObserver = new MutationObserver(() => this.syncTriggerAria()));
1003
+ this._ariaObserver.disconnect();
1004
+ if (trigger) {
1005
+ this._ariaObserver.observe(trigger, {
1006
+ attributes: true,
1007
+ attributeFilter: ["aria-invalid", "aria-description"],
1008
+ });
1009
+ }
1010
+ }
937
1011
  updateValidity() {
938
1012
  if (this.disabled) {
939
1013
  this.applyValidity({});
@@ -941,7 +1015,7 @@ return;
941
1015
  }
942
1016
  if (this.required && this.getSelectedValues().length === 0) {
943
1017
  const stub = this.shadowRoot?.querySelector(".select-stub");
944
- this.applyValidity({ valueMissing: true }, "Please select a value", stub ?? undefined);
1018
+ this.applyValidity({ valueMissing: true }, nativeMessage("select", { required: true, multiple: this._multiple }), stub ?? undefined);
945
1019
  }
946
1020
  else {
947
1021
  this.applyValidity({});
@@ -958,7 +1032,10 @@ const hasTrigger = slot.assignedElements().length > 0;
958
1032
  stub.classList.toggle("custom-trigger", hasTrigger);
959
1033
  this.toggleAttribute("has-trigger", hasTrigger);
960
1034
  };
961
- slot.addEventListener("slotchange", sync);
1035
+ slot.addEventListener("slotchange", () => {
1036
+ sync();
1037
+ this.syncFieldChrome();
1038
+ });
962
1039
  sync();
963
1040
  }
964
1041
  setupEventListeners() {
@@ -991,9 +1068,13 @@ if (e.target === dialog) {
991
1068
  this.closeDropdown();
992
1069
  }
993
1070
  });
1071
+ dialog.addEventListener("cancel", (e) => {
1072
+ e.preventDefault();
1073
+ this.closeDropdown();
1074
+ this.focus();
1075
+ });
1076
+ this.syncOnOutsideClose(dialog, () => this.closeDropdown());
994
1077
  }
995
- this._keyboardHandler = this.handleKeyboard.bind(this);
996
- document.addEventListener("keydown", this._keyboardHandler);
997
1078
  }
998
1079
  buildStubClasses() {
999
1080
  const classes = [];
@@ -1187,10 +1268,6 @@ this.setupMobileEventListeners();
1187
1268
  this.updateSelectionDisplay();
1188
1269
  }
1189
1270
  setupMobileEventListeners() {
1190
- if (!this._keyboardHandler) {
1191
- this._keyboardHandler = this.handleKeyboard.bind(this);
1192
- document.addEventListener("keydown", this._keyboardHandler);
1193
- }
1194
1271
  const shadow = this.shadowRoot;
1195
1272
  const stub = shadow.querySelector(".select-stub");
1196
1273
  const optionsSlot = shadow.querySelector("#options-slot");
@@ -1225,8 +1302,16 @@ this.closeMobileModal();
1225
1302
  dialog.addEventListener("cancel", (e) => {
1226
1303
  e.preventDefault();
1227
1304
  this.closeMobileModal();
1305
+ this.focus();
1228
1306
  });
1307
+ this.syncOnOutsideClose(dialog, () => this.closeMobileModal());
1308
+ }
1229
1309
  }
1310
+ syncOnOutsideClose(dialog, close) {
1311
+ new MutationObserver(() => {
1312
+ if (this._state.open && !dialog.open)
1313
+ close();
1314
+ }).observe(dialog, { attributes: true, attributeFilter: ["open"] });
1230
1315
  }
1231
1316
  handleMobileStubClick(e) {
1232
1317
  e.preventDefault();
@@ -1254,7 +1339,7 @@ const shadow = this.shadowRoot;
1254
1339
  this.renderMobileSelectedChips();
1255
1340
  const availableSection = shadow.querySelector(".mobile-available-section");
1256
1341
  if (availableSection) {
1257
- const visibleAvailable = this.getTagElements().filter((tag) => !tag.hasAttribute("hidden") && !tag.hasAttribute("selected")).length;
1342
+ const visibleAvailable = this.getOfferedTags().filter(({ element }) => !element.hasAttribute("filtered-out") && !element.hasAttribute("selected")).length;
1258
1343
  availableSection.setAttribute("data-empty", String(visibleAvailable === 0));
1259
1344
  this.setMobileSectionTitle(availableSection, this._availableLabel, visibleAvailable);
1260
1345
  }
@@ -1399,6 +1484,8 @@ const option = this.getTagElements().find((el) => this.getTagData(el).value ===
1399
1484
  if (option) {
1400
1485
  const clone = option.cloneNode(true);
1401
1486
  clone.removeAttribute("selected");
1487
+ clone.removeAttribute("hidden");
1488
+ clone.removeAttribute("filtered-out");
1402
1489
  clone.setAttribute("slot", "selected");
1403
1490
  clone.setAttribute("cloned", "true");
1404
1491
  this.appendChild(clone);
@@ -1421,7 +1508,7 @@ return;
1421
1508
  this.updateComponentValue(current.filter((v) => v !== value), true, "remove", value);
1422
1509
  }
1423
1510
  clear() {
1424
- this.updateComponentValue([], true, "clear", null);
1511
+ this.updateComponentValue([], true, "clear", null, false);
1425
1512
  }
1426
1513
  get value() {
1427
1514
  return this.getSelectedValues().join(",");
@@ -1489,6 +1576,12 @@ return this.getProperty("required");
1489
1576
  set required(value) {
1490
1577
  this.setProperty("required", value);
1491
1578
  }
1579
+ get error() {
1580
+ return this.getProperty("error");
1581
+ }
1582
+ set error(value) {
1583
+ this.setProperty("error", value);
1584
+ }
1492
1585
  get notSearchable() {
1493
1586
  return this.getProperty("notSearchable");
1494
1587
  }
@@ -1592,6 +1685,11 @@ type: "boolean",
1592
1685
  visual: true,
1593
1686
  default: false,
1594
1687
  },
1688
+ error: {
1689
+ type: "string",
1690
+ visual: true,
1691
+ default: "",
1692
+ },
1595
1693
  noClear: {
1596
1694
  type: "boolean",
1597
1695
  visual: true,
@@ -24,6 +24,8 @@ export interface TySwitchElement extends HTMLElement {
24
24
  }
25
25
  export declare class TySwitch extends TyComponent<SwitchState> implements TySwitchElement {
26
26
  static formAssociated: boolean;
27
+ protected static delegatesFocus: boolean;
28
+ protected static labelClicks: boolean;
27
29
  protected static properties: {
28
30
  checked: {
29
31
  type: "boolean";
@@ -78,13 +80,17 @@ export declare class TySwitch extends TyComponent<SwitchState> implements TySwit
78
80
  protected getFormValue(): FormDataEntryValue | null;
79
81
  private buildClassList;
80
82
  private handleSwitchClick;
83
+ /** Keys show the ring, a click hides it (see checkbox.ts). */
84
+ private showFocusRing;
81
85
  private handleSwitchKeydown;
82
86
  private removeEventListeners;
83
87
  private setupEventListeners;
84
88
  protected render(): void;
85
89
  /** Focus the switch itself (the host is not focusable; its role element is). */
86
90
  focus(options?: FocusOptions): void;
87
- /** required + unchecked => valueMissing; otherwise valid. */
91
+ /** required + unchecked => valueMissing; otherwise valid. The message is a
92
+ * native checkbox's, as a switch where the browser has one (Safari's
93
+ * `switch` attribute). */
88
94
  private updateValidity;
89
95
  get checked(): boolean;
90
96
  set checked(value: boolean);