tyrell-components 1.0.0-TC58 → 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 (65) 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 +8 -12
  8. package/lib/base/ty-component.js +38 -9
  9. package/lib/components/button.d.ts +10 -0
  10. package/lib/components/button.js +22 -1
  11. package/lib/components/calendar-month.d.ts +7 -1
  12. package/lib/components/calendar-month.js +32 -18
  13. package/lib/components/calendar-navigation.js +19 -6
  14. package/lib/components/calendar.d.ts +24 -5
  15. package/lib/components/calendar.js +83 -37
  16. package/lib/components/checkbox.d.ts +5 -0
  17. package/lib/components/checkbox.js +18 -5
  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 +7 -26
  21. package/lib/components/date-picker.js +14 -84
  22. package/lib/components/file-upload.js +6 -2
  23. package/lib/components/input.d.ts +0 -9
  24. package/lib/components/input.js +17 -40
  25. package/lib/components/modal.js +59 -18
  26. package/lib/components/popup.js +56 -5
  27. package/lib/components/radio.d.ts +6 -0
  28. package/lib/components/radio.js +12 -28
  29. package/lib/components/select.d.ts +29 -11
  30. package/lib/components/select.js +43 -57
  31. package/lib/components/switch.d.ts +4 -0
  32. package/lib/components/switch.js +18 -5
  33. package/lib/components/tabs.js +1 -1
  34. package/lib/components/textarea.d.ts +1 -0
  35. package/lib/components/textarea.js +21 -8
  36. package/lib/index.d.ts +1 -0
  37. package/lib/index.js +1 -0
  38. package/lib/styles/button.js +1 -0
  39. package/lib/styles/calendar-month.js +1 -1
  40. package/lib/styles/calendar.js +3 -0
  41. package/lib/styles/checkbox.js +11 -5
  42. package/lib/styles/date-picker.js +5 -10
  43. package/lib/styles/input.js +5 -9
  44. package/lib/styles/option.d.ts +1 -1
  45. package/lib/styles/option.js +4 -0
  46. package/lib/styles/popup.d.ts +1 -1
  47. package/lib/styles/popup.js +2 -0
  48. package/lib/styles/radio.js +10 -4
  49. package/lib/styles/select-base.js +5 -10
  50. package/lib/styles/select.d.ts +1 -1
  51. package/lib/styles/select.js +4 -0
  52. package/lib/styles/switch.js +10 -5
  53. package/lib/styles/tag.js +2 -1
  54. package/lib/utils/calendar-utils.d.ts +49 -0
  55. package/lib/utils/calendar-utils.js +63 -6
  56. package/lib/utils/scroll-lock.d.ts +10 -4
  57. package/lib/utils/scroll-lock.js +102 -2
  58. package/lib/utils/validity.d.ts +27 -1
  59. package/lib/utils/validity.js +68 -16
  60. package/lib/version.d.ts +1 -1
  61. package/lib/version.js +1 -1
  62. package/package.json +1 -1
  63. package/dist/tyrell.js.map +0 -1
  64. package/lib/utils/error-flavor.d.ts +0 -8
  65. package/lib/utils/error-flavor.js +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";
@@ -169,6 +170,11 @@ export declare class TyRadioGroup extends TyComponent<RadioGroupState> implement
169
170
  radio's; it needs a name, a nameless radio never reports valueMissing. */
170
171
  private updateValidity;
