tyrell-components 1.0.0-TC58 → 1.0.0-TC59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/css/tyrell-colors-static.css +14 -0
  2. package/css/tyrell-theme.css +1 -0
  3. package/css/tyrell.css +8 -6
  4. package/dist/tyrell-colors-static.css +1 -1
  5. package/dist/tyrell-theme.css +1 -1
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.d.ts +8 -12
  8. package/lib/base/ty-component.js +38 -9
  9. package/lib/components/button.d.ts +10 -0
  10. package/lib/components/button.js +22 -1
  11. package/lib/components/calendar-month.d.ts +7 -1
  12. package/lib/components/calendar-month.js +32 -18
  13. package/lib/components/calendar-navigation.js +19 -6
  14. package/lib/components/calendar.d.ts +24 -5
  15. package/lib/components/calendar.js +83 -37
  16. package/lib/components/checkbox.d.ts +5 -0
  17. package/lib/components/checkbox.js +18 -5
  18. package/lib/components/copy.d.ts +2 -0
  19. package/lib/components/copy.js +15 -31
  20. package/lib/components/date-picker.d.ts +7 -26
  21. package/lib/components/date-picker.js +14 -84
  22. package/lib/components/file-upload.js +6 -2
  23. package/lib/components/input.d.ts +0 -9
  24. package/lib/components/input.js +17 -40
  25. package/lib/components/modal.js +59 -18
  26. package/lib/components/popup.js +56 -5
  27. package/lib/components/radio.d.ts +6 -0
  28. package/lib/components/radio.js +12 -28
  29. package/lib/components/select.d.ts +29 -11
  30. package/lib/components/select.js +43 -57
  31. package/lib/components/switch.d.ts +4 -0
  32. package/lib/components/switch.js +18 -5
  33. package/lib/components/tabs.js +1 -1
  34. package/lib/components/textarea.d.ts +1 -0
  35. package/lib/components/textarea.js +21 -8
  36. package/lib/index.d.ts +1 -0
  37. package/lib/index.js +1 -0
  38. package/lib/styles/button.js +1 -0
  39. package/lib/styles/calendar-month.js +1 -1
  40. package/lib/styles/calendar.js +3 -0
  41. package/lib/styles/checkbox.js +11 -5
  42. package/lib/styles/date-picker.js +5 -10
  43. package/lib/styles/input.js +5 -9
  44. package/lib/styles/option.d.ts +1 -1
  45. package/lib/styles/option.js +4 -0
  46. package/lib/styles/popup.d.ts +1 -1
  47. package/lib/styles/popup.js +2 -0
  48. package/lib/styles/radio.js +10 -4
  49. package/lib/styles/select-base.js +5 -10
  50. package/lib/styles/select.d.ts +1 -1
  51. package/lib/styles/select.js +4 -0
  52. package/lib/styles/switch.js +10 -5
  53. package/lib/styles/tag.js +2 -1
  54. package/lib/utils/calendar-utils.d.ts +49 -0
  55. package/lib/utils/calendar-utils.js +63 -6
  56. package/lib/utils/scroll-lock.d.ts +10 -4
  57. package/lib/utils/scroll-lock.js +102 -2
  58. package/lib/utils/validity.d.ts +27 -1
  59. package/lib/utils/validity.js +68 -16
  60. package/lib/version.d.ts +1 -1
  61. package/lib/version.js +1 -1
  62. package/package.json +1 -1
  63. package/dist/tyrell.js.map +0 -1
  64. package/lib/utils/error-flavor.d.ts +0 -8
  65. package/lib/utils/error-flavor.js +0 -1
