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
@@ -9,17 +9,28 @@
9
9
  * - Property change events via 'prop:change' (separate from user 'change' events)
10
10
  */
11
11
  import { PropertyManager, PropertyConfig, PropertyChange } from '../utils/property-manager.js';
12
+ import { upgradeProperties } from '../utils/upgrade-properties.js';
13
+ export { upgradeProperties };
12
14
  /**
13
15
  * Base class for all Ty components with unified property lifecycle
14
16
  */
15
17
  export declare abstract class TyComponent<T = any> extends HTMLElement {
16
18
  static formAssociated: boolean;
17
19
  protected static properties: Record<string, PropertyConfig>;
20
+ /** attachShadow delegatesFocus: the host itself counts as focusable when
21
+ * its focusable role element lives in the shadow (checkbox/switch) —
22
+ * Firefox checks the host, not the validation anchor, on a blocked submit. */
23
+ protected static delegatesFocus: boolean;
24
+ /** A wrapping <label> delegates clicks to the host (see watchLabelForwards). */
25
+ protected static labelClicks: boolean;
18
26
  protected props: PropertyManager<T>;
19
27
  protected _internals: ElementInternals;
20
28
  private _validity;
21
29
  protected _connected: boolean;
22
30
  protected _rendered: boolean;
31
+ /** The user edited the field since it was created or its form reset:
32
+ * blur then shows its validation message (like native :user-invalid). */
33
+ protected _userEdited: boolean;
23
34
  constructor();
24
35
  /**
25
36
  * Define which attributes to observe
@@ -55,11 +66,14 @@ export declare abstract class TyComponent<T = any> extends HTMLElement {
55
66
  * Disconnected callback
56
67
  */
57
68
  disconnectedCallback(): void;
69
+ /** The form owner changed (moved, `form` attribute): see ValidityController.track(). */
70
+ formAssociatedCallback(): void;
58
71
  /**
59
- * Capture properties that were set before connectedCallback
60
- * (React, Vue, Reagent set properties before element is connected)
72
+ * upgradeProperties(), plus declared properties without an accessor of
73
+ * their own name (e.g. ty-input's `minlength`): those go straight to
74
+ * setProperty().
61
75
  */
62
- private _capturePreSetProperties;
76
+ private _upgradeProperties;
63
77
  /**
64
78
  * THE CORE LIFECYCLE METHOD
65
79
  * All property changes flow through here
@@ -137,18 +151,6 @@ export declare abstract class TyComponent<T = any> extends HTMLElement {
137
151
  * the role element as aria-label, giving it an accessible name.
138
152
  */
139
153
  protected applyLabelName(roleEl: HTMLElement): void;
140
- private _handlingActivationClick;
141
- /**
142
- * Guard for click-driven toggle handlers (checkbox/switch) on components
143
- * whose whole host is the click target AND that expect a wrapping
144
- * <label> to delegate to them. Some browsers still forward a label's own
145
- * synthetic click to the associated control even when the original click
146
- * already targeted that control directly, firing 'click' twice for one
147
- * user gesture. Both arrive synchronously in the same task, so a flag
148
- * cleared on the next microtask catches only the spurious duplicate —
149
- * two genuinely separate clicks are always in different tasks.
150
- */
151
- protected isDuplicateActivationClick(): boolean;
152
154
  private _resetDefaults;
153
155
  /**
154
156
  * Form reset callback - called when form.reset() is invoked.
@@ -157,6 +159,9 @@ export declare abstract class TyComponent<T = any> extends HTMLElement {
157
159
  * semantics). Non-form state (e.g. a checkbox's submission `value`, a
158
160
  * ty-radio's option `value`) is deliberately left alone — the old
159
161
  * blanket 'value' reset corrupted radio groups by wiping child values.
162
+ * Like a native control the field also looks untouched again: the
163
+ * validation message it showed is gone (an `error` the author set stays)
164
+ * and a blur shows nothing until the user edits it.
160
165
  */
161
166
  formResetCallback(): void;
162
167
  /**
@@ -1,18 +1,47 @@
1
1
  import { PropertyManager } from '../utils/property-manager.js';
2
2
  import { ValidityController } from '../utils/validity.js';
3
+ import { upgradeProperties } from '../utils/upgrade-properties.js';
4
+ export { upgradeProperties };
5
+ const labelClickHosts = new WeakSet();
6
+ const directClicks = new WeakMap();
7
+ let labelForwardsWatched = false;
8
+ function watchLabelForwards() {
9
+ if (labelForwardsWatched)
10
+ return;
11
+ labelForwardsWatched = true;
12
+ window.addEventListener('click', (e) => {
13
+ const path = e.composedPath();
14
+ const host = path.find((n) => labelClickHosts.has(n));
15
+ if (!host)
16
+ return;
17
+ const at = `${e.clientX},${e.clientY}`;
18
+ if (path[0] !== host) {
19
+ directClicks.set(host, at);
20
+ setTimeout(() => { if (directClicks.get(host) === at)
21
+ directClicks.delete(host); });
22
+ }
23
+ else if (directClicks.get(host) === at) {
24
+ e.stopImmediatePropagation();
25
+ }
26
+ }, true);
27
+ }
3
28
  export class TyComponent extends HTMLElement {
4
29
  constructor() {
5
30
  super();
6
31
  this._connected = false;
7
32
  this._rendered = false;
8
- this._handlingActivationClick = false;
33
+ this._userEdited = false;
9
34
  this._resetDefaults = null;
10
35
  const ctor = this.constructor;
11
36
  this.props = new PropertyManager(ctor.properties);
12
37
  this._internals = this.attachInternals();
13
38
  this._validity = new ValidityController(this, this._internals);
14
39
  if (!this.shadowRoot) {
15
- this.attachShadow({ mode: 'open' });
40
+ this.attachShadow({ mode: 'open', delegatesFocus: ctor.delegatesFocus });
41
+ }
42
+ if (ctor.labelClicks) {
43
+ labelClickHosts.add(this);
44
+ watchLabelForwards();
16
45
  }
17
46
  }
18
47
  static get observedAttributes() {
@@ -81,7 +110,6 @@ this._handlePropertyChanges([change]);
81
110
  }
82
111
  }
83
112
  connectedCallback() {
84
- this._connected = true;
85
113
  if (!this._resetDefaults) {
86
114
  this._resetDefaults = new Map();
87
115
  const ctor = this.constructor;
@@ -92,10 +120,10 @@ const attr = this.getAttribute(this._toKebabCase(name));
92
120
  this._resetDefaults.set(name, attr ?? (typeof config.default === 'function' ? config.default() : config.default) ?? null);
93
121
  }
94
122
  }
95
- const preSetProps = this._capturePreSetProperties();
96
- if (preSetProps.length > 0) {
97
- this._handlePropertyChanges(preSetProps);
98
- }
123
+ this._upgradeProperties();
124
+ this._connected = true;
125
+ if (this._validates)
126
+ this._validity.track();
99
127
  this.onConnect();
100
128
  if (!this._rendered) {
101
129
  this.render();
@@ -104,21 +132,28 @@ this._rendered = true;
104
132
  }
105
133
  disconnectedCallback() {
106
134
  this._connected = false;
135
+ if (this._validates)
136
+ this._validity.track();
107
137
  this.onDisconnect();
108
138
  }
109
- _capturePreSetProperties() {
110
- const changes = [];
111
- const ctor = this.constructor;
112
- for (const propName of Object.keys(ctor.properties)) {
113
- const descriptor = Object.getOwnPropertyDescriptor(this, propName);
114
- if (descriptor && descriptor.value !== undefined) {
115
- const change = this.props.updateProperty(propName, descriptor.value, 'property');
116
- if (change) {
117
- changes.push(change);
118
- }
139
+ formAssociatedCallback() {
140
+ if (this._validates)
141
+ this._validity.track();
119
142
  }
143
+ _upgradeProperties() {
144
+ for (const name of Object.keys(this.constructor.properties)) {
145
+ if (!Object.prototype.hasOwnProperty.call(this, name))
146
+ continue;
147
+ const value = this[name];
148
+ delete this[name];
149
+ if (value === undefined)
150
+ continue;
151
+ if (name in this)
152
+ this[name] = value;
153
+ else
154
+ this.setProperty(name, value);
120
155
  }
121
- return changes;
156
+ upgradeProperties(this);
122
157
  }
123
158
  _handlePropertyChanges(changes) {
124
159
  if (changes.length === 0)
@@ -212,14 +247,10 @@ roleEl.setAttribute('aria-label', text);
212
247
  else
213
248
  roleEl.removeAttribute('aria-label');
214
249
  }
215
- isDuplicateActivationClick() {
216
- if (this._handlingActivationClick)
217
- return true;
218
- this._handlingActivationClick = true;
219
- queueMicrotask(() => { this._handlingActivationClick = false; });
220
- return false;
221
- }
222
250
  formResetCallback() {
251
+ this._userEdited = false;
252
+ if (this._validates)
253
+ this._validity.reset();
223
254
  for (const [name, def] of this._resetDefaults ?? []) {
224
255
  this.setProperty(name, def);
225
256
  }
@@ -230,3 +261,5 @@ onFormReset() {
230
261
  }
231
262
  TyComponent.formAssociated = true;
232
263
  TyComponent.properties = {};
264
+ TyComponent.delegatesFocus = false;
265
+ TyComponent.labelClicks = false;
@@ -89,11 +89,25 @@ export declare class TyButton extends TyComponent<ButtonState> implements TyButt
89
89
  };
90
90
  };
91
91
  private _structureInitialized;
92
+ private _formDisabled;
92
93
  private _customFlavorSheet;
94
+ private _ownClick;
93
95
  constructor();
96
+ /**
97
+ * A real press inside the host's content box that missed the button: the
98
+ * empty host below a wide button that a min-height made taller, or around
99
+ * one a page's display: flex stretched. Nothing there looks like the
100
+ * button, so it's no press. Host padding and border are outside the
101
+ * content box.
102
+ */
103
+ private _pressOffButton;
94
104
  protected onConnect(): void;
95
105
  protected onDisconnect(): void;
96
106
  protected onPropertiesChanged(changes: PropertyChange[]): void;
107
+ formDisabledCallback(disabled: boolean): void;
108
+ private get _isDisabled();
109
+ /** Focus the inner <button>. The host itself isn't focusable. */
110
+ focus(options?: FocusOptions): void;
97
111
  /**
98
112
  * Custom flavors: adopt generated per-flavor rules pointed at the user's
99
113
  * design tokens (--ty-solid-X / --ty-color-X / --ty-bg-X), falling back to
@@ -4,12 +4,37 @@ import { ensureStyles, buildClassList } from "../utils/styles.js";
4
4
  import { buttonStyles, buttonCustomFlavorCss } from "../styles/button.js";
5
5
  import { syncCustomFlavorSheet } from "../utils/flavor-sheet.js";
6
6
  import { getLoaderSvg } from "../utils/loader-registry.js";
7
+ import { asSubmitAttempt } from "../utils/validity.js";
7
8
  export class TyButton extends TyComponent {
8
9
  constructor() {
9
10
  super();
10
11
  this._structureInitialized = false;
12
+ this._formDisabled = false;
11
13
  this._customFlavorSheet = null;
14
+ this._ownClick = null;
12
15
  ensureStyles(this.shadowRoot, { css: buttonStyles, id: "ty-button" });
16
+ this.addEventListener("click", (e) => {
17
+ const button = this.shadowRoot.querySelector("button");
18
+ if (!button || e === this._ownClick)
19
+ return;
20
+ if (e.composedPath().includes(button))
21
+ return;
22
+ e.stopImmediatePropagation();
23
+ if (!this._pressOffButton(e))
24
+ button.click();
25
+ }, true);
26
+ }
27
+ _pressOffButton(e) {
28
+ if (!e.isTrusted || !(e instanceof MouseEvent))
29
+ return false;
30
+ const r = this.getBoundingClientRect();
31
+ const s = getComputedStyle(this);
32
+ const px = (p) => parseFloat(s.getPropertyValue(p)) || 0;
33
+ const left = r.left + px("border-left-width") + px("padding-left");
34
+ const right = r.right - px("border-right-width") - px("padding-right");
35
+ const top = r.top + px("border-top-width") + px("padding-top");
36
+ const bottom = r.bottom - px("border-bottom-width") - px("padding-bottom");
37
+ return e.clientX >= left && e.clientX < right && e.clientY >= top && e.clientY < bottom;
13
38
  }
14
39
  onConnect() {
15
40
  this._syncCustomFlavor();
@@ -20,6 +45,21 @@ if (changes.some((c) => c.name === "flavor")) {
20
45
  this._syncCustomFlavor();
21
46
  }
22
47
  }
48
+ formDisabledCallback(disabled) {
49
+ this._formDisabled = disabled;
50
+ if (this._structureInitialized)
51
+ this.render();
52
+ }
53
+ get _isDisabled() {
54
+ return this.disabled || this._formDisabled;
55
+ }
56
+ focus(options) {
57
+ const button = this.shadowRoot?.querySelector("button");
58
+ if (button)
59
+ button.focus(options);
60
+ else
61
+ super.focus(options);
62
+ }
23
63
  _syncCustomFlavor() {
24
64
  const { base } = this.parseFlavor();
25
65
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, base, () => buttonCustomFlavorCss(base));
@@ -118,7 +158,7 @@ if (this.name && this.value) {
118
158
  this._internals.setFormValue(this.value);
119
159
  }
120
160
  try {
121
- form.requestSubmit();
161
+ asSubmitAttempt(() => form.requestSubmit());
122
162
  }
123
163
  finally {
124
164
  if (this.name && this.value)
@@ -136,7 +176,7 @@ initializeButtonStructure() {
136
176
  const shadow = this.shadowRoot;
137
177
  const classes = this.buildClasses();
138
178
  const button = document.createElement("button");
139
- button.disabled = this.disabled;
179
+ button.disabled = this._isDisabled;
140
180
  button.className = classes;
141
181
  button.setAttribute("part", "button");
142
182
  const loader = document.createElement("span");
@@ -155,15 +195,21 @@ button.appendChild(startSlot);
155
195
  button.appendChild(defaultSlot);
156
196
  button.appendChild(endSlot);
157
197
  button.addEventListener("click", (e) => {
158
- if (this.disabled || this.loading)
198
+ const path = e.composedPath();
199
+ if (path.slice(0, path.indexOf(button)).some((n) => n instanceof Element && n.localName === "ty-popup"))
159
200
  return;
160
201
  e.stopPropagation();
161
- this.handleFormAction();
162
- this.dispatchEvent(new CustomEvent("click", {
202
+ if (this._isDisabled || this.loading)
203
+ return;
204
+ const click = new CustomEvent("click", {
163
205
  bubbles: true,
164
206
  composed: true,
207
+ cancelable: true,
165
208
  detail: { originalEvent: e },
166
- }));
209
+ });
210
+ this._ownClick = click;
211
+ if (this.dispatchEvent(click))
212
+ this.handleFormAction();
167
213
  });
168
214
  this.applyLoadingState(button);
169
215
  shadow.appendChild(button);
@@ -174,12 +220,14 @@ const isLoading = this.loading;
174
220
  button.classList.toggle("loading", isLoading);
175
221
  if (isLoading) {
176
222
  button.setAttribute("aria-busy", "true");
223
+ button.setAttribute("aria-disabled", "true");
177
224
  const loader = button.querySelector(".loader-icon");
178
225
  if (loader)
179
226
  loader.innerHTML = getLoaderSvg();
180
227
  }
181
228
  else {
182
229
  button.removeAttribute("aria-busy");
230
+ button.removeAttribute("aria-disabled");
183
231
  }
184
232
  }
185
233
  render() {
@@ -191,7 +239,7 @@ return;
191
239
  const classes = this.buildClasses();
192
240
  const button = shadow.querySelector("button");
193
241
  if (button) {
194
- button.disabled = this.disabled;
242
+ button.disabled = this._isDisabled;
195
243
  button.className = classes;
196
244
  this.applyLoadingState(button);
197
245
  }
@@ -32,7 +32,7 @@ export interface DayClickDetail {
32
32
  export declare class TyCalendarMonth extends HTMLElement {
33
33
  private _displayYear;
34
34
  private _displayMonth;
35
- private _locale;
35
+ private _locale?;
36
36
  private _size;
37
37
  private _width?;
38
38
  private _minWidth?;
@@ -97,6 +97,12 @@ export declare class TyCalendarMonth extends HTMLElement {
97
97
  * Useful when external data changes (e.g., async event loading)
98
98
  */
99
99
  refresh(): void;
100
+ /**
101
+ * A day was picked (click, Enter, Space): it becomes the grid's tab stop and
102
+ * takes the focus, which render() keeps on it when the pick re-renders the
103
+ * grid. Then the events go out.
104
+ */
105
+ private pickDay;
100
106
  private dispatchDayClick;
101
107
  /**
102
108
  * WAI-ARIA grid pattern: gridcell + roving tabindex (exactly one cell in
@@ -1,7 +1,8 @@
1
1
  import { ensureStyles } from '../utils/styles.js';
2
+ import { liftProperties, upgradeProperties } from '../utils/upgrade-properties.js';
2
3
  import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
3
4
  import { calendarMonthStyles, calendarMonthCustomFlavorCss } from '../styles/calendar-month.js';
4
- import { getCalendarMonthDays, getLocalizedWeekdays, parseISODate } from '../utils/calendar-utils.js';
5
+ import { getCalendarMonthDays, getLocalizedWeekdays, localMidnight, parseDateDay, utcMidnight } from '../utils/calendar-utils.js';
5
6
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
6
7
  function defaultDayContent(dayContext) {
7
8
  return dayContext.dayInMonth.toString();
@@ -24,10 +25,10 @@ return value instanceof HTMLElement;
24
25
  function isoToUTCTimestamp(iso) {
25
26
  if (!iso)
26
27
  return null;
27
- const parsed = parseISODate(iso);
28
+ const parsed = parseDateDay(iso);
28
29
  if (!parsed)
29
30
  return null;
30
- return Date.UTC(parsed.year, parsed.month - 1, parsed.day);
31
+ return utcMidnight(parsed.year, parsed.month, parsed.day);
31
32
  }
32
33
  function normalizeSizeValue(value) {
33
34
  if (!value || value === '')
@@ -44,7 +45,6 @@ return ['display-year', 'display-month', 'locale', 'size', 'min', 'max', 'flavor
44
45
  }
45
46
  constructor() {
46
47
  super();
47
- this._locale = 'en-US';
48
48
  this._size = 'md';
49
49
  this._customFlavorSheet = null;
50
50
  this._focusedIndex = null;
@@ -53,6 +53,7 @@ const today = new Date();
53
53
  this._displayYear = today.getFullYear();
54
54
  this._displayMonth = today.getMonth() + 1;
55
55
  this.attachShadow({ mode: 'open' });
56
+ liftProperties(this);
56
57
  }
57
58
  connectedCallback() {
58
59
  if (!this._displayMonth) {
@@ -62,23 +63,15 @@ if (!this._displayYear) {
62
63
  this._displayYear = new Date().getFullYear();
63
64
  }
64
65
  if (!this._locale) {
65
- this._locale = this.getAttribute('locale') || 'en-US';
66
+ this._locale = this.getAttribute('locale') || undefined;
66
67
  }
67
68
  const sizeAttr = this.getAttribute('size');
68
69
  if (sizeAttr && (sizeAttr === 'sm' || sizeAttr === 'md' || sizeAttr === 'lg')) {
69
70
  this._size = sizeAttr;
70
71
  }
71
- const plainDayContentFn = this.dayContentFn;
72
- if (plainDayContentFn && !this._dayContentFn) {
73
- this._dayContentFn = plainDayContentFn;
74
- delete this.dayContentFn;
75
- }
76
- const plainCustomCSS = this.customCSS;
77
- if (plainCustomCSS && !this._customCSS) {
78
- this._customCSS = plainCustomCSS;
79
- delete this.customCSS;
80
- }
72
+ upgradeProperties(this);
81
73
  this._localeObserver = observeLocaleChanges(this, () => {
74
+ if (!this._locale)
82
75
  this.render();
83
76
  });
84
77
  this._syncCustomFlavor();
@@ -102,7 +95,7 @@ this.displayMonth = n;
102
95
  }
103
96
  }
104
97
  else if (name === 'locale') {
105
- this._locale = newValue || 'en-US';
98
+ this._locale = newValue || undefined;
106
99
  this.render();
107
100
  }
108
101
  else if (name === 'size') {
@@ -143,11 +136,11 @@ this.render();
143
136
  }
144
137
  }
145
138
  get locale() {
146
- return getEffectiveLocale(this, this.getAttribute('locale'));
139
+ return getEffectiveLocale(this, this._locale);
147
140
  }
148
141
  set locale(value) {
149
142
  if (this._locale !== value) {
150
- this._locale = value;
143
+ this._locale = value || undefined;
151
144
  this.render();
152
145
  }
153
146
  }
@@ -239,6 +232,7 @@ set value(timestamp) {
239
232
  const newValue = timestamp ?? undefined;
240
233
  if (this._value !== newValue) {
241
234
  this._value = newValue;
235
+ this._focusedIndex = null;
242
236
  this.render();
243
237
  }
244
238
  }
@@ -276,6 +270,15 @@ this.shadowRoot.adoptedStyleSheets = [...existing, this._customCSS];
276
270
  refresh() {
277
271
  this.render();
278
272
  }
273
+ pickDay(cell, dayContext, domEvent) {
274
+ const index = this._cellElements.indexOf(cell);
275
+ if (index !== -1) {
276
+ this._cellElements.forEach((c) => c.setAttribute('tabindex', c === cell ? '0' : '-1'));
277
+ this._focusedIndex = index;
278
+ cell.focus({ preventScroll: true });
279
+ }
280
+ this.dispatchDayClick(dayContext, domEvent);
281
+ }
279
282
  dispatchDayClick(dayContext, _domEvent) {
280
283
  const detail = {
281
284
  dayContext,
@@ -297,7 +300,7 @@ cancelable: true,
297
300
  this.dispatchEvent(event);
298
301
  }
299
302
  }
300
- renderDayCell(dayContext, disabled, isRoving) {
303
+ renderDayCell(dayContext, disabled, isRoving, label) {
301
304
  const dayElement = document.createElement('div');
302
305
  const content = this._dayContentFn
303
306
  ? this._dayContentFn(dayContext)
@@ -319,8 +322,7 @@ else if (isDOMElement(content)) {
319
322
  dayElement.appendChild(content);
320
323
  }
321
324
  dayElement.setAttribute('role', 'gridcell');
322
- dayElement.setAttribute('aria-label', new Intl.DateTimeFormat(this._locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })
323
- .format(new Date(dayContext.localValue)));
325
+ dayElement.setAttribute('aria-label', label.format(new Date(dayContext.localValue)));
324
326
  dayElement.setAttribute('aria-selected', String(!!dayContext.isSelected));
325
327
  if (dayContext.today)
326
328
  dayElement.setAttribute('aria-current', 'date');
@@ -331,13 +333,13 @@ dayElement.setAttribute('tabindex', '-1');
331
333
  else {
332
334
  dayElement.setAttribute('tabindex', isRoving ? '0' : '-1');
333
335
  dayElement.addEventListener('click', (event) => {
334
- this.dispatchDayClick(dayContext, event);
336
+ this.pickDay(dayElement, dayContext, event);
335
337
  });
336
338
  dayElement.addEventListener('keydown', (event) => {
337
339
  const ke = event;
338
340
  if (ke.key === 'Enter' || ke.key === ' ') {
339
341
  ke.preventDefault();
340
- this.dispatchDayClick(dayContext, event);
342
+ this.pickDay(dayElement, dayContext, event);
341
343
  }
342
344
  });
343
345
  }
@@ -351,7 +353,7 @@ ensureStyles(root, { css: calendarMonthStyles, id: 'ty-calendar-month' });
351
353
  this.applyCustomCSS();
352
354
  this.applyWidthProperties();
353
355
  let selection;
354
- if (this._value) {
356
+ if (this._value !== undefined) {
355
357
  const date = new Date(this._value);
356
358
  selection = {
357
359
  year: date.getFullYear(),
@@ -360,13 +362,16 @@ day: date.getDate()
360
362
  };
361
363
  }
362
364
  const days = getCalendarMonthDays(this._displayYear, this._displayMonth, selection);
363
- const weekdays = getLocalizedWeekdays(this._locale, 'short');
365
+ const locale = this.locale;
366
+ const weekdays = getLocalizedWeekdays(locale, 'short');
367
+ const dayLabel = new Intl.DateTimeFormat(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
368
+ const hadFocus = !!root.activeElement;
364
369
  root.innerHTML = '';
365
370
  const calendarContainer = document.createElement('div');
366
371
  calendarContainer.className = `calendar-flex-container calendar-size-${this._size}`;
367
372
  calendarContainer.setAttribute('role', 'grid');
368
- calendarContainer.setAttribute('aria-label', new Intl.DateTimeFormat(this._locale, { month: 'long', year: 'numeric' })
369
- .format(new Date(this._displayYear, this._displayMonth - 1, 1)));
373
+ calendarContainer.setAttribute('aria-label', new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' })
374
+ .format(localMidnight(this._displayYear, this._displayMonth, 1)));
370
375
  const headerRow = document.createElement('div');
371
376
  headerRow.className = 'calendar-row calendar-header-row';
372
377
  headerRow.setAttribute('role', 'row');
@@ -421,7 +426,7 @@ dayRow.setAttribute('role', 'row');
421
426
  week.forEach(dayContext => {
422
427
  const disabled = (minTs !== null && dayContext.value < minTs) ||
423
428
  (maxTs !== null && dayContext.value > maxTs);
424
- const dayCell = this.renderDayCell(dayContext, disabled, flatIndex === effectiveFocusedIndex);
429
+ const dayCell = this.renderDayCell(dayContext, disabled, flatIndex === effectiveFocusedIndex, dayLabel);
425
430
  dayCell.className = `${dayCell.className} calendar-cell calendar-day-cell`;
426
431
  dayRow.appendChild(dayCell);
427
432
  this._cellElements[flatIndex] = dayCell;
@@ -452,6 +457,8 @@ nextCell.focus();
452
457
  }
453
458
  });
454
459
  root.appendChild(calendarContainer);
460
+ if (hadFocus)
461
+ this._cellElements[effectiveFocusedIndex]?.focus({ preventScroll: true });
455
462
  }
456
463
  }
457
464
  if (!customElements.get('ty-calendar-month')) {
@@ -1,6 +1,6 @@
1
1
  import { ensureStyles } from '../utils/styles.js';
2
2
  import { calendarNavigationStyles } from '../styles/calendar-navigation.js';
3
- import { getMonthName, parseISODate } from '../utils/calendar-utils.js';
3
+ import { getMonthName, parseDateDay } from '../utils/calendar-utils.js';
4
4
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
5
5
  import { TyComponent } from '../base/ty-component.js';
6
6
  const CHEVRON_LEFT_SVG = `<?xml version='1.0' encoding='UTF-8'?>
@@ -31,6 +31,7 @@ onConnect() {
31
31
  this._state.displayMonth = this.displayMonth;
32
32
  this._state.displayYear = this.displayYear;
33
33
  this._localeObserver = observeLocaleChanges(this, () => {
34
+ if (!this.getProperty('locale'))
34
35
  this.render();
35
36
  });
36
37
  this.render();
@@ -122,8 +123,8 @@ static monthIndex(year, month) {
122
123
  return year * 12 + (month - 1);
123
124
  }
124
125
  boundIndices() {
125
- const min = parseISODate(this.min);
126
- const max = parseISODate(this.max);
126
+ const min = parseDateDay(this.min);
127
+ const max = parseDateDay(this.max);
127
128
  return {
128
129
  minIdx: min ? TyCalendarNavigation.monthIndex(min.year, min.month) : null,
129
130
  maxIdx: max ? TyCalendarNavigation.monthIndex(max.year, max.month) : null,
@@ -175,6 +176,12 @@ this.style.removeProperty('--nav-width');
175
176
  }
176
177
  const months = this.view === 'months';
177
178
  const monthName = getMonthName(this._state.displayMonth, this.locale, 'long');
179
+ const had = root.activeElement;
180
+ const place = !had ? null
181
+ : had.classList.contains('nav-title') ? 'title'
182
+ : had.className.includes('-prev') ? 'prev'
183
+ : had.className.includes('-next') ? 'next'
184
+ : null;
178
185
  root.innerHTML = '';
179
186
  const header = document.createElement('div');
180
187
  header.className = 'calendar-navigation-header';
@@ -206,11 +213,17 @@ const step = months ? 12 : 1;
206
213
  const unit = months ? 'year' : 'month';
207
214
  const arrows = document.createElement('div');
208
215
  arrows.className = 'nav-group nav-arrows';
209
- arrows.appendChild(this.createButton(`nav-btn nav-${unit}-prev`, `Previous ${unit}`, CHEVRON_LEFT_SVG, () => this.navigateBy(-step), this.isNavDisabled(-step)));
210
- arrows.appendChild(this.createButton(`nav-btn nav-${unit}-next`, `Next ${unit}`, CHEVRON_RIGHT_SVG, () => this.navigateBy(step), this.isNavDisabled(step)));
216
+ const prev = this.createButton(`nav-btn nav-${unit}-prev`, `Previous ${unit}`, CHEVRON_LEFT_SVG, () => this.navigateBy(-step), this.isNavDisabled(-step));
217
+ const next = this.createButton(`nav-btn nav-${unit}-next`, `Next ${unit}`, CHEVRON_RIGHT_SVG, () => this.navigateBy(step), this.isNavDisabled(step));
218
+ arrows.appendChild(prev);
219
+ arrows.appendChild(next);
211
220
  header.appendChild(title);
212
221
  header.appendChild(arrows);
213
222
  root.appendChild(header);
223
+ if (place) {
224
+ const successor = place === 'prev' ? prev : place === 'next' ? next : title;
225
+ (successor.disabled ? title : successor).focus({ preventScroll: true });
226
+ }
214
227
  }
215
228
  }
216
229
  TyCalendarNavigation.properties = {
@@ -251,7 +264,7 @@ return num;
251
264
  locale: {
252
265
  type: 'string',
253
266
  visual: true,
254
- default: 'en-US'
267
+ default: ''
255
268
  },
256
269
  size: {
257
270
  type: 'string',