171
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;
172
178
  /**
173
179
  * Arrow-key navigation within group (W3C radio practice: arrow keys move
174
180
  * focus and selection together).
@@ -134,6 +134,7 @@ set size(v) { this.setProperty("size", v); }
134
134
  get flavor() { return this.getProperty("flavor"); }
135
135
  set flavor(v) { this.setProperty("flavor", v); }
136
136
  }
137
+ TyRadio.labelClicks = true;
137
138
  TyRadio.properties = {
138
139
  value: { type: "string", default: "" },
139
140
  checked: {
@@ -237,16 +238,17 @@ const detail = e.detail;
237
238
  const newValue = detail?.value;
238
239
  if (typeof newValue !== "string" || newValue === this.value)
239
240
  return;
240
- this.value = newValue;
241
+ this.select(newValue, e);
242
+ }
243
+ select(value, originalEvent) {
244
+ if (value === this.value)
245
+ return;
246
+ this.value = value;
241
247
  this.syncChildren();
248
+ const detail = { value, formValue: value, originalEvent };
242
249
  setTimeout(() => {
243
- const eventDetail = {
244
- value: newValue,
245
- formValue: newValue,
246
- originalEvent: e,
247
- };
248
- this.dispatchEvent(new CustomEvent("input", { detail: eventDetail, bubbles: true, composed: true }));
249
- 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 }));
250
252
  }, 0);
251
253
  }
252
254
  handleKeydown(e) {
@@ -271,16 +273,7 @@ nextIndex = current
271
273
  else if (["Space", " ", "Enter"].includes(e.key)) {
272
274
  if (current) {
273
275
  e.preventDefault();
274
- this.value = current.value;
275
- this.syncChildren();
276
- const eventDetail = {
277
- value: current.value,
278
- formValue: current.value,
279
- originalEvent: e,
280
- };
281
- setTimeout(() => {
282
- this.dispatchEvent(new CustomEvent("change", { detail: eventDetail, bubbles: true, composed: true }));
283
- }, 0);
276
+ this.select(current.value, e);
284
277
  }
285
278
  return;
286
279
  }
@@ -290,16 +283,7 @@ return;
290
283
  const next = radios[nextIndex];
291
284
  if (next) {
292
285
  next.focus();
293
- this.value = next.value;
294
- this.syncChildren();
295
- const eventDetail = {
296
- value: next.value,
297
- formValue: next.value,
298
- originalEvent: e,
299
- };
300
- setTimeout(() => {
301
- this.dispatchEvent(new CustomEvent("change", { detail: eventDetail, bubbles: true, composed: true }));
302
- }, 0);
286
+ this.select(next.value, e);
303
287
  }
304
288
  }
305
289
  removeEventListeners() {
@@ -196,18 +196,12 @@ export declare class TySelect extends TyComponent<SelectState> {
196
196
  private _selectedClone;
197
197
  private _customFlavorSheet;
198
198
  private _ariaTrigger;
199
+ private _ariaObserver;
199
200
  constructor();
200
201
  /** TyComponent handles property capture automatically. */
201
202
  protected onConnect(): void;
202
203
  /** Custom (non-built-in) flavors — see utils/flavor-sheet.ts. */
203
204
  private _syncCustomFlavor;
204
- /**
205
- * `error` turns a neutral select danger, like ty-input; clearing it puts
206
- * back exactly the flavor attribute the author had, or none, kept meanwhile
207
- * in ERROR_FLAVOR. Runs once connected: while the element upgrades, `error`
208
- * may come before the `flavor` written next to it.
209
- */
210
- private syncErrorFlavor;
211
205
  protected onDisconnect(): void;
212
206
  /** Handle state synchronization BEFORE render. */
213
207
  protected onPropertiesChanged(changes: PropertyChange[]): void;
