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.
- package/css/tyrell-colors-static.css +14 -0
- package/css/tyrell-theme.css +1 -0
- package/css/tyrell.css +8 -6
- package/dist/tyrell-colors-static.css +1 -1
- package/dist/tyrell-theme.css +1 -1
- package/dist/tyrell.js +1 -1
- package/lib/base/ty-component.d.ts +8 -12
- package/lib/base/ty-component.js +38 -9
- package/lib/components/button.d.ts +10 -0
- package/lib/components/button.js +22 -1
- package/lib/components/calendar-month.d.ts +7 -1
- package/lib/components/calendar-month.js +32 -18
- package/lib/components/calendar-navigation.js +19 -6
- package/lib/components/calendar.d.ts +24 -5
- package/lib/components/calendar.js +83 -37
- package/lib/components/checkbox.d.ts +5 -0
- package/lib/components/checkbox.js +18 -5
- package/lib/components/copy.d.ts +2 -0
- package/lib/components/copy.js +15 -31
- package/lib/components/date-picker.d.ts +7 -26
- package/lib/components/date-picker.js +14 -84
- package/lib/components/file-upload.js +6 -2
- package/lib/components/input.d.ts +0 -9
- package/lib/components/input.js +17 -40
- package/lib/components/modal.js +59 -18
- package/lib/components/popup.js +56 -5
- package/lib/components/radio.d.ts +6 -0
- package/lib/components/radio.js +12 -28
- package/lib/components/select.d.ts +29 -11
- package/lib/components/select.js +43 -57
- package/lib/components/switch.d.ts +4 -0
- package/lib/components/switch.js +18 -5
- package/lib/components/tabs.js +1 -1
- package/lib/components/textarea.d.ts +1 -0
- package/lib/components/textarea.js +21 -8
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/styles/button.js +1 -0
- package/lib/styles/calendar-month.js +1 -1
- package/lib/styles/calendar.js +3 -0
- package/lib/styles/checkbox.js +11 -5
- package/lib/styles/date-picker.js +5 -10
- package/lib/styles/input.js +5 -9
- package/lib/styles/option.d.ts +1 -1
- package/lib/styles/option.js +4 -0
- package/lib/styles/popup.d.ts +1 -1
- package/lib/styles/popup.js +2 -0
- package/lib/styles/radio.js +10 -4
- package/lib/styles/select-base.js +5 -10
- package/lib/styles/select.d.ts +1 -1
- package/lib/styles/select.js +4 -0
- package/lib/styles/switch.js +10 -5
- package/lib/styles/tag.js +2 -1
- package/lib/utils/calendar-utils.d.ts +49 -0
- package/lib/utils/calendar-utils.js +63 -6
- package/lib/utils/scroll-lock.d.ts +10 -4
- package/lib/utils/scroll-lock.js +102 -2
- package/lib/utils/validity.d.ts +27 -1
- package/lib/utils/validity.js +68 -16
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/dist/tyrell.js.map +0 -1
- package/lib/utils/error-flavor.d.ts +0 -8
- 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.
|
package/lib/base/ty-component.js
CHANGED
|
@@ -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
|
package/lib/components/button.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
28
|
+
const parsed = parseDateDay(iso);
|
|
29
29
|
if (!parsed)
|
|
30
30
|
return null;
|
|
31
|
-
return
|
|
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') ||
|
|
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 ||
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
362
|
-
.format(
|
|
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,
|
|
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 =
|
|
126
|
-
const 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
|
-
|
|
210
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
-
/**
|
|
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
|
}
|