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
@@ -50,6 +50,8 @@ export declare class TyCalendar extends HTMLElement {
50
50
  private _internals?;
51
51
  private _validity?;
52
52
  private _initialValue;
53
+ private _valueSets;
54
+ private _pressing;
53
55
  private _localeObserver?;
54
56
  static formAssociated: boolean;
55
57
  static get observedAttributes(): string[];
@@ -63,6 +65,8 @@ export declare class TyCalendar extends HTMLElement {
63
65
  set max(value: string | undefined);
64
66
  get flavor(): string;
65
67
  set flavor(value: string);
68
+ get required(): boolean;
69
+ set required(value: boolean);
66
70
  get year(): number | undefined;
67
71
  set year(value: number | undefined);
68
72
  get month(): number | undefined;
@@ -84,7 +88,12 @@ export declare class TyCalendar extends HTMLElement {
84
88
  get customCSS(): CSSStyleSheet | undefined;
85
89
  set customCSS(sheet: CSSStyleSheet | undefined);
86
90
  get value(): string;
87
- set value(isoDate: string);
91
+ /**
92
+ * Reads what ty-date-picker reads: 'YYYY-MM-DD', 'YYYY-MM-DDTHH:mm[:ss]'
93
+ * (that day), an ISO instant with Z/offset or epoch ms / a Date (its local
94
+ * day). The value stays 'YYYY-MM-DD'; anything else is no value.
95
+ */
96
+ set value(isoDate: string | number | Date | null | undefined);
88
97
  /**
89
98
  * Initialize state from HTML attributes on first load
90
99
  */
@@ -94,8 +103,12 @@ export declare class TyCalendar extends HTMLElement {
94
103
  private syncChildComponents;
95
104
  private updateFormValue;
96
105
  /**
97
- * Selection outside [min, max] => rangeUnderflow/rangeOverflow.
98
- * UI can't produce this (days are disabled), but programmatic values can.
106
+ * required with no selection => valueMissing; a selection outside
107
+ * [min, max] => rangeUnderflow/rangeOverflow (UI can't pick one, days are
108
+ * disabled, but programmatic values can). With a native date input's
109
+ * message. The grid's tab stop is the anchor, so a submit focuses it and
110
+ * the browser shows its bubble there; the cells are rebuilt on every grid
111
+ * render, so each one runs this again.
99
112
  */
100
113
  private updateValidity;
101
114
  get form(): HTMLFormElement | null;
@@ -128,7 +141,15 @@ export declare class TyCalendar extends HTMLElement {
128
141
  private pickMonth;
129
142
  private handleKeydown;
130
143
  private focusNavTitle;
131
- /** Focus the grid's tab stop (the focused day, or month in the month view). */
144
+ /**
145
+ * The grid's tab stop: the focused day, or month in the month view. When
146
+ * every day shown is disabled (a value outside min/max) there is none: then
147
+ * the selected day, or the first one; disabled cells still take focus.
148
+ * Disabled month cells don't (buttons): with every month of the year
149
+ * disabled, the navigation title (back to the days) stands in.
150
+ */
151
+ private gridCell;
152
+ /** Focus the grid's tab stop (see gridCell). */
132
153
  focus(options?: FocusOptions): void;
133
154
  private focusDayGrid;
134
155
  }
@@ -1,10 +1,11 @@
1
1
  import { ensureStyles } from '../utils/styles.js';
2
+ import { liftProperties, upgradeProperties } from '../utils/upgrade-properties.js';
2
3
  import { calendarStyles } from '../styles/calendar.js';
3
4
  import './calendar-month.js';
4
5
  import './calendar-navigation.js';
5
- import { parseISODate, getMonthName } from '../utils/calendar-utils.js';
6
+ import { parseDateDay, formatISODay, toISODay, getMonthName, localMidnight, utcMidnight } from '../utils/calendar-utils.js';
6
7
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
7
- import { ValidityController } from '../utils/validity.js';
8
+ import { ValidityController, nativeMessage } from '../utils/validity.js';
8
9
  function getCurrentDate() {
9
10
  const now = new Date();
10
11
  return {
@@ -31,14 +32,12 @@ const day = parseInt(dayStr, 10);
31
32
  const daysInMonth = new Date(year, month, 0).getDate();
32
33
  return day >= 1 && day <= daysInMonth ? day : null;
33
34
  }
34
- function formatDateISO(year, month, day) {
35
- const monthStr = month.toString().padStart(2, '0');
36
- const dayStr = day.toString().padStart(2, '0');
37
- return `${year}-${monthStr}-${dayStr}`;
35
+ function formatYear(year) {
36
+ return year.toString().padStart(4, '0');
38
37
  }
39
38
  export class TyCalendar extends HTMLElement {
40
39
  static get observedAttributes() {
41
- 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'];
42
41
  }
43
42
  constructor() {
44
43
  super();
@@ -49,12 +48,15 @@ this._flavor = 'primary';
49
48
  this._view = 'days';
50
49
  this._dayGridHeight = 0;
51
50
  this._initialValue = '';
51
+ this._valueSets = 0;
52
+ this._pressing = false;
52
53
  const current = getCurrentDate();
53
54
  this._state = {
54
55
  displayYear: current.year,
55
56
  displayMonth: current.month,
56
57
  };
57
- this.attachShadow({ mode: 'open' });
58
+ this.attachShadow({ mode: 'open', delegatesFocus: true });
59
+ liftProperties(this);
58
60
  if ('attachInternals' in this) {
59
61
  this._internals = this.attachInternals();
60
62
  this._validity = new ValidityController(this, this._internals);
@@ -62,36 +64,7 @@ this._validity = new ValidityController(this, this._internals);
62
64
  }
63
65
  connectedCallback() {
64
66
  this.initializeFromAttributes();
65
- const plainDayContentFn = this.dayContentFn;
66
- if (plainDayContentFn && !this._dayContentFn) {
67
- this._dayContentFn = plainDayContentFn;
68
- delete this.dayContentFn;
69
- }
70
- const plainCustomCSS = this.customCSS;
71
- if (plainCustomCSS && !this._customCSS) {
72
- this._customCSS = plainCustomCSS;
73
- delete this.customCSS;
74
- }
75
- const plainValue = this.value;
76
- if (plainValue && typeof plainValue === 'string' && plainValue !== this.value) {
77
- const match = plainValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
78
- if (match) {
79
- const year = parseInt(match[1], 10);
80
- const month = parseInt(match[2], 10);
81
- const day = parseInt(match[3], 10);
82
- this._state.selectedYear = year;
83
- this._state.selectedMonth = month;
84
- this._state.selectedDay = day;
85
- this._state.displayYear = year;
86
- this._state.displayMonth = month;
87
- if (!this._stateless) {
88
- this.setAttribute('year', year.toString());
89
- this.setAttribute('month', month.toString());
90
- this.setAttribute('day', day.toString());
91
- }
92
- }
93
- delete this.value;
94
- }
67
+ upgradeProperties(this);
95
68
  this.render();
96
69
  this.updateFormValue();
97
70
  this._localeObserver = observeLocaleChanges(this, () => {
@@ -158,6 +131,9 @@ this[name === 'min' ? '_min' : '_max'] = newValue || undefined;
158
131
  this.syncChildComponents();
159
132
  this.updateValidity();
160
133
  break;
134
+ case 'required':
135
+ this.updateValidity();
136
+ break;
161
137
  case 'flavor':
162
138
  this._flavor = newValue || 'primary';
163
139
  this.syncChildComponents();
@@ -191,6 +167,12 @@ this.setAttribute('flavor', value);
191
167
  else
192
168
  this.removeAttribute('flavor');
193
169
  }
170
+ get required() {
171
+ return this.hasAttribute('required');
172
+ }
173
+ set required(value) {
174
+ this.toggleAttribute('required', !!value);
175
+ }
194
176
  get year() {
195
177
  return this._state.selectedYear;
196
178
  }
@@ -198,12 +180,12 @@ set year(value) {
198
180
  if (value !== undefined) {
199
181
  if (this._stateless) {
200
182
  this._state.displayYear = value;
201
- this.setAttribute('year', value.toString());
183
+ this.setAttribute('year', formatYear(value));
202
184
  }
203
185
  else {
204
186
  this._state.selectedYear = value;
205
187
  this._state.displayYear = value;
206
- this.setAttribute('year', value.toString());
188
+ this.setAttribute('year', formatYear(value));
207
189
  }
208
190
  }
209
191
  else {
@@ -332,35 +314,43 @@ this.syncChildComponents();
332
314
  get value() {
333
315
  const { selectedYear, selectedMonth, selectedDay } = this._state;
334
316
  if (selectedYear && selectedMonth && selectedDay) {
335
- return formatDateISO(selectedYear, selectedMonth, selectedDay);
317
+ return formatISODay(selectedYear, selectedMonth, selectedDay);
336
318
  }
337
319
  return '';
338
320
  }
339
321
  set value(isoDate) {
340
- if (!isoDate) {
322
+ const set = ++this._valueSets;
323
+ const superseded = () => set !== this._valueSets;
324
+ if (isoDate === null || isoDate === undefined || isoDate === '') {
341
325
  delete this._state.selectedYear;
342
326
  delete this._state.selectedMonth;
343
327
  delete this._state.selectedDay;
344
328
  if (!this._stateless) {
345
329
  this.removeAttribute('year');
330
+ if (superseded())
331
+ return;
346
332
  this.removeAttribute('month');
333
+ if (superseded())
334
+ return;
347
335
  this.removeAttribute('day');
348
336
  }
349
337
  }
350
338
  else {
351
- const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
352
- if (match) {
353
- const year = parseInt(match[1], 10);
354
- const month = parseInt(match[2], 10);
355
- const day = parseInt(match[3], 10);
339
+ const parsed = parseDateDay(isoDate);
340
+ if (parsed) {
341
+ const { year, month, day } = parsed;
356
342
  this._state.selectedYear = year;
357
343
  this._state.selectedMonth = month;
358
344
  this._state.selectedDay = day;
359
345
  this._state.displayYear = year;
360
346
  this._state.displayMonth = month;
361
347
  if (!this._stateless) {
362
- this.setAttribute('year', year.toString());
348
+ this.setAttribute('year', formatYear(year));
349
+ if (superseded())
350
+ return;
363
351
  this.setAttribute('month', month.toString());
352
+ if (superseded())
353
+ return;
364
354
  this.setAttribute('day', day.toString());
365
355
  }
366
356
  }
@@ -471,13 +461,13 @@ this._monthDisplay.customCSS = this._customCSS;
471
461
  }
472
462
  const { selectedYear, selectedMonth, selectedDay } = this._state;
473
463
  if (selectedYear && selectedMonth && selectedDay) {
474
- const date = new Date(selectedYear, selectedMonth - 1, selectedDay);
475
- this._monthDisplay.value = date.getTime();
464
+ this._monthDisplay.value = localMidnight(selectedYear, selectedMonth, selectedDay).getTime();
476
465
  }
477
466
  else {
478
467
  this._monthDisplay.value = null;
479
468
  }
480
469
  }
470
+ this.updateValidity();
481
471
  }
482
472
  updateFormValue() {
483
473
  if (this._stateless)
@@ -487,7 +477,7 @@ return;
487
477
  const elementName = this.getAttribute('name');
488
478
  const { selectedYear, selectedMonth, selectedDay } = this._state;
489
479
  if (elementName && selectedYear && selectedMonth && selectedDay) {
490
- const isoDate = formatDateISO(selectedYear, selectedMonth, selectedDay);
480
+ const isoDate = formatISODay(selectedYear, selectedMonth, selectedDay);
491
481
  this._internals.setFormValue(isoDate);
492
482
  }
493
483
  else {
@@ -498,21 +488,27 @@ this.updateValidity();
498
488
  updateValidity() {
499
489
  if (!this._internals)
500
490
  return;
491
+ const anchor = this.gridCell() ?? undefined;
501
492
  const { selectedYear, selectedMonth, selectedDay } = this._state;
502
493
  if (selectedYear && selectedMonth && selectedDay) {
503
- const sel = Date.UTC(selectedYear, selectedMonth - 1, selectedDay);
504
- const min = this._min ? parseISODate(this._min) : null;
505
- const max = this._max ? parseISODate(this._max) : null;
506
- if (min && sel < Date.UTC(min.year, min.month - 1, min.day)) {
507
- this._validity.set({ rangeUnderflow: true }, `Date must be ${this._min} or later`);
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);
508
500
  return;
509
501
  }
510
- if (max && sel > Date.UTC(max.year, max.month - 1, max.day)) {
511
- this._validity.set({ rangeOverflow: true }, `Date must be ${this._max} or earlier`);
502
+ if (max && sel > utcMidnight(max.year, max.month, max.day)) {
503
+ this._validity.set({ rangeOverflow: true }, message(), anchor);
512
504
  return;
513
505
  }
514
506
  }
515
- 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);
516
512
  }
517
513
  get form() { return this._internals?.form ?? null; }
518
514
  get validity() { return this._internals?.validity; }
@@ -562,7 +558,7 @@ this._state.selectedMonth = month;
562
558
  this._state.selectedDay = day;
563
559
  this._state.displayYear = year;
564
560
  this._state.displayMonth = month;
565
- this.setAttribute('year', year.toString());
561
+ this.setAttribute('year', formatYear(year));
566
562
  this.setAttribute('month', month.toString());
567
563
  this.setAttribute('day', day.toString());
568
564
  this.updateFormValue();
@@ -587,6 +583,7 @@ this.syncChildComponents();
587
583
  if (this._monthDisplay && typeof this._monthDisplay.refresh === 'function') {
588
584
  this._monthDisplay.refresh();
589
585
  }
586
+ this.updateValidity();
590
587
  }
591
588
  createNavigation() {
592
589
  const nav = document.createElement('ty-calendar-navigation');
@@ -622,8 +619,7 @@ month.width = this._width;
622
619
  }
623
620
  const { selectedYear, selectedMonth, selectedDay } = this._state;
624
621
  if (selectedYear && selectedMonth && selectedDay) {
625
- const date = new Date(selectedYear, selectedMonth - 1, selectedDay);
626
- month.value = date.getTime();
622
+ month.value = localMidnight(selectedYear, selectedMonth, selectedDay).getTime();
627
623
  }
628
624
  if (this._dayContentFn) {
629
625
  month.dayContentFn = this._dayContentFn;
@@ -632,6 +628,7 @@ if (this._customCSS) {
632
628
  month.customCSS = this._customCSS;
633
629
  }
634
630
  month.addEventListener('day-click', (e) => this.handleDayClick(e));
631
+ month.shadowRoot?.addEventListener('focusin', () => this.updateValidity());
635
632
  this._monthDisplay = month;
636
633
  return month;
637
634
  }
@@ -644,7 +641,23 @@ root.innerHTML = '';
644
641
  this._monthGrid = undefined;
645
642
  const container = document.createElement('div');
646
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
+ });
647
659
  container.addEventListener('keydown', (e) => this.handleKeydown(e));
660
+ container.addEventListener('focusin', () => this.updateValidity());
648
661
  this._container = container;
649
662
  if (!this._hideNavigation) {
650
663
  const nav = this.createNavigation();
@@ -655,6 +668,7 @@ container.appendChild(month);
655
668
  root.appendChild(container);
656
669
  if (this._view === 'months')
657
670
  this.showBody();
671
+ this.updateValidity();
658
672
  }
659
673
  setView(view) {
660
674
  if (view === this._view)
@@ -666,6 +680,7 @@ this._view = view;
666
680
  if (this._navigation)
667
681
  this._navigation.view = view;
668
682
  this.showBody();
683
+ this.updateValidity();
669
684
  }
670
685
  showBody() {
671
686
  const container = this._container;
@@ -691,8 +706,8 @@ static monthIndex(year, month) {
691
706
  return year * 12 + (month - 1);
692
707
  }
693
708
  boundIndices() {
694
- const min = this._min ? parseISODate(this._min) : null;
695
- 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;
696
711
  return {
697
712
  minIdx: min ? TyCalendar.monthIndex(min.year, min.month) : null,
698
713
  maxIdx: max ? TyCalendar.monthIndex(max.year, max.month) : null,
@@ -757,6 +772,11 @@ this.goTo(month, this._state.displayYear);
757
772
  this.focusDayGrid();
758
773
  }
759
774
  handleKeydown(e) {
775
+ if (e.target === this._container && e.key.startsWith('Arrow')) {
776
+ e.preventDefault();
777
+ this.gridCell()?.focus();
778
+ return;
779
+ }
760
780
  if (e.key === 'Escape' && this._view === 'months') {
761
781
  e.preventDefault();
762
782
  e.stopPropagation();
@@ -792,10 +812,21 @@ requestAnimationFrame(() => {
792
812
  this._navigation?.shadowRoot?.querySelector('.nav-title')?.focus();
793
813
  });
794
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
+ }
795
828
  focus(options) {
796
- const cell = this._view === 'months'
797
- ? this._monthGrid?.querySelector('.month-cell[tabindex="0"]')
798
- : this._monthDisplay?.shadowRoot?.querySelector('.calendar-day-cell[tabindex="0"]');
829
+ const cell = this.gridCell();
799
830
  if (cell)
800
831
  cell.focus(options);
801
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
  /**
@@ -112,7 +117,7 @@ export declare class TyCheckbox extends TyComponent<CheckboxState> implements Ty
112
117
  protected render(): void;
113
118
  /** Focus the checkbox itself (the host is not focusable; its role element is). */
114
119
  focus(options?: FocusOptions): void;
115
- /** required + unchecked => valueMissing; otherwise valid. */
120
+ /** required + unchecked => valueMissing (the browser's own message); otherwise valid. */
116
121
  private updateValidity;
117
122
  get checked(): boolean;
118
123
  set checked(value: boolean);
@@ -3,6 +3,7 @@ import { TyComponent } from "../base/ty-component.js";
3
3
  import { ensureStyles } from "../utils/styles.js";
4
4
  import { syncCustomFlavorSheet } from "../utils/flavor-sheet.js";
5
5
  import { checkboxStyles, checkboxCustomFlavorCss } from "../styles/checkbox.js";
6
+ import { nativeMessage } from "../utils/validity.js";
6
7
  const CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`;
7
8
  const DASH_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>`;
8
9
  export class TyCheckbox extends TyComponent {
@@ -20,6 +21,9 @@ ensureStyles(shadow, { css: checkboxStyles, id: "ty-checkbox" });
20
21
  onConnect() {
21
22
  this.setupEventListeners();
22
23
  this._syncCustomFlavor();
24
+ const el = this.shadowRoot.querySelector(".checkbox-container");
25
+ if (el && this.shadowRoot.activeElement !== el)
26
+ el.classList.remove("focused");
23
27
  }
24
28
  _syncCustomFlavor() {
25
29
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, this.flavor, ({ base }) => checkboxCustomFlavorCss(base));
@@ -46,8 +50,7 @@ return classes.join(" ");
46
50
  handleCheckboxClick(e) {
47
51
  if (this.disabled)
48
52
  return;
49
- if (this.isDuplicateActivationClick())
50
- return;
53
+ this.showFocusRing(e instanceof KeyboardEvent);
51
54
  const newValue = this.indeterminate ? true : !this.checked;
52
55
  if (this.indeterminate)
53
56
  this.indeterminate = false;
@@ -71,6 +74,9 @@ composed: true,
71
74
  }));
72
75
  }, 0);
73
76
  }
77
+ showFocusRing(on) {
78
+ this.shadowRoot?.querySelector(".checkbox-container")?.classList.toggle("focused", on);
79
+ }
74
80
  handleCheckboxKeydown(e) {
75
81
  if (this.disabled)
76
82
  return;
@@ -123,7 +129,7 @@ this._keydownHandler = (e) => {
123
129
  this.handleCheckboxKeydown(e);
124
130
  };
125
131
  this._focusHandler = () => {
126
- checkboxEl.classList.add("focused");
132
+ checkboxEl.classList.toggle("focused", checkboxEl.matches(":focus-visible"));
127
133
  };
128
134
  this._blurHandler = () => {
129
135
  checkboxEl.classList.remove("focused");
@@ -142,7 +148,8 @@ const ariaChecked = this.indeterminate ? "mixed" : String(this.checked);
142
148
  if (!checkboxEl) {
143
149
  checkboxEl = document.createElement("div");
144
150
  checkboxEl.className = "checkbox-container " + classes;
145
- checkboxEl.tabIndex = this.disabled ? -1 : 0;
151
+ if (!this.disabled)
152
+ checkboxEl.tabIndex = 0;
146
153
  checkboxEl.setAttribute("role", "checkbox");
147
154
  checkboxEl.setAttribute("aria-checked", ariaChecked);
148
155
  checkboxEl.setAttribute("aria-disabled", String(this.disabled));
@@ -153,8 +160,13 @@ checkboxEl.appendChild(checkboxIcon);
153
160
  shadow.appendChild(checkboxEl);
154
161
  }
155
162
  else {
163
+ const focused = checkboxEl.classList.contains("focused") && shadow.activeElement === checkboxEl;
156
164
  checkboxEl.className = "checkbox-container " + classes;
157
- 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;
158
170
  checkboxEl.setAttribute("aria-checked", ariaChecked);
159
171
  checkboxEl.setAttribute("aria-disabled", String(this.disabled));
160
172
  checkboxEl.setAttribute("aria-required", String(this.required));
@@ -179,7 +191,7 @@ this.applyValidity({});
179
191
  return;
180
192
  }
181
193
  if (this.required && !this.checked) {
182
- this.applyValidity({ valueMissing: true }, "Please check this box", anchor ?? undefined);
194
+ this.applyValidity({ valueMissing: true }, nativeMessage("input", { type: "checkbox", required: true }), anchor ?? undefined);
183
195
  }
184
196
  else {
185
197
  this.applyValidity({});
@@ -238,6 +250,8 @@ return this._internals.form;
238
250
  }
239
251
  }
240
252
  TyCheckbox.formAssociated = true;
253
+ TyCheckbox.delegatesFocus = true;
254
+ TyCheckbox.labelClicks = true;
241
255
  TyCheckbox.properties = {
242
256
  checked: {
243
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() {