tyrell-components 1.0.0-TC56 → 1.0.0-TC57
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.css +3 -3
- package/dist/tyrell.js +1 -1
- package/lib/base/ty-component.d.ts +3 -0
- package/lib/base/ty-component.js +5 -1
- package/lib/components/calendar.d.ts +2 -0
- package/lib/components/calendar.js +9 -0
- package/lib/components/checkbox.d.ts +2 -0
- package/lib/components/checkbox.js +7 -0
- package/lib/components/date-picker.d.ts +2 -0
- package/lib/components/date-picker.js +9 -2
- package/lib/components/file-upload.d.ts +2 -0
- package/lib/components/file-upload.js +15 -2
- package/lib/components/input.d.ts +74 -2
- package/lib/components/input.js +166 -6
- package/lib/components/modal.d.ts +2 -0
- package/lib/components/modal.js +12 -9
- package/lib/components/option.js +5 -0
- package/lib/components/popup.js +16 -4
- package/lib/components/radio.d.ts +2 -0
- package/lib/components/radio.js +8 -0
- package/lib/components/select.d.ts +2 -0
- package/lib/components/select.js +12 -0
- package/lib/components/selected-tags.d.ts +7 -1
- package/lib/components/selected-tags.js +10 -1
- package/lib/components/switch.d.ts +2 -0
- package/lib/components/switch.js +7 -0
- package/lib/components/textarea.d.ts +4 -0
- package/lib/components/textarea.js +7 -0
- package/lib/styles/date-picker.js +1 -1
- package/lib/styles/file-upload.d.ts +1 -1
- package/lib/styles/file-upload.js +3 -3
- package/lib/styles/icon.d.ts +1 -1
- package/lib/styles/icon.js +2 -2
- package/lib/styles/input.js +1 -1
- package/lib/styles/radio.js +2 -2
- package/lib/utils/property-manager.d.ts +6 -0
- package/lib/utils/property-manager.js +15 -5
- package/lib/utils/validity.d.ts +12 -0
- package/lib/utils/validity.js +65 -2
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -2
|
@@ -119,6 +119,9 @@ export declare abstract class TyComponent<T = any> extends HTMLElement {
|
|
|
119
119
|
/** Subclasses report their own validity here, not via _internals directly,
|
|
120
120
|
* so a user's setCustomValidity() survives re-validation. */
|
|
121
121
|
protected applyValidity(flags: ValidityStateFlags, message?: string, anchor?: HTMLElement): void;
|
|
122
|
+
/** Show the field's current validation message in its error display, if
|
|
123
|
+
* it's invalid. Called on blur after the user edited the field. */
|
|
124
|
+
protected revealValidity(): void;
|
|
122
125
|
get validity(): ValidityState | undefined;
|
|
123
126
|
get validationMessage(): string;
|
|
124
127
|
get willValidate(): boolean;
|
package/lib/base/ty-component.js
CHANGED
|
@@ -89,7 +89,7 @@ for (const [name, config] of Object.entries(ctor.properties)) {
|
|
|
89
89
|
if (!config.formValue)
|
|
90
90
|
continue;
|
|
91
91
|
const attr = this.getAttribute(this._toKebabCase(name));
|
|
92
|
-
this._resetDefaults.set(name, attr ?? config.default ?? null);
|
|
92
|
+
this._resetDefaults.set(name, attr ?? (typeof config.default === 'function' ? config.default() : config.default) ?? null);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
const preSetProps = this._capturePreSetProperties();
|
|
@@ -171,6 +171,10 @@ return this.constructor.formAssociated === true;
|
|
|
171
171
|
applyValidity(flags, message, anchor) {
|
|
172
172
|
this._validity.set(flags, message, anchor);
|
|
173
173
|
}
|
|
174
|
+
revealValidity() {
|
|
175
|
+
if (this._validates)
|
|
176
|
+
this._validity.reveal();
|
|
177
|
+
}
|
|
174
178
|
get validity() {
|
|
175
179
|
return this._validates ? this._internals.validity : undefined;
|
|
176
180
|
}
|
|
@@ -128,5 +128,7 @@ export declare class TyCalendar extends HTMLElement {
|
|
|
128
128
|
private pickMonth;
|
|
129
129
|
private handleKeydown;
|
|
130
130
|
private focusNavTitle;
|
|
131
|
+
/** Focus the grid's tab stop (the focused day, or month in the month view). */
|
|
132
|
+
focus(options?: FocusOptions): void;
|
|
131
133
|
private focusDayGrid;
|
|
132
134
|
}
|
|
@@ -792,6 +792,15 @@ requestAnimationFrame(() => {
|
|
|
792
792
|
this._navigation?.shadowRoot?.querySelector('.nav-title')?.focus();
|
|
793
793
|
});
|
|
794
794
|
}
|
|
795
|
+
focus(options) {
|
|
796
|
+
const cell = this._view === 'months'
|
|
797
|
+
? this._monthGrid?.querySelector('.month-cell[tabindex="0"]')
|
|
798
|
+
: this._monthDisplay?.shadowRoot?.querySelector('.calendar-day-cell[tabindex="0"]');
|
|
799
|
+
if (cell)
|
|
800
|
+
cell.focus(options);
|
|
801
|
+
else
|
|
802
|
+
super.focus(options);
|
|
803
|
+
}
|
|
795
804
|
focusDayGrid() {
|
|
796
805
|
requestAnimationFrame(() => {
|
|
797
806
|
const cell = this._monthDisplay?.shadowRoot?.querySelector('.calendar-day-cell[tabindex="0"]');
|
|
@@ -110,6 +110,8 @@ export declare class TyCheckbox extends TyComponent<CheckboxState> implements Ty
|
|
|
110
110
|
* Label, required asterisk, and error UI are the consumer's responsibility.
|
|
111
111
|
*/
|
|
112
112
|
protected render(): void;
|
|
113
|
+
/** Focus the checkbox itself (the host is not focusable; its role element is). */
|
|
114
|
+
focus(options?: FocusOptions): void;
|
|
113
115
|
/** required + unchecked => valueMissing; otherwise valid. */
|
|
114
116
|
private updateValidity;
|
|
115
117
|
get checked(): boolean;
|
|
@@ -165,6 +165,13 @@ this.setupEventListeners();
|
|
|
165
165
|
this.applyLabelName(checkboxEl);
|
|
166
166
|
this.updateValidity();
|
|
167
167
|
}
|
|
168
|
+
focus(options) {
|
|
169
|
+
const el = this.shadowRoot?.querySelector(".checkbox-container");
|
|
170
|
+
if (el)
|
|
171
|
+
el.focus(options);
|
|
172
|
+
else
|
|
173
|
+
super.focus(options);
|
|
174
|
+
}
|
|
168
175
|
updateValidity() {
|
|
169
176
|
const anchor = this.shadowRoot?.querySelector(".checkbox-container");
|
|
170
177
|
if (this.disabled) {
|
|
@@ -147,6 +147,8 @@ export declare class TyDatePicker extends TyComponent<DatePickerState> {
|
|
|
147
147
|
*/
|
|
148
148
|
protected getFormValue(): FormDataEntryValue | null;
|
|
149
149
|
protected updateFormValue(): void;
|
|
150
|
+
/** Focus the field: the host itself isn't focusable. */
|
|
151
|
+
focus(options?: FocusOptions): void;
|
|
150
152
|
/**
|
|
151
153
|
* `required` used to be cosmetic: nothing reported valueMissing, so an empty
|
|
152
154
|
* required picker never blocked its form. Disabled pickers don't validate.
|
|
@@ -220,7 +220,7 @@ ensureStyles(shadow, { css: datePickerStyles, id: 'ty-date-picker' });
|
|
|
220
220
|
onConnect() {
|
|
221
221
|
this.initializeState();
|
|
222
222
|
this.render();
|
|
223
|
-
this.
|
|
223
|
+
this.updateFormValue();
|
|
224
224
|
this._localeObserver = observeLocaleChanges(this, () => {
|
|
225
225
|
this.render();
|
|
226
226
|
});
|
|
@@ -311,9 +311,16 @@ getFormValue() {
|
|
|
311
311
|
return componentsToOutputValue(this.getComponents());
|
|
312
312
|
}
|
|
313
313
|
updateFormValue() {
|
|
314
|
-
|
|
314
|
+
this._internals.setFormValue(this.getFormValue() ?? '');
|
|
315
315
|
this.updateValidity();
|
|
316
316
|
}
|
|
317
|
+
focus(options) {
|
|
318
|
+
const stub = this.shadowRoot?.querySelector('.date-picker-stub');
|
|
319
|
+
if (stub)
|
|
320
|
+
stub.focus(options);
|
|
321
|
+
else
|
|
322
|
+
super.focus(options);
|
|
323
|
+
}
|
|
317
324
|
updateValidity() {
|
|
318
325
|
if (this.getProperty('disabled') || !this.getProperty('required') || this.getFormValue()) {
|
|
319
326
|
this.applyValidity({});
|
|
@@ -84,6 +84,8 @@ export declare class TyFileUpload extends TyComponent<FileUploadState> implement
|
|
|
84
84
|
do — without this the asterisk showed but submission went through. */
|
|
85
85
|
private updateValidity;
|
|
86
86
|
protected getFormValue(): FormData | string | null;
|
|
87
|
+
/** Focus the drop zone: the host itself isn't focusable. */
|
|
88
|
+
focus(options?: FocusOptions): void;
|
|
87
89
|
/**
|
|
88
90
|
* Match a file against the `accept` attribute the way native inputs do:
|
|
89
91
|
* comma-separated list of `.ext`, `type/subtype` or `type/*` entries.
|
|
@@ -29,7 +29,9 @@ this._dropHandler = null;
|
|
|
29
29
|
this._inputChangeHandler = null;
|
|
30
30
|
ensureStyles(this.shadowRoot, { css: fileUploadStyles, id: "ty-file-upload" });
|
|
31
31
|
}
|
|
32
|
-
onConnect() {
|
|
32
|
+
onConnect() {
|
|
33
|
+
this.updateFormValue();
|
|
34
|
+
}
|
|
33
35
|
onDisconnect() {
|
|
34
36
|
this.removeEventListeners();
|
|
35
37
|
}
|
|
@@ -57,14 +59,24 @@ this.applyValidity({});
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
getFormValue() {
|
|
60
|
-
if (
|
|
62
|
+
if (!this.name)
|
|
61
63
|
return null;
|
|
62
64
|
const fd = new FormData();
|
|
65
|
+
if (this._files.length === 0) {
|
|
66
|
+
fd.append(this.name, new File([], '', { type: 'application/octet-stream' }));
|
|
67
|
+
}
|
|
63
68
|
for (const file of this._files) {
|
|
64
69
|
fd.append(this.name, file);
|
|
65
70
|
}
|
|
66
71
|
return fd;
|
|
67
72
|
}
|
|
73
|
+
focus(options) {
|
|
74
|
+
const zone = this.shadowRoot?.querySelector('.drop-zone');
|
|
75
|
+
if (zone)
|
|
76
|
+
zone.focus(options);
|
|
77
|
+
else
|
|
78
|
+
super.focus(options);
|
|
79
|
+
}
|
|
68
80
|
matchesAccept(file) {
|
|
69
81
|
const accept = this.accept?.trim();
|
|
70
82
|
if (!accept)
|
|
@@ -146,6 +158,7 @@ names: this._files.map((f) => f.name),
|
|
|
146
158
|
};
|
|
147
159
|
setTimeout(() => {
|
|
148
160
|
this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true, composed: true }));
|
|
161
|
+
this.revealValidity();
|
|
149
162
|
}, 0);
|
|
150
163
|
}
|
|
151
164
|
removeFile(index) {
|
|
@@ -59,6 +59,41 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
|
|
|
59
59
|
visual: boolean;
|
|
60
60
|
default: boolean;
|
|
61
61
|
};
|
|
62
|
+
pattern: {
|
|
63
|
+
type: "string";
|
|
64
|
+
visual: boolean;
|
|
65
|
+
default: string;
|
|
66
|
+
};
|
|
67
|
+
minlength: {
|
|
68
|
+
type: "string";
|
|
69
|
+
visual: boolean;
|
|
70
|
+
default: string;
|
|
71
|
+
};
|
|
72
|
+
maxlength: {
|
|
73
|
+
type: "string";
|
|
74
|
+
visual: boolean;
|
|
75
|
+
default: string;
|
|
76
|
+
};
|
|
77
|
+
min: {
|
|
78
|
+
type: "string";
|
|
79
|
+
visual: boolean;
|
|
80
|
+
default: string;
|
|
81
|
+
};
|
|
82
|
+
max: {
|
|
83
|
+
type: "string";
|
|
84
|
+
visual: boolean;
|
|
85
|
+
default: string;
|
|
86
|
+
};
|
|
87
|
+
tooShortMessage: {
|
|
88
|
+
type: "string";
|
|
89
|
+
visual: boolean;
|
|
90
|
+
default: string;
|
|
91
|
+
};
|
|
92
|
+
tooLongMessage: {
|
|
93
|
+
type: "string";
|
|
94
|
+
visual: boolean;
|
|
95
|
+
default: string;
|
|
96
|
+
};
|
|
62
97
|
error: {
|
|
63
98
|
type: "string";
|
|
64
99
|
visual: boolean;
|
|
@@ -102,6 +137,8 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
|
|
|
102
137
|
};
|
|
103
138
|
private _shadowValue;
|
|
104
139
|
private _isFocused;
|
|
140
|
+
/** The user typed since the field was created: blur then shows its validation message. */
|
|
141
|
+
private _userEdited;
|
|
105
142
|
private _listenersSetup;
|
|
106
143
|
private _errorAutoDanger;
|
|
107
144
|
private _customFlavorSheet;
|
|
@@ -143,8 +180,24 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
|
|
|
143
180
|
set label(v: string);
|
|
144
181
|
get disabled(): boolean;
|
|
145
182
|
set disabled(v: boolean);
|
|
183
|
+
/** Focus the inner <input>: the host itself isn't focusable. */
|
|
184
|
+
focus(options?: FocusOptions): void;
|
|
146
185
|
get required(): boolean;
|
|
147
186
|
set required(v: boolean);
|
|
187
|
+
get pattern(): string;
|
|
188
|
+
set pattern(v: string);
|
|
189
|
+
get minLength(): number;
|
|
190
|
+
set minLength(v: number);
|
|
191
|
+
get maxLength(): number;
|
|
192
|
+
set maxLength(v: number);
|
|
193
|
+
get min(): string;
|
|
194
|
+
set min(v: string);
|
|
195
|
+
get max(): string;
|
|
196
|
+
set max(v: string);
|
|
197
|
+
get tooShortMessage(): string;
|
|
198
|
+
set tooShortMessage(v: string);
|
|
199
|
+
get tooLongMessage(): string;
|
|
200
|
+
set tooLongMessage(v: string);
|
|
148
201
|
get error(): string;
|
|
149
202
|
set error(v: string);
|
|
150
203
|
get size(): Size;
|
|
@@ -165,6 +218,13 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
|
|
|
165
218
|
private dispatchChangeEvent;
|
|
166
219
|
private removeEventListeners;
|
|
167
220
|
/** IMPORTANT: Only called ONCE, not on every render. */
|
|
221
|
+
/**
|
|
222
|
+
* Enter submits the owning form, like a native input (HTML implicit
|
|
223
|
+
* submission): through the form's default button when it has one (nothing
|
|
224
|
+
* if that button is disabled), otherwise only when this is the form's only
|
|
225
|
+
* text field.
|
|
226
|
+
*/
|
|
227
|
+
private _implicitSubmit;
|
|
168
228
|
private setupEventListeners;
|
|
169
229
|
/** Render the input component with wrapper and slots. */
|
|
170
230
|
render(): void;
|
|
@@ -174,10 +234,22 @@ export declare class TyInput extends TyComponent<InputState> implements TyInputE
|
|
|
174
234
|
* `type` property stays "password" (form value and API unaffected).
|
|
175
235
|
*/
|
|
176
236
|
private setupPasswordToggle;
|
|
237
|
+
/**
|
|
238
|
+
* pattern and maxlength go on the inner <input>: pattern for the browser's
|
|
239
|
+
* own patternMismatch check, maxlength so typing stops at the limit like a
|
|
240
|
+
* native field. Formatted (numeric) types show a display string, so they
|
|
241
|
+
* get neither.
|
|
242
|
+
*/
|
|
243
|
+
private syncConstraintAttributes;
|
|
177
244
|
/**
|
|
178
245
|
* Reflect constraint validation to the owning <form> via ElementInternals.
|
|
179
|
-
* Without this
|
|
180
|
-
*
|
|
246
|
+
* Without this the inner shadow <input> can't block the outer form.
|
|
247
|
+
* Rules, first failing one supplies the message:
|
|
248
|
+
* - required: empty => valueMissing
|
|
249
|
+
* - email/url: the inner input's own typeMismatch
|
|
250
|
+
* - text types: pattern, minlength, maxlength
|
|
251
|
+
* - number/currency/percent/compact: min, max against the numeric value
|
|
252
|
+
* Empty values only fail `required`, like native inputs.
|
|
181
253
|
* Disabled fields are barred from constraint validation.
|
|
182
254
|
*/
|
|
183
255
|
private updateValidity;
|
package/lib/components/input.js
CHANGED
|
@@ -13,6 +13,7 @@ constructor() {
|
|
|
13
13
|
super();
|
|
14
14
|
this._shadowValue = null;
|
|
15
15
|
this._isFocused = false;
|
|
16
|
+
this._userEdited = false;
|
|
16
17
|
this._listenersSetup = false;
|
|
17
18
|
this._errorAutoDanger = false;
|
|
18
19
|
this._customFlavorSheet = null;
|
|
@@ -22,6 +23,34 @@ this._focusHandler = null;
|
|
|
22
23
|
this._blurHandler = null;
|
|
23
24
|
this._inputDebounceTimer = null;
|
|
24
25
|
this._changeDebounceTimer = null;
|
|
26
|
+
this._implicitSubmit = (e) => {
|
|
27
|
+
if (e.key !== 'Enter' || e.isComposing || e.defaultPrevented)
|
|
28
|
+
return;
|
|
29
|
+
const form = this._internals.form;
|
|
30
|
+
if (!form)
|
|
31
|
+
return;
|
|
32
|
+
const controls = Array.from(form.elements);
|
|
33
|
+
const isSubmitter = (el) => (el instanceof HTMLButtonElement && el.type === 'submit') ||
|
|
34
|
+
(el instanceof HTMLInputElement && (el.type === 'submit' || el.type === 'image')) ||
|
|
35
|
+
(el.tagName === 'TY-BUTTON' && !['button', 'reset'].includes(el.getAttribute('type') ?? ''));
|
|
36
|
+
const submitter = controls.find(isSubmitter);
|
|
37
|
+
if (submitter) {
|
|
38
|
+
if (submitter.disabled || submitter.hasAttribute('disabled'))
|
|
39
|
+
return;
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
const target = submitter.tagName === 'TY-BUTTON'
|
|
42
|
+
? submitter.shadowRoot?.querySelector('button') ?? submitter
|
|
43
|
+
: submitter;
|
|
44
|
+
target.click();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const textFields = controls.filter((el) => el.tagName === 'TY-INPUT' ||
|
|
48
|
+
(el instanceof HTMLInputElement && !['button', 'checkbox', 'radio', 'file', 'hidden', 'image', 'reset', 'submit', 'range', 'color'].includes(el.type)));
|
|
49
|
+
if (textFields.length === 1) {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
form.requestSubmit();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
25
54
|
const shadow = this.shadowRoot;
|
|
26
55
|
ensureStyles(shadow, { css: inputStyles, id: 'ty-input' });
|
|
27
56
|
}
|
|
@@ -95,14 +124,17 @@ this._changeDebounceTimer = null;
|
|
|
95
124
|
this.render();
|
|
96
125
|
}
|
|
97
126
|
getFormValue() {
|
|
98
|
-
const formValue = this._shadowValue !== null ? String(this._shadowValue) :
|
|
127
|
+
const formValue = this._shadowValue !== null ? String(this._shadowValue) : '';
|
|
99
128
|
return formValue;
|
|
100
129
|
}
|
|
101
130
|
initializeShadowValue() {
|
|
102
131
|
const currentValue = this.getProperty('value');
|
|
103
132
|
if (currentValue) {
|
|
104
133
|
this._shadowValue = this.parseShadowValue(currentValue);
|
|
105
|
-
this._internals.setFormValue(this._shadowValue !== null ? String(this._shadowValue) :
|
|
134
|
+
this._internals.setFormValue(this._shadowValue !== null ? String(this._shadowValue) : '');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this._internals.setFormValue('');
|
|
106
138
|
}
|
|
107
139
|
}
|
|
108
140
|
parseShadowValue(value) {
|
|
@@ -155,8 +187,25 @@ get label() { return this.getProperty('label'); }
|
|
|
155
187
|
set label(v) { this.setProperty('label', v); }
|
|
156
188
|
get disabled() { return this.getProperty('disabled'); }
|
|
157
189
|
set disabled(v) { this.setProperty('disabled', v); }
|
|
190
|
+
focus(options) {
|
|
191
|
+
this.shadowRoot?.querySelector('input')?.focus(options);
|
|
192
|
+
}
|
|
158
193
|
get required() { return this.getProperty('required'); }
|
|
159
194
|
set required(v) { this.setProperty('required', v); }
|
|
195
|
+
get pattern() { return this.getProperty('pattern'); }
|
|
196
|
+
set pattern(v) { this.setProperty('pattern', v ?? ''); }
|
|
197
|
+
get minLength() { return toLimit(this.getProperty('minlength')) ?? -1; }
|
|
198
|
+
set minLength(v) { this.setProperty('minlength', v == null || v < 0 ? '' : String(v)); }
|
|
199
|
+
get maxLength() { return toLimit(this.getProperty('maxlength')) ?? -1; }
|
|
200
|
+
set maxLength(v) { this.setProperty('maxlength', v == null || v < 0 ? '' : String(v)); }
|
|
201
|
+
get min() { return this.getProperty('min'); }
|
|
202
|
+
set min(v) { this.setProperty('min', v == null ? '' : String(v)); }
|
|
203
|
+
get max() { return this.getProperty('max'); }
|
|
204
|
+
set max(v) { this.setProperty('max', v == null ? '' : String(v)); }
|
|
205
|
+
get tooShortMessage() { return this.getProperty('tooShortMessage'); }
|
|
206
|
+
set tooShortMessage(v) { this.setProperty('tooShortMessage', v ?? ''); }
|
|
207
|
+
get tooLongMessage() { return this.getProperty('tooLongMessage'); }
|
|
208
|
+
set tooLongMessage(v) { this.setProperty('tooLongMessage', v ?? ''); }
|
|
160
209
|
get error() { return this.getProperty('error'); }
|
|
161
210
|
set error(v) { this.setProperty('error', v); }
|
|
162
211
|
get size() { return this.getProperty('size'); }
|
|
@@ -235,6 +284,7 @@ if (this._blurHandler) {
|
|
|
235
284
|
inputEl.removeEventListener('blur', this._blurHandler);
|
|
236
285
|
this._blurHandler = null;
|
|
237
286
|
}
|
|
287
|
+
inputEl.removeEventListener('keydown', this._implicitSubmit);
|
|
238
288
|
this._listenersSetup = false;
|
|
239
289
|
}
|
|
240
290
|
setupEventListeners() {
|
|
@@ -249,12 +299,13 @@ console.warn('[TyInput.setupEventListeners] ⚠️ Input element not found!');
|
|
|
249
299
|
return;
|
|
250
300
|
}
|
|
251
301
|
this._inputHandler = (e) => {
|
|
302
|
+
this._userEdited = true;
|
|
252
303
|
e.stopPropagation();
|
|
253
304
|
e.stopImmediatePropagation();
|
|
254
305
|
const target = e.target;
|
|
255
306
|
const rawValue = target.value;
|
|
256
307
|
this._shadowValue = this.parseShadowValue(rawValue);
|
|
257
|
-
this._internals.setFormValue(this._shadowValue !== null ? String(this._shadowValue) :
|
|
308
|
+
this._internals.setFormValue(this._shadowValue !== null ? String(this._shadowValue) : '');
|
|
258
309
|
this.updateValidity();
|
|
259
310
|
if (this._inputDebounceTimer !== null) {
|
|
260
311
|
clearTimeout(this._inputDebounceTimer);
|
|
@@ -275,7 +326,7 @@ e.stopImmediatePropagation();
|
|
|
275
326
|
const target = e.target;
|
|
276
327
|
const rawValue = target.value;
|
|
277
328
|
this._shadowValue = this.parseShadowValue(rawValue);
|
|
278
|
-
this._internals.setFormValue(this._shadowValue !== null ? String(this._shadowValue) :
|
|
329
|
+
this._internals.setFormValue(this._shadowValue !== null ? String(this._shadowValue) : '');
|
|
279
330
|
this.updateValidity();
|
|
280
331
|
if (this._changeDebounceTimer !== null) {
|
|
281
332
|
clearTimeout(this._changeDebounceTimer);
|
|
@@ -317,6 +368,8 @@ this.dispatchChangeEvent(rawValue, e);
|
|
|
317
368
|
}
|
|
318
369
|
this._isFocused = false;
|
|
319
370
|
wrapperEl.classList.remove('focused');
|
|
371
|
+
if (this._userEdited)
|
|
372
|
+
this.revealValidity();
|
|
320
373
|
if (this.shouldFormat()) {
|
|
321
374
|
inputEl.value = this.getDisplayValue();
|
|
322
375
|
}
|
|
@@ -330,6 +383,7 @@ inputEl.addEventListener('input', this._inputHandler);
|
|
|
330
383
|
inputEl.addEventListener('change', this._changeHandler);
|
|
331
384
|
inputEl.addEventListener('focus', this._focusHandler);
|
|
332
385
|
inputEl.addEventListener('blur', this._blurHandler);
|
|
386
|
+
inputEl.addEventListener('keydown', this._implicitSubmit);
|
|
333
387
|
this._listenersSetup = true;
|
|
334
388
|
}
|
|
335
389
|
render() {
|
|
@@ -372,6 +426,7 @@ existingInput.setAttribute('required', '');
|
|
|
372
426
|
else {
|
|
373
427
|
existingInput.removeAttribute('required');
|
|
374
428
|
}
|
|
429
|
+
this.syncConstraintAttributes(existingInput);
|
|
375
430
|
if (this.label)
|
|
376
431
|
existingInput.setAttribute('aria-label', this.label);
|
|
377
432
|
else
|
|
@@ -445,6 +500,7 @@ ${errorHtml}
|
|
|
445
500
|
const inputEl = shadow.querySelector('input');
|
|
446
501
|
inputEl.disabled = this.disabled;
|
|
447
502
|
inputEl.required = this.required;
|
|
503
|
+
this.syncConstraintAttributes(inputEl);
|
|
448
504
|
if (this.label)
|
|
449
505
|
inputEl.setAttribute('aria-label', this.label);
|
|
450
506
|
this.setupPasswordToggle();
|
|
@@ -467,6 +523,12 @@ toggle.setAttribute('aria-label', reveal ? 'Hide password' : 'Show password');
|
|
|
467
523
|
toggle.setAttribute('aria-pressed', String(reveal));
|
|
468
524
|
});
|
|
469
525
|
}
|
|
526
|
+
syncConstraintAttributes(inputEl) {
|
|
527
|
+
const text = !shouldFormatType(this.type);
|
|
528
|
+
const set = (name, v) => text && v !== '' ? inputEl.setAttribute(name, v) : inputEl.removeAttribute(name);
|
|
529
|
+
set('pattern', this.pattern);
|
|
530
|
+
set('maxlength', toLimit(this.getProperty('maxlength')) != null ? String(this.maxLength) : '');
|
|
531
|
+
}
|
|
470
532
|
updateValidity() {
|
|
471
533
|
const inputEl = this.shadowRoot?.querySelector('input');
|
|
472
534
|
if (this.disabled) {
|
|
@@ -474,12 +536,48 @@ this.applyValidity({});
|
|
|
474
536
|
return;
|
|
475
537
|
}
|
|
476
538
|
const current = inputEl ? inputEl.value : String(this.getProperty('value') ?? '');
|
|
477
|
-
if (
|
|
478
|
-
this.
|
|
539
|
+
if (current.trim() === '') {
|
|
540
|
+
if (this.required) {
|
|
541
|
+
this.applyValidity({ valueMissing: true }, nativeMessage('text', { required: '' }, ''), inputEl ?? undefined);
|
|
479
542
|
}
|
|
480
543
|
else {
|
|
481
544
|
this.applyValidity({});
|
|
482
545
|
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const flags = {};
|
|
549
|
+
const messages = [];
|
|
550
|
+
const fail = (flag, message) => {
|
|
551
|
+
flags[flag] = true;
|
|
552
|
+
messages.push(message);
|
|
553
|
+
};
|
|
554
|
+
if (shouldFormatType(this.type)) {
|
|
555
|
+
const n = typeof this._shadowValue === 'number' ? this._shadowValue : null;
|
|
556
|
+
const min = toNumber(this.min);
|
|
557
|
+
const max = toNumber(this.max);
|
|
558
|
+
const range = (flag) => fail(flag, nativeMessage('number', { step: 'any', min: this.min, max: this.max }, String(n)));
|
|
559
|
+
if (n !== null && min !== null && n < min)
|
|
560
|
+
range('rangeUnderflow');
|
|
561
|
+
if (n !== null && max !== null && n > max)
|
|
562
|
+
range('rangeOverflow');
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
if (inputEl?.validity.typeMismatch)
|
|
566
|
+
fail('typeMismatch', inputEl.validationMessage);
|
|
567
|
+
if (inputEl?.validity.patternMismatch)
|
|
568
|
+
fail('patternMismatch', inputEl.validationMessage);
|
|
569
|
+
const length = current.length;
|
|
570
|
+
const minLength = toLimit(this.getProperty('minlength'));
|
|
571
|
+
const maxLength = toLimit(this.getProperty('maxlength'));
|
|
572
|
+
const vars = { minlength: minLength, maxlength: maxLength, length };
|
|
573
|
+
if (minLength !== null && length < minLength) {
|
|
574
|
+
fail('tooShort', fillMessage(this.tooShortMessage || 'Use at least {minlength} characters (now {length})', vars));
|
|
575
|
+
}
|
|
576
|
+
if (maxLength !== null && length > maxLength) {
|
|
577
|
+
fail('tooLong', fillMessage(this.tooLongMessage || 'Use at most {maxlength} characters (now {length})', vars));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
this.applyValidity(flags, messages[0], inputEl ?? undefined);
|
|
483
581
|
}
|
|
484
582
|
}
|
|
485
583
|
TyInput.formAssociated = true;
|
|
@@ -538,6 +636,41 @@ type: 'boolean',
|
|
|
538
636
|
visual: true,
|
|
539
637
|
default: false
|
|
540
638
|
},
|
|
639
|
+
pattern: {
|
|
640
|
+
type: 'string',
|
|
641
|
+
visual: true,
|
|
642
|
+
default: ''
|
|
643
|
+
},
|
|
644
|
+
minlength: {
|
|
645
|
+
type: 'string',
|
|
646
|
+
visual: true,
|
|
647
|
+
default: ''
|
|
648
|
+
},
|
|
649
|
+
maxlength: {
|
|
650
|
+
type: 'string',
|
|
651
|
+
visual: true,
|
|
652
|
+
default: ''
|
|
653
|
+
},
|
|
654
|
+
min: {
|
|
655
|
+
type: 'string',
|
|
656
|
+
visual: true,
|
|
657
|
+
default: ''
|
|
658
|
+
},
|
|
659
|
+
max: {
|
|
660
|
+
type: 'string',
|
|
661
|
+
visual: true,
|
|
662
|
+
default: ''
|
|
663
|
+
},
|
|
664
|
+
tooShortMessage: {
|
|
665
|
+
type: 'string',
|
|
666
|
+
visual: true,
|
|
667
|
+
default: ''
|
|
668
|
+
},
|
|
669
|
+
tooLongMessage: {
|
|
670
|
+
type: 'string',
|
|
671
|
+
visual: true,
|
|
672
|
+
default: ''
|
|
673
|
+
},
|
|
541
674
|
error: {
|
|
542
675
|
type: 'string',
|
|
543
676
|
visual: true,
|
|
@@ -604,6 +737,33 @@ return Math.max(0, Math.min(5000, Math.floor(num)));
|
|
|
604
737
|
}
|
|
605
738
|
}
|
|
606
739
|
};
|
|
740
|
+
let probe = null;
|
|
741
|
+
function nativeMessage(type, attrs, value) {
|
|
742
|
+
probe ?? (probe = document.createElement('input'));
|
|
743
|
+
for (const name of Array.from(probe.getAttributeNames()))
|
|
744
|
+
probe.removeAttribute(name);
|
|
745
|
+
probe.type = type;
|
|
746
|
+
for (const [name, v] of Object.entries(attrs))
|
|
747
|
+
if (v !== '' || name === 'required')
|
|
748
|
+
probe.setAttribute(name, v);
|
|
749
|
+
probe.value = value;
|
|
750
|
+
return probe.validationMessage;
|
|
751
|
+
}
|
|
752
|
+
function fillMessage(template, vars) {
|
|
753
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => vars[key] != null ? String(vars[key]) : match);
|
|
754
|
+
}
|
|
755
|
+
function toLimit(v) {
|
|
756
|
+
if (v === null || v === undefined || String(v).trim() === '')
|
|
757
|
+
return null;
|
|
758
|
+
const n = Number(v);
|
|
759
|
+
return Number.isInteger(n) && n >= 0 ? n : null;
|
|
760
|
+
}
|
|
761
|
+
function toNumber(v) {
|
|
762
|
+
if (v === null || v === undefined || String(v).trim() === '')
|
|
763
|
+
return null;
|
|
764
|
+
const n = Number(v);
|
|
765
|
+
return Number.isFinite(n) ? n : null;
|
|
766
|
+
}
|
|
607
767
|
if (!customElements.get('ty-input')) {
|
|
608
768
|
customElements.define('ty-input', TyInput);
|
|
609
769
|
}
|
|
@@ -52,6 +52,8 @@ export declare class TyModal extends HTMLElement {
|
|
|
52
52
|
set open(v: boolean);
|
|
53
53
|
get noBackdrop(): boolean;
|
|
54
54
|
set noBackdrop(v: boolean);
|
|
55
|
+
get noZoom(): boolean;
|
|
56
|
+
set noZoom(v: boolean);
|
|
55
57
|
get preventOutsideClick(): boolean;
|
|
56
58
|
set preventOutsideClick(v: boolean);
|
|
57
59
|
get preventEscape(): boolean;
|
package/lib/components/modal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
|
|
2
|
+
import { coerceBoolean } from '../utils/property-manager.js';
|
|
2
3
|
import { ensureStyles } from '../utils/styles.js';
|
|
3
4
|
import { modalStyles } from '../styles/modal.js';
|
|
4
5
|
const backdropClickHandlers = new WeakMap();
|
|
@@ -259,6 +260,7 @@ pendingCloseReasons.delete(el);
|
|
|
259
260
|
dispatchModalEvent(el, 'close', { reason });
|
|
260
261
|
}
|
|
261
262
|
}
|
|
263
|
+
syncBackdropZoom();
|
|
262
264
|
dialog.onclose = (event) => {
|
|
263
265
|
if (event.target !== dialog)
|
|
264
266
|
return;
|
|
@@ -274,10 +276,7 @@ syncBackdropZoom();
|
|
|
274
276
|
};
|
|
275
277
|
}
|
|
276
278
|
function syncBackdropZoom() {
|
|
277
|
-
const anyOpen = Array.from(document.querySelectorAll('ty-modal[open]
|
|
278
|
-
const v = m.getAttribute('backdrop-zoom').toLowerCase().trim();
|
|
279
|
-
return v !== 'false' && v !== '0';
|
|
280
|
-
});
|
|
279
|
+
const anyOpen = Array.from(document.querySelectorAll('ty-modal[open], ty-dialog[open]')).some((m) => parseBoolAttr(m, 'open') && !parseBoolAttr(m, 'no-zoom'));
|
|
281
280
|
document.documentElement.classList.toggle('ty-modal-zoom', anyOpen);
|
|
282
281
|
}
|
|
283
282
|
function cleanup(el) {
|
|
@@ -325,21 +324,25 @@ hoverLeaveHandlers.delete(el);
|
|
|
325
324
|
}
|
|
326
325
|
export class TyModal extends HTMLElement {
|
|
327
326
|
static get observedAttributes() {
|
|
328
|
-
return ['open', 'no-backdrop', 'prevent-outside-click', 'prevent-escape', 'label'];
|
|
327
|
+
return ['open', 'no-backdrop', 'no-zoom', 'prevent-outside-click', 'prevent-escape', 'label'];
|
|
329
328
|
}
|
|
330
329
|
get open() { return parseBoolAttr(this, 'open'); }
|
|
331
|
-
set open(v) { v ? this.setAttribute('open', 'true') : this.removeAttribute('open'); }
|
|
330
|
+
set open(v) { coerceBoolean(v) ? this.setAttribute('open', 'true') : this.removeAttribute('open'); }
|
|
332
331
|
get noBackdrop() { return parseBoolAttr(this, 'no-backdrop'); }
|
|
333
332
|
set noBackdrop(v) {
|
|
334
|
-
v ? this.setAttribute('no-backdrop', '') : this.removeAttribute('no-backdrop');
|
|
333
|
+
coerceBoolean(v) ? this.setAttribute('no-backdrop', '') : this.removeAttribute('no-backdrop');
|
|
334
|
+
}
|
|
335
|
+
get noZoom() { return parseBoolAttr(this, 'no-zoom'); }
|
|
336
|
+
set noZoom(v) {
|
|
337
|
+
coerceBoolean(v) ? this.setAttribute('no-zoom', '') : this.removeAttribute('no-zoom');
|
|
335
338
|
}
|
|
336
339
|
get preventOutsideClick() { return parseBoolAttr(this, 'prevent-outside-click'); }
|
|
337
340
|
set preventOutsideClick(v) {
|
|
338
|
-
v ? this.setAttribute('prevent-outside-click', '') : this.removeAttribute('prevent-outside-click');
|
|
341
|
+
coerceBoolean(v) ? this.setAttribute('prevent-outside-click', '') : this.removeAttribute('prevent-outside-click');
|
|
339
342
|
}
|
|
340
343
|
get preventEscape() { return parseBoolAttr(this, 'prevent-escape'); }
|
|
341
344
|
set preventEscape(v) {
|
|
342
|
-
v ? this.setAttribute('prevent-escape', '') : this.removeAttribute('prevent-escape');
|
|
345
|
+
coerceBoolean(v) ? this.setAttribute('prevent-escape', '') : this.removeAttribute('prevent-escape');
|
|
343
346
|
}
|
|
344
347
|
constructor() {
|
|
345
348
|
super();
|
package/lib/components/option.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ensureStyles } from '../utils/styles.js';
|
|
2
|
+
import { coerceBoolean } from '../utils/property-manager.js';
|
|
2
3
|
import { optionStyles } from '../styles/option.js';
|
|
3
4
|
const CHECK_SVG = `
|
|
4
5
|
<svg class="option-check" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
@@ -80,6 +81,7 @@ get selected() {
|
|
|
80
81
|
return this._selected;
|
|
81
82
|
}
|
|
82
83
|
set selected(value) {
|
|
84
|
+
value = coerceBoolean(value);
|
|
83
85
|
if (this._selected !== value) {
|
|
84
86
|
this._selected = value;
|
|
85
87
|
if (value) {
|
|
@@ -95,6 +97,7 @@ get disabled() {
|
|
|
95
97
|
return this._disabled;
|
|
96
98
|
}
|
|
97
99
|
set disabled(value) {
|
|
100
|
+
value = coerceBoolean(value);
|
|
98
101
|
if (this._disabled !== value) {
|
|
99
102
|
this._disabled = value;
|
|
100
103
|
if (value) {
|
|
@@ -110,6 +113,7 @@ get highlighted() {
|
|
|
110
113
|
return this._highlighted;
|
|
111
114
|
}
|
|
112
115
|
set highlighted(value) {
|
|
116
|
+
value = coerceBoolean(value);
|
|
113
117
|
if (this._highlighted !== value) {
|
|
114
118
|
this._highlighted = value;
|
|
115
119
|
if (value) {
|
|
@@ -125,6 +129,7 @@ get hidden() {
|
|
|
125
129
|
return this._hidden;
|
|
126
130
|
}
|
|
127
131
|
set hidden(value) {
|
|
132
|
+
value = coerceBoolean(value);
|
|
128
133
|
if (this._hidden !== value) {
|
|
129
134
|
this._hidden = value;
|
|
130
135
|
if (value) {
|