@@ -17,6 +17,12 @@ export { upgradeProperties };
17
17
  export declare abstract class TyComponent<T = any> extends HTMLElement {
18
18
  static formAssociated: boolean;
19
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;
20
26
  protected props: PropertyManager<T>;
21
27
  protected _internals: ElementInternals;
22
28
  private _validity;
@@ -60,6 +66,8 @@ export declare abstract class TyComponent<T = any> extends HTMLElement {
60
66
  * Disconnected callback
61
67
  */
62
68
  disconnectedCallback(): void;
69
+ /** The form owner changed (moved, `form` attribute): see ValidityController.track(). */
70
+ formAssociatedCallback(): void;
63
71
  /**
64
72
  * upgradeProperties(), plus declared properties without an accessor of
65
73
  * their own name (e.g. ty-input's `minlength`): those go straight to
@@ -143,18 +151,6 @@ export declare abstract class TyComponent<T = any> extends HTMLElement {
143
151
  * the role element as aria-label, giving it an accessible name.
144
152
  */
145
153
  protected applyLabelName(roleEl: HTMLElement): void;
146
- private _handlingActivationClick;
147
- /**
148
- * Guard for click-driven toggle handlers (checkbox/switch) on components
149
- * whose whole host is the click target AND that expect a wrapping
150
- * <label> to delegate to them. Some browsers still forward a label's own
151
- * synthetic click to the associated control even when the original click
152
- * already targeted that control directly, firing 'click' twice for one
153
- * user gesture. Both arrive synchronously in the same task, so a flag
154
- * cleared on the next microtask catches only the spurious duplicate —
155
- * two genuinely separate clicks are always in different tasks.
156
- */
157
- protected isDuplicateActivationClick(): boolean;
158
154
  private _resetDefaults;
159
155
  /**
160
156
  * Form reset callback - called when form.reset() is invoked.
@@ -2,20 +2,46 @@ import { PropertyManager } from '../utils/property-manager.js';
2
2
  import { ValidityController } from '../utils/validity.js';
3
3
  import { upgradeProperties } from '../utils/upgrade-properties.js';
4
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
+ }
5
28
  export class TyComponent extends HTMLElement {
6
29
  constructor() {
7
30
  super();
8
31
  this._connected = false;
9
32
  this._rendered = false;
10
33
  this._userEdited = false;
11
- this._handlingActivationClick = false;
12
34
  this._resetDefaults = null;
13
35
  const ctor = this.constructor;
14
36
  this.props = new PropertyManager(ctor.properties);
15
37
  this._internals = this.attachInternals();
16
38
  this._validity = new ValidityController(this, this._internals);
17
39
  if (!this.shadowRoot) {
18
- this.attachShadow({ mode: 'open' });
40
+ this.attachShadow({ mode: 'open', delegatesFocus: ctor.delegatesFocus });
41
+ }
42
+ if (ctor.labelClicks) {
43
+ labelClickHosts.add(this);
44
+ watchLabelForwards();
19
45
  }
20
46
  }
21
47
  static get observedAttributes() {
@@ -96,6 +122,8 @@ this._resetDefaults.set(name, attr ?? (typeof config.default === 'function' ? co
96
122
  }
97
123
  this._upgradeProperties();
98
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,8 +132,14 @@ 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
  }
139
+ formAssociatedCallback() {
140
+ if (this._validates)
141
+ this._validity.track();
142
+ }
109
143
  _upgradeProperties() {
110
144
  for (const name of Object.keys(this.constructor.properties)) {
111
145
  if (!Object.prototype.hasOwnProperty.call(this, name))
@@ -213,13 +247,6 @@ roleEl.setAttribute('aria-label', text);
213
247
  else
214
248
  roleEl.removeAttribute('aria-label');
215
249
  }
216
- isDuplicateActivationClick() {
217
- if (this._handlingActivationClick)
218
- return true;
219
- this._handlingActivationClick = true;
220
- queueMicrotask(() => { this._handlingActivationClick = false; });
221
- return false;
222
- }
223
250
  formResetCallback() {
224
251
  this._userEdited = false;
225
252
  if (this._validates)
@@ -234,3 +261,5 @@ onFormReset() {
234
261
  }
235
262
  TyComponent.formAssociated = true;
236
263
  TyComponent.properties = {};
264
+ TyComponent.delegatesFocus = false;
265
+ TyComponent.labelClicks = false;
@@ -93,11 +93,21 @@ export declare class TyButton extends TyComponent<ButtonState> implements TyButt
93
93
  private _customFlavorSheet;
94
94
  private _ownClick;
95
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;
96
104
  protected onConnect(): void;
97
105
  protected onDisconnect(): void;
98
106
  protected onPropertiesChanged(changes: PropertyChange[]): void;
99
107
  formDisabledCallback(disabled: boolean): void;
100
108
  private get _isDisabled();
109
+ /** Focus the inner <button>. The host itself isn't focusable. */
110
+ focus(options?: FocusOptions): void;
101
111
  /**
102
112
  * Custom flavors: adopt generated per-flavor rules pointed at the user's
103
113
  * design tokens (--ty-solid-X / --ty-color-X / --ty-bg-X), falling back to
@@ -4,6 +4,7 @@ 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();
@@ -19,9 +20,22 @@ return;
19
20
  if (e.composedPath().includes(button))
20
21
  return;
21
22
  e.stopImmediatePropagation();
23
+ if (!this._pressOffButton(e))
22
24
  button.click();
23
25
  }, true);
24
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;
38
+ }
25
39
  onConnect() {
26
40
  this._syncCustomFlavor();
27
41
  }
@@ -39,6 +53,13 @@ this.render();
39
53
  get _isDisabled() {
40
54
  return this.disabled || this._formDisabled;
41
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
+ }
42
63
  _syncCustomFlavor() {
43
64
  const { base } = this.parseFlavor();
44
65
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, base, () => buttonCustomFlavorCss(base));
@@ -137,7 +158,7 @@ if (this.name && this.value) {
137
158
  this._internals.setFormValue(this.value);
138
159
  }
139
160
  try {
140
- form.requestSubmit();
161
+ asSubmitAttempt(() => form.requestSubmit());
141
162
  }
142
163
  finally {
143
164
  if (this.name && this.value)
@@ -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
@@ -2,7 +2,7 @@ import { ensureStyles } from '../utils/styles.js';
2
2
  import { liftProperties, upgradeProperties } from '../utils/upgrade-properties.js';
3
3
  import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
4
4
  import { calendarMonthStyles, calendarMonthCustomFlavorCss } from '../styles/calendar-month.js';
5
- import { getCalendarMonthDays, getLocalizedWeekdays, parseISODate } from '../utils/calendar-utils.js';
5
+ import { getCalendarMonthDays, getLocalizedWeekdays, localMidnight, parseDateDay, utcMidnight } from '../utils/calendar-utils.js';
6
6
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
7
7
  function defaultDayContent(dayContext) {
8
8
  return dayContext.dayInMonth.toString();
@@ -25,10 +25,10 @@ return value instanceof HTMLElement;
25
25
  function isoToUTCTimestamp(iso) {
26
26
  if (!iso)
27
27
  return null;
28
- const parsed = parseISODate(iso);
28
+ const parsed = parseDateDay(iso);
29
29
  if (!parsed)
30
30
  return null;
31
- return Date.UTC(parsed.year, parsed.month - 1, parsed.day);
31
+ return utcMidnight(parsed.year, parsed.month, parsed.day);
32
32
  }
33
33
  function normalizeSizeValue(value) {
34
34
  if (!value || value === '')
@@ -45,7 +45,6 @@ return ['display-year', 'display-month', 'locale', 'size', 'min', 'max', 'flavor
45
45
  }
46
46
  constructor() {
47
47
  super();
48
- this._locale = 'en-US';
49
48
  this._size = 'md';
50
49
  this._customFlavorSheet = null;
51
50
  this._focusedIndex = null;
@@ -64,7 +63,7 @@ if (!this._displayYear) {
64
63
  this._displayYear = new Date().getFullYear();
65
64
  }
66
65
  if (!this._locale) {
67
- this._locale = this.getAttribute('locale') || 'en-US';
66
+ this._locale = this.getAttribute('locale') || undefined;
68
67
  }
69
68
  const sizeAttr = this.getAttribute('size');
70
69
  if (sizeAttr && (sizeAttr === 'sm' || sizeAttr === 'md' || sizeAttr === 'lg')) {
@@ -72,6 +71,7 @@ this._size = sizeAttr;
72
71
  }
73
72
  upgradeProperties(this);
74
73
  this._localeObserver = observeLocaleChanges(this, () => {
74
+ if (!this._locale)
75
75
  this.render();
76
76
  });
77
77
  this._syncCustomFlavor();
@@ -95,7 +95,7 @@ this.displayMonth = n;
95
95
  }
96
96
  }
97
97
  else if (name === 'locale') {
98
- this._locale = newValue || 'en-US';
98
+ this._locale = newValue || undefined;
99
99
  this.render();
100
100
  }
101
101
  else if (name === 'size') {
@@ -136,11 +136,11 @@ this.render();
136
136
  }
137
137
  }
138
138
  get locale() {
139
- return getEffectiveLocale(this, this.getAttribute('locale'));
139
+ return getEffectiveLocale(this, this._locale);
140
140
  }
141
141
  set locale(value) {
142
142
  if (this._locale !== value) {
143
- this._locale = value;
143
+ this._locale = value || undefined;
144
144
  this.render();
145
145
  }
146
146
  }
@@ -232,6 +232,7 @@ set value(timestamp) {
232
232
  const newValue = timestamp ?? undefined;
233
233
  if (this._value !== newValue) {
234
234
  this._value = newValue;
235
+ this._focusedIndex = null;
235
236
  this.render();
236
237
  }
237
238
  }
@@ -269,6 +270,15 @@ this.shadowRoot.adoptedStyleSheets = [...existing, this._customCSS];
269
270
  refresh() {
270
271
  this.render();
271
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
+ }
272
282
  dispatchDayClick(dayContext, _domEvent) {
273
283
  const detail = {
274
284
  dayContext,
@@ -290,7 +300,7 @@ cancelable: true,
290
300
  this.dispatchEvent(event);
291
301
  }
292
302
  }
293
- renderDayCell(dayContext, disabled, isRoving) {
303
+ renderDayCell(dayContext, disabled, isRoving, label) {
294
304
  const dayElement = document.createElement('div');
295
305
  const content = this._dayContentFn
296
306
  ? this._dayContentFn(dayContext)
@@ -312,8 +322,7 @@ else if (isDOMElement(content)) {
312
322
  dayElement.appendChild(content);
313
323
  }
314
324
  dayElement.setAttribute('role', 'gridcell');
315
- dayElement.setAttribute('aria-label', new Intl.DateTimeFormat(this._locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })
316
- .format(new Date(dayContext.localValue)));
325
+ dayElement.setAttribute('aria-label', label.format(new Date(dayContext.localValue)));
317
326
  dayElement.setAttribute('aria-selected', String(!!dayContext.isSelected));
318
327
  if (dayContext.today)
319
328
  dayElement.setAttribute('aria-current', 'date');
@@ -324,13 +333,13 @@ dayElement.setAttribute('tabindex', '-1');
324
333
  else {
325
334
  dayElement.setAttribute('tabindex', isRoving ? '0' : '-1');
326
335
  dayElement.addEventListener('click', (event) => {
327
- this.dispatchDayClick(dayContext, event);
336
+ this.pickDay(dayElement, dayContext, event);
328
337
  });
329
338
  dayElement.addEventListener('keydown', (event) => {
330
339
  const ke = event;
331
340
  if (ke.key === 'Enter' || ke.key === ' ') {
332
341
  ke.preventDefault();
333
- this.dispatchDayClick(dayContext, event);
342
+ this.pickDay(dayElement, dayContext, event);
334
343
  }
335
344
  });
336
345
  }
@@ -344,7 +353,7 @@ ensureStyles(root, { css: calendarMonthStyles, id: 'ty-calendar-month' });
344
353
  this.applyCustomCSS();
345
354
  this.applyWidthProperties();
346
355
  let selection;
347
- if (this._value) {
356
+ if (this._value !== undefined) {
348
357
  const date = new Date(this._value);
349
358
  selection = {
350
359
  year: date.getFullYear(),
@@ -353,13 +362,16 @@ day: date.getDate()
353
362
  };
354
363
  }
355
364
  const days = getCalendarMonthDays(this._displayYear, this._displayMonth, selection);
356
- 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;
357
369
  root.innerHTML = '';
358
370
  const calendarContainer = document.createElement('div');
359
371
  calendarContainer.className = `calendar-flex-container calendar-size-${this._size}`;
360
372
  calendarContainer.setAttribute('role', 'grid');
361
- calendarContainer.setAttribute('aria-label', new Intl.DateTimeFormat(this._locale, { month: 'long', year: 'numeric' })
362
- .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)));
363
375
  const headerRow = document.createElement('div');
364
376
  headerRow.className = 'calendar-row calendar-header-row';
365
377
  headerRow.setAttribute('role', 'row');
@@ -414,7 +426,7 @@ dayRow.setAttribute('role', 'row');
414
426
  week.forEach(dayContext => {
415
427
  const disabled = (minTs !== null && dayContext.value < minTs) ||
416
428
  (maxTs !== null && dayContext.value > maxTs);
417
- const dayCell = this.renderDayCell(dayContext, disabled, flatIndex === effectiveFocusedIndex);
429
+ const dayCell = this.renderDayCell(dayContext, disabled, flatIndex === effectiveFocusedIndex, dayLabel);
418
430
  dayCell.className = `${dayCell.className} calendar-cell calendar-day-cell`;
419
431
  dayRow.appendChild(dayCell);
420
432
  this._cellElements[flatIndex] = dayCell;
@@ -445,6 +457,8 @@ nextCell.focus();
445
457
  }
446
458
  });
447
459
  root.appendChild(calendarContainer);
460
+ if (hadFocus)
461
+ this._cellElements[effectiveFocusedIndex]?.focus({ preventScroll: true });
448
462
  }
449
463
  }
450
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',
@@ -51,6 +51,7 @@ export declare class TyCalendar extends HTMLElement {
51
51
  private _validity?;
52
52
  private _initialValue;
53
53
  private _valueSets;
54
+ private _pressing;
54
55
  private _localeObserver?;
55
56
  static formAssociated: boolean;
56
57
  static get observedAttributes(): string[];
@@ -64,6 +65,8 @@ export declare class TyCalendar extends HTMLElement {
64
65
  set max(value: string | undefined);
65
66
  get flavor(): string;
66
67
  set flavor(value: string);
68
+ get required(): boolean;
69
+ set required(value: boolean);
67
70
  get year(): number | undefined;
68
71
  set year(value: number | undefined);
69
72
  get month(): number | undefined;
@@ -85,7 +88,12 @@ export declare class TyCalendar extends HTMLElement {
85
88
  get customCSS(): CSSStyleSheet | undefined;
86
89
  set customCSS(sheet: CSSStyleSheet | undefined);
87
90
  get value(): string;
88
- 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);
89
97
  /**
90
98
  * Initialize state from HTML attributes on first load
91
99
  */
@@ -95,9 +103,12 @@ export declare class TyCalendar extends HTMLElement {
95
103
  private syncChildComponents;
96
104
  private updateFormValue;
97
105
  /**
98
- * Selection outside [min, max] => rangeUnderflow/rangeOverflow, with a
99
- * native date input's message.
100
- * 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.
101
112
  */
102
113
  private updateValidity;
103
114
  get form(): HTMLFormElement | null;
@@ -130,7 +141,15 @@ export declare class TyCalendar extends HTMLElement {
130
141
  private pickMonth;
131
142
  private handleKeydown;
132
143
  private focusNavTitle;
133
- /** 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). */
134
153
  focus(options?: FocusOptions): void;
135
154
  private focusDayGrid;
136
155
  }