@@ -220,7 +214,11 @@ export declare class TySelect extends TyComponent<SelectState> {
220
214
  private applyLoadingState;
221
215
  /** Form value: a plain entry for single, FormData with repeated entries for multiple (HTMX standard). */
222
216
  protected getFormValue(): FormDataEntryValue | FormData | null;
223
- /** 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
+ */
224
222
  focus(options?: FocusOptions): void;
225
223
  private parseValue;
226
224
  /** Initialize component state from the value property / pre-selected children. */
@@ -252,6 +250,12 @@ export declare class TySelect extends TyComponent<SelectState> {
252
250
  * Get all option elements from the component (ty-option preferred, ty-tag accepted)
253
251
  */
254
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;
255
259
  private getTagData;
256
260
  /**
257
261
  * Select an option - attribute only. The option stays in the list with
@@ -350,11 +354,17 @@ export declare class TySelect extends TyComponent<SelectState> {
350
354
  /**
351
355
  * Next tag index in `direction` (-1/1) that isn't disabled, wrapping
352
356
  * around — mirrors handleTagClick's disabled check so keyboard nav can
353
- * never land on/select what a click already refuses to. Returns -1 if
354
- * 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).
355
361
  */
356
362
  private nextEnabledTagIndex;
357
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
+ */
358
368
  private updateTagVisibility;
359
369
  private updateOptionsVisibility;
360
370
  private clearHighlights;
@@ -378,6 +388,15 @@ export declare class TySelect extends TyComponent<SelectState> {
378
388
  * `compact` after connect never showed up.
379
389
  */
380
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;
381
400
  /**
382
401
  * Reflect constraint validation to the owning <form> via ElementInternals.
383
402
  * Without this `required` is only cosmetic — form.checkValidity() (and a
@@ -511,7 +530,6 @@ export declare class TySelect extends TyComponent<SelectState> {
511
530
  set placement(value: Placement | "");
512
531
  get flavor(): Flavor;
513
532
  set flavor(value: Flavor);
514
- attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
515
533
  get form(): HTMLFormElement | null;
516
534
  }
517
535
  export {};
@@ -1,7 +1,6 @@
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";
5
4
  import { selectBaseStyles, selectCustomFlavorCss } from "../styles/select-base.js";
6
5
  import { selectStyles } from "../styles/select.js";
7
6
  import { getLoaderSvg } from "../utils/loader-registry.js";
@@ -84,6 +83,7 @@ this._childObserver = null;
84
83
  this._selectedClone = null;
85
84
  this._customFlavorSheet = null;
86
85
  this._ariaTrigger = null;
86
+ this._ariaObserver = null;
87
87
  const shadow = this.shadowRoot;
88
88
  ensureStyles(shadow, { css: selectBaseStyles, id: "ty-select-base" });
89
89
  ensureStyles(shadow, { css: selectStyles, id: "ty-select" });
@@ -112,7 +112,6 @@ this.updateMobileSelectedState();
112
112
  this._childObserver.observe(this, { childList: true });
113
113
  this.ensureOptionRoles();
114
114
  this._syncCustomFlavor();
115
- this.syncErrorFlavor();
116
115
  if (!this._keyboardHandler) {
117
116
  this._keyboardHandler = this.handleKeyboard.bind(this);
118
117
  document.addEventListener("keydown", this._keyboardHandler, true);
@@ -121,27 +120,6 @@ document.addEventListener("keydown", this._keyboardHandler, true);
121
120
  _syncCustomFlavor() {
122
121
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.getProperty("flavor"), ({ base }) => selectCustomFlavorCss(base));
123
122
  }
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
- }
145
123
  onDisconnect() {
146
124
  if (this._state.open) {
147
125
  if (this._state.mode === "mobile")
@@ -186,15 +164,8 @@ case "name":
186
164
  this._name = newValue || "";
187
165
  break;
188
166
  case "flavor":
189
- if (newValue !== "danger") {
190
- this.removeAttribute(ERROR_FLAVOR);
191
- this.syncErrorFlavor();
192
- }
193
167
  this._syncCustomFlavor();
194
168
  break;
195
- case "error":
196
- this.syncErrorFlavor();
197
- break;
198
169
  case "multiple":
199
170
  this._multiple = newValue;
200
171
  if (!newValue && this._state.selectedValues.length > 1) {
@@ -301,10 +272,15 @@ focus(options) {
301
272
  if (this._disabled)
302
273
  return;
303
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
+ }
304
281
  const stub = this.shadowRoot?.querySelector(".select-stub");
305
- const target = trigger ?? stub;
306
- if (target)
307
- target.focus(options);
282
+ if (stub)
283
+ stub.focus(options);
308
284
  else
309
285
  super.focus(options);
310
286
  }
@@ -348,6 +324,11 @@ this.resolveDeclarativeSelection(); }, { once: true });
348
324
  getTagElements() {
349
325
  return Array.from(this.querySelectorAll("ty-option:not([cloned]), ty-tag:not([cloned])"));
350
326
  }
327
+ getOfferedTags() {
328
+ return this.getTagElements()
329
+ .filter((el) => !el.hasAttribute("hidden"))
330
+ .map((el) => this.getTagData(el));
331
+ }
351
332
  getTagData(element) {
352
333
  const value = element.value ||
353
334
  element.getAttribute("value") ||
@@ -514,8 +495,7 @@ chevron.classList.add("open");
514
495
  const searchChevron = shadow.querySelector(".dropdown-search-chevron");
515
496
  if (searchChevron)
516
497
  searchChevron.classList.add("open");
517
- const tags = this.getTagElements().map((el) => this.getTagData(el));
518
- this._state.filteredTags = tags;
498
+ this._state.filteredTags = this.getOfferedTags();
519
499
  this._state.highlightedIndex = -1;
520
500
  this.updateOptionsVisibility(true);
521
501
  this._setupOptionsScrollbar();
@@ -567,7 +547,7 @@ this.fireSearchEvent("");
567
547
  }
568
548
  else {
569
549
  const allTags = this.getTagElements().map((el) => this.getTagData(el));
570
- this._state.filteredTags = allTags;
550
+ this._state.filteredTags = this.getOfferedTags();
571
551
  this.updateTagVisibility(allTags, allTags);
572
552
  this.clearHighlights(allTags);
573
553
  }
@@ -589,7 +569,7 @@ dialog.getBoundingClientRect();
589
569
  dialog.classList.add("open");
590
570
  this._state.open = true;
591
571
  const tags = this.getTagElements().map((el) => this.getTagData(el));
592
- this._state.filteredTags = tags;
572
+ this._state.filteredTags = this.getOfferedTags();
593
573
  this.updateTagVisibility(tags, tags);
594
574
  requestAnimationFrame(() => {
595
575
  this.updateMobileSelectedState();
@@ -629,7 +609,7 @@ this.fireSearchEvent("");
629
609
  }
630
610
  else {
631
611
  const allTags = this.getTagElements();
632
- allTags.forEach((el) => el.removeAttribute("hidden"));
612
+ allTags.forEach((el) => el.removeAttribute("filtered-out"));
633
613
  }
634
614
  this.fireCloseEvent();
635
615
  }
@@ -749,7 +729,7 @@ desktopInput.value = "";
749
729
  if (mobileInput)
750
730
  mobileInput.value = "";
751
731
  const allTags = this.getTagElements().map((el) => this.getTagData(el));
752
- this._state.filteredTags = allTags;
732
+ this._state.filteredTags = this.getOfferedTags();
753
733
  this.updateTagVisibility(allTags, allTags);
754
734
  this.updateOptionsVisibility(true);
755
735
  this.clearHighlights(allTags);
@@ -770,7 +750,7 @@ if (this._externalSearch) {
770
750
  this.dispatchSearchEvent(query);
771
751
  return;
772
752
  }
773
- const allTags = this.getTagElements().map((el) => this.getTagData(el));
753
+ const allTags = this.getOfferedTags();
774
754
  const filtered = this.filterTags(allTags, query);
775
755
  this._state.filteredTags = filtered;
776
756
  this._state.highlightedIndex = -1;
@@ -838,7 +818,8 @@ let index = fromIndex === -1
838
818
  ? (direction === 1 ? 0 : count - 1)
839
819
  : (fromIndex + direction + count) % count;
840
820
  for (let i = 0; i < count; i++) {
841
- if (!tags[index].element.hasAttribute("disabled"))
821
+ const el = tags[index].element;
822
+ if (!el.hasAttribute("disabled") && !el.hasAttribute("hidden") && !el.hasAttribute("filtered-out"))
842
823
  return index;
843
824
  index = (index + direction + count) % count;
844
825
  }
@@ -854,12 +835,7 @@ return tags.filter(({ text }) => text.toLowerCase().includes(searchLower));
854
835
  updateTagVisibility(filteredTags, allTags) {
855
836
  const visibleValues = new Set(filteredTags.map((tag) => tag.value));
856
837
  allTags.forEach(({ value, element }) => {
857
- if (visibleValues.has(value)) {
858
- element.removeAttribute("hidden");
859
- }
860
- else {
861
- element.setAttribute("hidden", "");
862
- }
838
+ element.toggleAttribute("filtered-out", !visibleValues.has(value));
863
839
  });
864
840
  }
865
841
  updateOptionsVisibility(hasOptions) {
@@ -1002,6 +978,13 @@ stub.removeAttribute("aria-invalid");
1002
978
  stub.removeAttribute("aria-describedby");
1003
979
  }
1004
980
  }
981
+ this.syncTriggerAria();
982
+ shadow
983
+ .querySelectorAll(".dropdown-search-input, .mobile-search-input")
984
+ .forEach((input) => (input.disabled = this._disabled));
985
+ }
986
+ syncTriggerAria() {
987
+ const error = this.getProperty("error") || "";
1005
988
  const trigger = error
1006
989
  ? this.querySelector(':scope > [slot="trigger"]')
1007
990
  : null;
@@ -1010,13 +993,20 @@ this._ariaTrigger.removeAttribute("aria-invalid");
1010
993
  this._ariaTrigger.removeAttribute("aria-description");
1011
994
  }
1012
995
  if (trigger) {
996
+ if (trigger.getAttribute("aria-invalid") !== "true")
1013
997
  trigger.setAttribute("aria-invalid", "true");
998
+ if (trigger.getAttribute("aria-description") !== error)
1014
999
  trigger.setAttribute("aria-description", error);
1015
1000
  }
1016
1001
  this._ariaTrigger = trigger;
1017
- shadow
1018
- .querySelectorAll(".dropdown-search-input, .mobile-search-input")
1019
- .forEach((input) => (input.disabled = this._disabled));
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
+ }
1020
1010
  }
1021
1011
  updateValidity() {
1022
1012
  if (this.disabled) {
@@ -1349,7 +1339,7 @@ const shadow = this.shadowRoot;
1349
1339
  this.renderMobileSelectedChips();
1350
1340
  const availableSection = shadow.querySelector(".mobile-available-section");
1351
1341
  if (availableSection) {
1352
- 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;
1353
1343
  availableSection.setAttribute("data-empty", String(visibleAvailable === 0));
1354
1344
  this.setMobileSectionTitle(availableSection, this._availableLabel, visibleAvailable);
1355
1345
  }
@@ -1494,6 +1484,8 @@ const option = this.getTagElements().find((el) => this.getTagData(el).value ===
1494
1484
  if (option) {
1495
1485
  const clone = option.cloneNode(true);
1496
1486
  clone.removeAttribute("selected");
1487
+ clone.removeAttribute("hidden");
1488
+ clone.removeAttribute("filtered-out");
1497
1489
  clone.setAttribute("slot", "selected");
1498
1490
  clone.setAttribute("cloned", "true");
1499
1491
  this.appendChild(clone);
@@ -1631,14 +1623,8 @@ get flavor() {
1631
1623
  return this.getProperty("flavor");
1632
1624
  }
1633
1625
  set flavor(value) {
1634
- this.removeAttribute(ERROR_FLAVOR);
1635
1626
  this.setProperty("flavor", value);
1636
1627
  }
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
- }
1642
1628
  get form() {
1643
1629
  return this._internals.form;
1644
1630
  }
@@ -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,6 +80,8 @@ 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;
@@ -19,6 +19,9 @@ ensureStyles(shadow, { css: switchStyles, id: "ty-switch" });
19
19
  onConnect() {
20
20
  this.setupEventListeners();
21
21
  this._syncCustomFlavor();
22
+ const el = this.shadowRoot.querySelector(".switch-container");
23
+ if (el && this.shadowRoot.activeElement !== el)
24
+ el.classList.remove("focused");
22
25
  }
23
26
  onDisconnect() {
24
27
  this.removeEventListeners();
@@ -45,8 +48,7 @@ return classes.join(" ");
45
48
  handleSwitchClick(e) {
46
49
  if (this.disabled)
47
50
  return;
48
- if (this.isDuplicateActivationClick())
49
- return;
51
+ this.showFocusRing(e instanceof KeyboardEvent);
50
52
  const newValue = !this.checked;
51
53
  this.checked = newValue;
52
54
  setTimeout(() => {
@@ -60,6 +62,9 @@ this.dispatchEvent(new CustomEvent("input", { detail: eventDetail, bubbles: true
60
62
  this.dispatchEvent(new CustomEvent("change", { detail: eventDetail, bubbles: true, composed: true }));
61
63
  }, 0);
62
64
  }
65
+ showFocusRing(on) {
66
+ this.shadowRoot?.querySelector(".switch-container")?.classList.toggle("focused", on);
67
+ }
63
68
  handleSwitchKeydown(e) {
64
69
  if (this.disabled)
65
70
  return;
@@ -107,7 +112,7 @@ if (!el)
107
112
  return;
108
113
  this._clickHandler = (e) => this.handleSwitchClick(e);
109
114
  this._keydownHandler = (e) => this.handleSwitchKeydown(e);
110
- this._focusHandler = () => el.classList.add("focused");
115
+ this._focusHandler = () => el.classList.toggle("focused", el.matches(":focus-visible"));
111
116
  this._blurHandler = () => el.classList.remove("focused");
112
117
  this.addEventListener("click", this._clickHandler);
113
118
  el.addEventListener("keydown", this._keydownHandler);
@@ -122,7 +127,8 @@ const classes = this.buildClassList();
122
127
  if (!switchEl) {
123
128
  switchEl = document.createElement("div");
124
129
  switchEl.className = "switch-container " + classes;
125
- switchEl.tabIndex = this.disabled ? -1 : 0;
130
+ if (!this.disabled)
131
+ switchEl.tabIndex = 0;
126
132
  switchEl.setAttribute("role", "switch");
127
133
  switchEl.setAttribute("aria-checked", this.checked ? "true" : "false");
128
134
  switchEl.setAttribute("aria-disabled", this.disabled ? "true" : "false");
@@ -136,8 +142,13 @@ switchEl.appendChild(track);
136
142
  shadow.appendChild(switchEl);
137
143
  }
138
144
  else {
145
+ const focused = switchEl.classList.contains("focused") && shadow.activeElement === switchEl;
139
146
  switchEl.className = "switch-container " + classes;
140
- switchEl.tabIndex = this.disabled ? -1 : 0;
147
+ switchEl.classList.toggle("focused", focused && !this.disabled);
148
+ if (this.disabled)
149
+ switchEl.removeAttribute("tabindex");
150
+ else
151
+ switchEl.tabIndex = 0;
141
152
  switchEl.setAttribute("aria-checked", this.checked ? "true" : "false");
142
153
  switchEl.setAttribute("aria-disabled", this.disabled ? "true" : "false");
143
154
  switchEl.setAttribute("aria-required", this.required ? "true" : "false");
@@ -183,6 +194,8 @@ set flavor(value) { this.setProperty("flavor", value); }
183
194
  get form() { return this._internals.form; }
184
195
  }
185
196
  TySwitch.formAssociated = true;
197
+ TySwitch.delegatesFocus = true;
198
+ TySwitch.labelClicks = true;
186
199
  TySwitch.properties = {
187
200
  checked: {
188
201
  type: "boolean",
@@ -251,7 +251,7 @@ marker.style.top = `${position.top}px`;
251
251
  marker.style.width = `${position.width}px`;
252
252
  marker.style.height = `${position.height}px`;
253
253
  if (snap) {
254
- marker.offsetWidth;
254
+ marker.getBoundingClientRect();
255
255
  marker.style.transition = '';
256
256
  }
257
257
  }
@@ -191,6 +191,7 @@ export declare class TyTextarea extends TyComponent<TextareaState> implements Ty
191
191
  private buildClassList;
192
192
  private applyHeightConstraints;
193
193
  private setupTextareaEvents;
194
+ private removeTextareaEvents;
194
195
  protected render(): void;
195
196
  /**
196
197
  * minlength goes on the inner <textarea> for the browser's own too-short
@@ -15,6 +15,8 @@ this._scrollbar = null;
15
15
  this._isFocused = false;
16
16
  this.handleInputEvent = (e) => {
17
17
  this._userEdited = true;
18
+ e.stopPropagation();
19
+ e.stopImmediatePropagation();
18
20
  const target = e.target;
19
21
  const newValue = target.value;
20
22
  this.value = newValue;
@@ -46,20 +48,22 @@ ensureStyles(shadow, { css: customScrollbarStyles, id: 'ty-custom-scrollbar' });
46
48
  }
47
49
  }
48
50
  onConnect() {
51
+ this.updateFormValue();
52
+ if (this._rendered) {
53
+ this.render();
54
+ if (this._textareaEl)
55
+ this.setupTextareaEvents(this._textareaEl);
56
+ }
49
57
  }
50
58
  onDisconnect() {
51
59
  this._destroyScrollbar();
52
- if (this._textareaEl) {
53
- const newTextarea = this._textareaEl.cloneNode(true);
54
- this._textareaEl.parentNode?.replaceChild(newTextarea, this._textareaEl);
55
- this._textareaEl = null;
56
- }
57
- this._dummyEl = null;
60
+ if (this._textareaEl)
61
+ this.removeTextareaEvents(this._textareaEl);
58
62
  }
59
63
  onPropertiesChanged(changes) {
60
64
  }
61
65
  getFormValue() {
62
- return this.getProperty('value') || null;
66
+ return this.getProperty('value') || '';
63
67
  }
64
68
  get value() { return this.getProperty('value'); }
65
69
  set value(v) { this.setProperty('value', v); }
@@ -190,6 +194,10 @@ textareaEl.style.overflowY = 'hidden';
190
194
  this._destroyScrollbar();
191
195
  }
192
196
  }
197
+ else if (maxHeight !== null && measuredHeight >= maxHeight && !this._scrollbar) {
198
+ textareaEl.style.overflowY = 'auto';
199
+ this._setupScrollbar(textareaEl);
200
+ }
193
201
  this._scrollbar?.update();
194
202
  }
195
203
  _setupScrollbar(textareaEl) {
@@ -226,7 +234,7 @@ value: this.value,
226
234
  originalEvent
227
235
  }
228
236
  };
229
- setTimeout(() => this.dispatchEvent(new CustomEvent(type, data)), 0);
237
+ this.dispatchEvent(new CustomEvent(type, data));
230
238
  }
231
239
  buildClassList() {
232
240
  const classes = [this.size];
@@ -256,6 +264,11 @@ textareaEl.addEventListener('input', this.handleInputEvent);
256
264
  textareaEl.addEventListener('focus', this.handleFocusEvent);
257
265
  textareaEl.addEventListener('blur', this.handleBlurEvent);
258
266
  }
267
+ removeTextareaEvents(textareaEl) {
268
+ textareaEl.removeEventListener('input', this.handleInputEvent);
269
+ textareaEl.removeEventListener('focus', this.handleFocusEvent);
270
+ textareaEl.removeEventListener('blur', this.handleBlurEvent);
271
+ }
259
272
  render() {
260
273
  const root = this.shadowRoot;
261
274
  const existingContainer = root.querySelector('.textarea-container');
package/lib/index.d.ts CHANGED
@@ -75,6 +75,7 @@ declare global {
75
75
  tyResizeObserver: {
76
76
  getSize: (id: string) => ResizeSize | undefined;
77
77
  onResize: (id: string, callback: ResizeCallback) => () => void;
78
+ getAllSizes: () => Record<string, ResizeSize>;
78
79
  sizes: Record<string, ResizeSize>;
79
80
  };
80
81
  tyLoader: {
package/lib/index.js CHANGED
@@ -54,6 +54,7 @@ clearCache: () => clearIcons()
54
54
  window.tyResizeObserver = {
55
55
  getSize: (id) => getResizeSize(id),
56
56
  onResize: (id, callback) => subscribeResize(id, callback),
57
+ getAllSizes: () => getAllSizes(),
57
58
  get sizes() { return getAllSizes(); }
58
59
  };
59
60
  }
@@ -85,6 +85,7 @@ aspect-ratio: 1;
85
85
  :host([wide]) {
86
86
  display: grid;
87
87
  flex-grow: 1;
88
+ max-height: var(--_rung);
88
89
  grid-template-rows: minmax(var(--_rung), max-content);
89
90
  }
90
91
  :host([wide]) button {
@@ -124,7 +124,7 @@ color: var(--ty-calendar-weekend-color, var(--ty-calendar-day-color, var(--ty-te
124
124
  color: var(--ty-calendar-other-month-color, color-mix(in oklab, var(--ty-text-faint) 65%, transparent));
125
125
  }
126
126
  .calendar-day-cell.other-month:hover {
127
- color: var(--ty-calendar-day-hover-color, var(--ty-text-soft, var(--ty-text-)));
127
+ color: var(--ty-calendar-day-hover-color, var(--ty-text-soft));
128
128
  background-color: var(--ty-calendar-day-hover-bg, var(--ty-bg-neutral-soft));
129
129
  }
130
130
  .calendar-day-cell.disabled {
@@ -10,6 +10,9 @@ box-sizing: border-box;
10
10
  font-family: inherit;
11
11
  align-items: center;
12
12
  }
13
+ .calendar-container:focus {
14
+ outline: none;
15
+ }
13
16
  @media (max-width: 320px) {
14
17
  .calendar-container {
15
18
  gap: 0.375rem;