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
@@ -3,7 +3,7 @@ import { liftProperties, upgradeProperties } from '../utils/upgrade-properties.j
3
3
  import { calendarStyles } from '../styles/calendar.js';
4
4
  import './calendar-month.js';
5
5
  import './calendar-navigation.js';
6
- import { parseISODate, getMonthName } from '../utils/calendar-utils.js';
6
+ import { parseDateDay, formatISODay, toISODay, getMonthName, localMidnight, utcMidnight } from '../utils/calendar-utils.js';
7
7
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
8
8
  import { ValidityController, nativeMessage } from '../utils/validity.js';
9
9
  function getCurrentDate() {
@@ -32,14 +32,12 @@ const day = parseInt(dayStr, 10);
32
32
  const daysInMonth = new Date(year, month, 0).getDate();
33
33
  return day >= 1 && day <= daysInMonth ? day : null;
34
34
  }
35
- function formatDateISO(year, month, day) {
36
- const monthStr = month.toString().padStart(2, '0');
37
- const dayStr = day.toString().padStart(2, '0');
38
- return `${year}-${monthStr}-${dayStr}`;
35
+ function formatYear(year) {
36
+ return year.toString().padStart(4, '0');
39
37
  }
40
38
  export class TyCalendar extends HTMLElement {
41
39
  static get observedAttributes() {
42
- return ['value', 'year', 'month', 'day', 'hide-navigation', 'stateless', 'locale', 'name', 'size', 'width', 'min', 'max', 'flavor'];
40
+ return ['value', 'year', 'month', 'day', 'hide-navigation', 'stateless', 'locale', 'name', 'size', 'width', 'min', 'max', 'flavor', 'required'];
43
41
  }
44
42
  constructor() {
45
43
  super();
@@ -51,12 +49,13 @@ this._view = 'days';
51
49
  this._dayGridHeight = 0;
52
50
  this._initialValue = '';
53
51
  this._valueSets = 0;
52
+ this._pressing = false;
54
53
  const current = getCurrentDate();
55
54
  this._state = {
56
55
  displayYear: current.year,
57
56
  displayMonth: current.month,
58
57
  };
59
- this.attachShadow({ mode: 'open' });
58
+ this.attachShadow({ mode: 'open', delegatesFocus: true });
60
59
  liftProperties(this);
61
60
  if ('attachInternals' in this) {
62
61
  this._internals = this.attachInternals();
@@ -132,6 +131,9 @@ this[name === 'min' ? '_min' : '_max'] = newValue || undefined;
132
131
  this.syncChildComponents();
133
132
  this.updateValidity();
134
133
  break;
134
+ case 'required':
135
+ this.updateValidity();
136
+ break;
135
137
  case 'flavor':
136
138
  this._flavor = newValue || 'primary';
137
139
  this.syncChildComponents();
@@ -165,6 +167,12 @@ this.setAttribute('flavor', value);
165
167
  else
166
168
  this.removeAttribute('flavor');
167
169
  }
170
+ get required() {
171
+ return this.hasAttribute('required');
172
+ }
173
+ set required(value) {
174
+ this.toggleAttribute('required', !!value);
175
+ }
168
176
  get year() {
169
177
  return this._state.selectedYear;
170
178
  }
@@ -172,12 +180,12 @@ set year(value) {
172
180
  if (value !== undefined) {
173
181
  if (this._stateless) {
174
182
  this._state.displayYear = value;
175
- this.setAttribute('year', value.toString());
183
+ this.setAttribute('year', formatYear(value));
176
184
  }
177
185
  else {
178
186
  this._state.selectedYear = value;
179
187
  this._state.displayYear = value;
180
- this.setAttribute('year', value.toString());
188
+ this.setAttribute('year', formatYear(value));
181
189
  }
182
190
  }
183
191
  else {
@@ -306,14 +314,14 @@ this.syncChildComponents();
306
314
  get value() {
307
315
  const { selectedYear, selectedMonth, selectedDay } = this._state;
308
316
  if (selectedYear && selectedMonth && selectedDay) {
309
- return formatDateISO(selectedYear, selectedMonth, selectedDay);
317
+ return formatISODay(selectedYear, selectedMonth, selectedDay);
310
318
  }
311
319
  return '';
312
320
  }
313
321
  set value(isoDate) {
314
322
  const set = ++this._valueSets;
315
323
  const superseded = () => set !== this._valueSets;
316
- if (!isoDate) {
324
+ if (isoDate === null || isoDate === undefined || isoDate === '') {
317
325
  delete this._state.selectedYear;
318
326
  delete this._state.selectedMonth;
319
327
  delete this._state.selectedDay;
@@ -328,18 +336,16 @@ this.removeAttribute('day');
328
336
  }
329
337
  }
330
338
  else {
331
- const match = typeof isoDate === 'string' && isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
332
- if (match) {
333
- const year = parseInt(match[1], 10);
334
- const month = parseInt(match[2], 10);
335
- const day = parseInt(match[3], 10);
339
+ const parsed = parseDateDay(isoDate);
340
+ if (parsed) {
341
+ const { year, month, day } = parsed;
336
342
  this._state.selectedYear = year;
337
343
  this._state.selectedMonth = month;
338
344
  this._state.selectedDay = day;
339
345
  this._state.displayYear = year;
340
346
  this._state.displayMonth = month;
341
347
  if (!this._stateless) {
342
- this.setAttribute('year', year.toString());
348
+ this.setAttribute('year', formatYear(year));
343
349
  if (superseded())
344
350
  return;
345
351
  this.setAttribute('month', month.toString());
@@ -455,13 +461,13 @@ this._monthDisplay.customCSS = this._customCSS;
455
461
  }
456
462
  const { selectedYear, selectedMonth, selectedDay } = this._state;
457
463
  if (selectedYear && selectedMonth && selectedDay) {
458
- const date = new Date(selectedYear, selectedMonth - 1, selectedDay);
459
- this._monthDisplay.value = date.getTime();
464
+ this._monthDisplay.value = localMidnight(selectedYear, selectedMonth, selectedDay).getTime();
460
465
  }
461
466
  else {
462
467
  this._monthDisplay.value = null;
463
468
  }
464
469
  }
470
+ this.updateValidity();
465
471
  }
466
472
  updateFormValue() {
467
473
  if (this._stateless)
@@ -471,7 +477,7 @@ return;
471
477
  const elementName = this.getAttribute('name');
472
478
  const { selectedYear, selectedMonth, selectedDay } = this._state;
473
479
  if (elementName && selectedYear && selectedMonth && selectedDay) {
474
- const isoDate = formatDateISO(selectedYear, selectedMonth, selectedDay);
480
+ const isoDate = formatISODay(selectedYear, selectedMonth, selectedDay);
475
481
  this._internals.setFormValue(isoDate);
476
482
  }
477
483
  else {
@@ -482,22 +488,27 @@ this.updateValidity();
482
488
  updateValidity() {
483
489
  if (!this._internals)
484
490
  return;
491
+ const anchor = this.gridCell() ?? undefined;
485
492
  const { selectedYear, selectedMonth, selectedDay } = this._state;
486
493
  if (selectedYear && selectedMonth && selectedDay) {
487
- const sel = Date.UTC(selectedYear, selectedMonth - 1, selectedDay);
488
- const min = this._min ? parseISODate(this._min) : null;
489
- const max = this._max ? parseISODate(this._max) : null;
490
- const message = () => nativeMessage('input', { type: 'date', min: this._min, max: this._max }, formatDateISO(selectedYear, selectedMonth, selectedDay));
491
- if (min && sel < Date.UTC(min.year, min.month - 1, min.day)) {
492
- this._validity.set({ rangeUnderflow: true }, message());
494
+ const sel = utcMidnight(selectedYear, selectedMonth, selectedDay);
495
+ const min = this._min ? parseDateDay(this._min) : null;
496
+ const max = this._max ? parseDateDay(this._max) : null;
497
+ const message = () => nativeMessage('input', { type: 'date', min: toISODay(this._min) ?? undefined, max: toISODay(this._max) ?? undefined }, formatISODay(selectedYear, selectedMonth, selectedDay));
498
+ if (min && sel < utcMidnight(min.year, min.month, min.day)) {
499
+ this._validity.set({ rangeUnderflow: true }, message(), anchor);
493
500
  return;
494
501
  }
495
- if (max && sel > Date.UTC(max.year, max.month - 1, max.day)) {
496
- this._validity.set({ rangeOverflow: true }, message());
502
+ if (max && sel > utcMidnight(max.year, max.month, max.day)) {
503
+ this._validity.set({ rangeOverflow: true }, message(), anchor);
497
504
  return;
498
505
  }
499
506
  }
500
- this._validity.set({});
507
+ else if (this.required) {
508
+ this._validity.set({ valueMissing: true }, nativeMessage('input', { type: 'date', required: true }), anchor);
509
+ return;
510
+ }
511
+ this._validity.set({}, undefined, anchor);
501
512
  }
502
513
  get form() { return this._internals?.form ?? null; }
503
514
  get validity() { return this._internals?.validity; }
@@ -547,7 +558,7 @@ this._state.selectedMonth = month;
547
558
  this._state.selectedDay = day;
548
559
  this._state.displayYear = year;
549
560
  this._state.displayMonth = month;
550
- this.setAttribute('year', year.toString());
561
+ this.setAttribute('year', formatYear(year));
551
562
  this.setAttribute('month', month.toString());
552
563
  this.setAttribute('day', day.toString());
553
564
  this.updateFormValue();
@@ -572,6 +583,7 @@ this.syncChildComponents();
572
583
  if (this._monthDisplay && typeof this._monthDisplay.refresh === 'function') {
573
584
  this._monthDisplay.refresh();
574
585
  }
586
+ this.updateValidity();
575
587
  }
576
588
  createNavigation() {
577
589
  const nav = document.createElement('ty-calendar-navigation');
@@ -607,8 +619,7 @@ month.width = this._width;
607
619
  }
608
620
  const { selectedYear, selectedMonth, selectedDay } = this._state;
609
621
  if (selectedYear && selectedMonth && selectedDay) {
610
- const date = new Date(selectedYear, selectedMonth - 1, selectedDay);
611
- month.value = date.getTime();
622
+ month.value = localMidnight(selectedYear, selectedMonth, selectedDay).getTime();
612
623
  }
613
624
  if (this._dayContentFn) {
614
625
  month.dayContentFn = this._dayContentFn;
@@ -617,6 +628,7 @@ if (this._customCSS) {
617
628
  month.customCSS = this._customCSS;
618
629
  }
619
630
  month.addEventListener('day-click', (e) => this.handleDayClick(e));
631
+ month.shadowRoot?.addEventListener('focusin', () => this.updateValidity());
620
632
  this._monthDisplay = month;
621
633
  return month;
622
634
  }
@@ -629,7 +641,23 @@ root.innerHTML = '';
629
641
  this._monthGrid = undefined;
630
642
  const container = document.createElement('div');
631
643
  container.className = 'calendar-container';
644
+ container.tabIndex = -1;
645
+ const press = () => {
646
+ this._pressing = true;
647
+ setTimeout(() => { this._pressing = false; });
648
+ };
649
+ container.addEventListener('pointerdown', press);
650
+ container.addEventListener('mousedown', press);
651
+ container.addEventListener('focus', () => {
652
+ if (this._pressing)
653
+ return;
654
+ queueMicrotask(() => {
655
+ if (this.shadowRoot?.activeElement === container)
656
+ this.gridCell()?.focus();
657
+ });
658
+ });
632
659
  container.addEventListener('keydown', (e) => this.handleKeydown(e));
660
+ container.addEventListener('focusin', () => this.updateValidity());
633
661
  this._container = container;
634
662
  if (!this._hideNavigation) {
635
663
  const nav = this.createNavigation();
@@ -640,6 +668,7 @@ container.appendChild(month);
640
668
  root.appendChild(container);
641
669
  if (this._view === 'months')
642
670
  this.showBody();
671
+ this.updateValidity();
643
672
  }
644
673
  setView(view) {
645
674
  if (view === this._view)
@@ -651,6 +680,7 @@ this._view = view;
651
680
  if (this._navigation)
652
681
  this._navigation.view = view;
653
682
  this.showBody();
683
+ this.updateValidity();
654
684
  }
655
685
  showBody() {
656
686
  const container = this._container;
@@ -676,8 +706,8 @@ static monthIndex(year, month) {
676
706
  return year * 12 + (month - 1);
677
707
  }
678
708
  boundIndices() {
679
- const min = this._min ? parseISODate(this._min) : null;
680
- const max = this._max ? parseISODate(this._max) : null;
709
+ const min = this._min ? parseDateDay(this._min) : null;
710
+ const max = this._max ? parseDateDay(this._max) : null;
681
711
  return {
682
712
  minIdx: min ? TyCalendar.monthIndex(min.year, min.month) : null,
683
713
  maxIdx: max ? TyCalendar.monthIndex(max.year, max.month) : null,
@@ -742,6 +772,11 @@ this.goTo(month, this._state.displayYear);
742
772
  this.focusDayGrid();
743
773
  }
744
774
  handleKeydown(e) {
775
+ if (e.target === this._container && e.key.startsWith('Arrow')) {
776
+ e.preventDefault();
777
+ this.gridCell()?.focus();
778
+ return;
779
+ }
745
780
  if (e.key === 'Escape' && this._view === 'months') {
746
781
  e.preventDefault();
747
782
  e.stopPropagation();
@@ -777,10 +812,21 @@ requestAnimationFrame(() => {
777
812
  this._navigation?.shadowRoot?.querySelector('.nav-title')?.focus();
778
813
  });
779
814
  }
815
+ gridCell() {
816
+ if (this._view === 'months') {
817
+ return this._monthGrid?.querySelector('.month-cell[tabindex="0"]:not(:disabled)')
818
+ ?? this._monthGrid?.querySelector('.month-cell:not(:disabled)')
819
+ ?? this._navigation?.shadowRoot?.querySelector('.nav-title')
820
+ ?? null;
821
+ }
822
+ const root = this._monthDisplay?.shadowRoot;
823
+ return root?.querySelector('.calendar-day-cell[tabindex="0"]')
824
+ ?? root?.querySelector('.calendar-day-cell.selected')
825
+ ?? root?.querySelector('.calendar-day-cell')
826
+ ?? null;
827
+ }
780
828
  focus(options) {
781
- const cell = this._view === 'months'
782
- ? this._monthGrid?.querySelector('.month-cell[tabindex="0"]')
783
- : this._monthDisplay?.shadowRoot?.querySelector('.calendar-day-cell[tabindex="0"]');
829
+ const cell = this.gridCell();
784
830
  if (cell)
785
831
  cell.focus(options);
786
832
  else
@@ -32,6 +32,8 @@ export interface TyCheckboxElement extends HTMLElement {
32
32
  */
33
33
  export declare class TyCheckbox extends TyComponent<CheckboxState> implements TyCheckboxElement {
34
34
  static formAssociated: boolean;
35
+ protected static delegatesFocus: boolean;
36
+ protected static labelClicks: boolean;
35
37
  protected static properties: {
36
38
  checked: {
37
39
  type: "boolean";
@@ -98,6 +100,9 @@ export declare class TyCheckbox extends TyComponent<CheckboxState> implements Ty
98
100
  protected getFormValue(): FormDataEntryValue | null;
99
101
  private buildClassList;
100
102
  private handleCheckboxClick;
103
+ /** The ring is for keyboard use: keys show it, a click hides it — also a
104
+ * <label>'s, which in Chromium focuses the box as :focus-visible. */
105
+ private showFocusRing;
101
106
  private handleCheckboxKeydown;
102
107
  private removeEventListeners;
103
108
  /**
@@ -21,6 +21,9 @@ ensureStyles(shadow, { css: checkboxStyles, id: "ty-checkbox" });
21
21
  onConnect() {
22
22
  this.setupEventListeners();
23
23
  this._syncCustomFlavor();
24
+ const el = this.shadowRoot.querySelector(".checkbox-container");
25
+ if (el && this.shadowRoot.activeElement !== el)
26
+ el.classList.remove("focused");
24
27
  }
25
28
  _syncCustomFlavor() {
26
29
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.flavor, ({ base }) => checkboxCustomFlavorCss(base));
@@ -47,8 +50,7 @@ return classes.join(" ");
47
50
  handleCheckboxClick(e) {
48
51
  if (this.disabled)
49
52
  return;
50
- if (this.isDuplicateActivationClick())
51
- return;
53
+ this.showFocusRing(e instanceof KeyboardEvent);
52
54
  const newValue = this.indeterminate ? true : !this.checked;
53
55
  if (this.indeterminate)
54
56
  this.indeterminate = false;
@@ -72,6 +74,9 @@ composed: true,
72
74
  }));
73
75
  }, 0);
74
76
  }
77
+ showFocusRing(on) {
78
+ this.shadowRoot?.querySelector(".checkbox-container")?.classList.toggle("focused", on);
79
+ }
75
80
  handleCheckboxKeydown(e) {
76
81
  if (this.disabled)
77
82
  return;
@@ -124,7 +129,7 @@ this._keydownHandler = (e) => {
124
129
  this.handleCheckboxKeydown(e);
125
130
  };
126
131
  this._focusHandler = () => {
127
- checkboxEl.classList.add("focused");
132
+ checkboxEl.classList.toggle("focused", checkboxEl.matches(":focus-visible"));
128
133
  };
129
134
  this._blurHandler = () => {
130
135
  checkboxEl.classList.remove("focused");
@@ -143,7 +148,8 @@ const ariaChecked = this.indeterminate ? "mixed" : String(this.checked);
143
148
  if (!checkboxEl) {
144
149
  checkboxEl = document.createElement("div");
145
150
  checkboxEl.className = "checkbox-container " + classes;
146
- checkboxEl.tabIndex = this.disabled ? -1 : 0;
151
+ if (!this.disabled)
152
+ checkboxEl.tabIndex = 0;
147
153
  checkboxEl.setAttribute("role", "checkbox");
148
154
  checkboxEl.setAttribute("aria-checked", ariaChecked);
149
155
  checkboxEl.setAttribute("aria-disabled", String(this.disabled));
@@ -154,8 +160,13 @@ checkboxEl.appendChild(checkboxIcon);
154
160
  shadow.appendChild(checkboxEl);
155
161
  }
156
162
  else {
163
+ const focused = checkboxEl.classList.contains("focused") && shadow.activeElement === checkboxEl;
157
164
  checkboxEl.className = "checkbox-container " + classes;
158
- checkboxEl.tabIndex = this.disabled ? -1 : 0;
165
+ checkboxEl.classList.toggle("focused", focused && !this.disabled);
166
+ if (this.disabled)
167
+ checkboxEl.removeAttribute("tabindex");
168
+ else
169
+ checkboxEl.tabIndex = 0;
159
170
  checkboxEl.setAttribute("aria-checked", ariaChecked);
160
171
  checkboxEl.setAttribute("aria-disabled", String(this.disabled));
161
172
  checkboxEl.setAttribute("aria-required", String(this.required));
@@ -239,6 +250,8 @@ return this._internals.form;
239
250
  }
240
251
  }
241
252
  TyCheckbox.formAssociated = true;
253
+ TyCheckbox.delegatesFocus = true;
254
+ TyCheckbox.labelClicks = true;
242
255
  TyCheckbox.properties = {
243
256
  checked: {
244
257
  type: "boolean",
@@ -109,6 +109,8 @@ export declare class TyCopy extends TyComponent<CopyState> implements TyCopyElem
109
109
  * Uses state flag so render() can show the correct icon
110
110
  */
111
111
  private showCopySuccess;
112
+ /** Feedback over: the copy icon again. */
113
+ private resetCopyButton;
112
114
  /**
113
115
  * Show copy error animation
114
116
  * Swaps copy icon → red X icon → copy icon
@@ -32,7 +32,7 @@ this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFla
32
32
  onDisconnect() {
33
33
  if (this._copyTimeout !== null) {
34
34
  clearTimeout(this._copyTimeout);
35
- this._copyTimeout = null;
35
+ this.resetCopyButton();
36
36
  }
37
37
  }
38
38
  onPropertiesChanged(changes) {
@@ -61,15 +61,15 @@ composed: true
61
61
  }));
62
62
  }
63
63
  catch (err) {
64
- try {
64
+ const focused = this.shadowRoot.activeElement;
65
65
  const textarea = document.createElement('textarea');
66
+ try {
66
67
  textarea.value = value;
67
68
  textarea.style.position = 'fixed';
68
69
  textarea.style.opacity = '0';
69
70
  document.body.appendChild(textarea);
70
71
  textarea.select();
71
72
  document.execCommand('copy');
72
- document.body.removeChild(textarea);
73
73
  this.dispatchEvent(new CustomEvent('copy-success', {
74
74
  detail: { value },
75
75
  bubbles: true,
@@ -86,6 +86,10 @@ composed: true
86
86
  this.showCopyError();
87
87
  return;
88
88
  }
89
+ finally {
90
+ textarea.remove();
91
+ focused?.focus();
92
+ }
89
93
  }
90
94
  this.showCopySuccess();
91
95
  }
@@ -100,16 +104,17 @@ copyButton.classList.add('success');
100
104
  copyButton.innerHTML = CHECK_ICON_SVG;
101
105
  copyButton.setAttribute('aria-label', 'Copied!');
102
106
  }
103
- this._copyTimeout = window.setTimeout(() => {
107
+ this._copyTimeout = window.setTimeout(() => this.resetCopyButton(), 2000);
108
+ }
109
+ resetCopyButton() {
104
110
  this._showingSuccess = false;
105
111
  const btn = this.shadowRoot.querySelector('.copy-button');
106
112
  if (btn) {
107
- btn.classList.remove('success');
113
+ btn.classList.remove('success', 'error');
108
114
  btn.innerHTML = COPY_ICON_SVG;
109
115
  btn.setAttribute('aria-label', 'Copy to clipboard');
110
116
  }
111
117
  this._copyTimeout = null;
112
- }, 2000);
113
118
  }
114
119
  showCopyError() {
115
120
  if (this._copyTimeout !== null) {
@@ -122,34 +127,13 @@ copyButton.classList.add('error');
122
127
  copyButton.innerHTML = ERROR_ICON_SVG;
123
128
  copyButton.setAttribute('aria-label', 'Copy failed');
124
129
  }
125
- this._copyTimeout = window.setTimeout(() => {
126
- const btn = this.shadowRoot.querySelector('.copy-button');
127
- if (btn) {
128
- btn.classList.remove('error');
129
- btn.innerHTML = COPY_ICON_SVG;
130
- btn.setAttribute('aria-label', 'Copy to clipboard');
131
- }
132
- this._copyTimeout = null;
133
- }, 2000);
130
+ this._copyTimeout = window.setTimeout(() => this.resetCopyButton(), 2000);
134
131
  }
135
132
  setupEventListeners() {
136
- const shadow = this.shadowRoot;
137
- const inputWrapper = shadow.querySelector('.input-wrapper');
138
- const copyButton = shadow.querySelector('.copy-button');
133
+ const inputWrapper = this.shadowRoot.querySelector('.input-wrapper');
139
134
  if (inputWrapper && !this.disabled) {
140
- const wrapperEl = inputWrapper;
141
- wrapperEl.addEventListener('click', (e) => {
142
- if (e.target === copyButton || e.target.closest('.copy-button')) {
143
- return;
144
- }
145
- this.copyToClipboard();
146
- });
147
- wrapperEl.style.cursor = 'pointer';
148
- }
149
- if (copyButton) {
150
- copyButton.addEventListener('click', () => {
151
- this.copyToClipboard();
152
- });
135
+ inputWrapper.addEventListener('click', () => this.copyToClipboard());
136
+ inputWrapper.style.cursor = 'pointer';
153
137
  }
154
138
  }
155
139
  render() {
@@ -8,22 +8,11 @@ import { type Placement } from '../utils/positioning.js';
8
8
  import { TyComponent } from '../base/ty-component.js';
9
9
  import type { Flavor } from '../types/common.js';
10
10
  import type { PropertyChange } from '../utils/property-manager.js';
11
- /**
12
- * Date components (internal representation in local timezone)
13
- */
14
- interface DateComponents {
15
- year?: number;
16
- month?: number;
17
- day?: number;
18
- hour?: number;
19
- minute?: number;
20
- second?: number;
21
- epoch?: number;
22
- }
11
+ import { type DateTimeParts } from '../utils/calendar-utils.js';
23
12
  /**
24
13
  * Internal date picker state
25
14
  */
26
- interface DatePickerState extends DateComponents {
15
+ interface DatePickerState extends DateTimeParts {
27
16
  withTime: boolean;
28
17
  open: boolean;
29
18
  }
@@ -193,14 +182,6 @@ export declare class TyDatePicker extends TyComponent<DatePickerState> {
193
182
  * Compares before setting to prevent circular triggers
194
183
  */
195
184
  private syncFormValue;
196
- /**
197
- * `error` turns an unflavored field ('default'/'neutral') danger, like
198
- * ty-input; clearing it puts back exactly the flavor attribute the author
199
- * had, or none, kept meanwhile in ERROR_FLAVOR. Runs once connected: while
200
- * the element upgrades, `error` may come before the `flavor` written next
201
- * to it.
202
- */
203
- private syncErrorFlavor;
204
185
  /**
205
186
  * Handle time changes from the footer's hour/minute fields
206
187
  */
@@ -214,7 +195,6 @@ export declare class TyDatePicker extends TyComponent<DatePickerState> {
214
195
  set placement(v: Placement | '');
215
196
  get flavor(): DatePickerFlavor;
216
197
  set flavor(v: DatePickerFlavor);
217
- attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
218
198
  get label(): string;
219
199
  set label(v: string);
220
200
  get placeholder(): string;
@@ -248,7 +228,7 @@ export declare class TyDatePicker extends TyComponent<DatePickerState> {
248
228
  */
249
229
  get value(): string | null;
250
230
  /**
251
- * Set value: any format parseValue() reads, a Date, epoch milliseconds,
231
+ * Set value: any format parseDateTime() reads, a Date, epoch milliseconds,
252
232
  * or null. It's normalized to the submitted form.
253
233
  *
254
234
  * When set to null/undefined/empty string, the attribute is removed.
@@ -268,9 +248,10 @@ export declare class TyDatePicker extends TyComponent<DatePickerState> {
268
248
  private renderTimeSection;
269
249
  private renderCalendarDropdown;
270
250
  /**
271
- * Match the popup calendar to the stub's flavor — the one the user set, not
272
- * the danger an error swapped in. 'default' is the stub's own unstyled
273
- * state, not a real flavor: the calendar stays on its own default.
251
+ * Match the popup calendar to the stub's look: danger while an error
252
+ * shows, as on the stub (styles: errorLook), else the user's flavor.
253
+ * 'default' is the stub's own unstyled state, not a real flavor: the
254
+ * calendar stays on its own default.
274
255
  */
275
256
  private calendarFlavor;
276
257
  /**