tyrell-components 1.0.0-TC53 → 1.0.0-TC54
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/dist/tyrell.js +1 -1
- package/lib/components/select.d.ts +32 -1
- package/lib/components/select.js +30 -11
- package/lib/components/selected-tags.js +4 -0
- package/lib/components/tabs.js +8 -10
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
|
@@ -208,6 +208,29 @@ export declare class TySelect extends TyComponent<SelectState> {
|
|
|
208
208
|
private parseValue;
|
|
209
209
|
/** Initialize component state from the value property / pre-selected children. */
|
|
210
210
|
private initializeState;
|
|
211
|
+
/**
|
|
212
|
+
* Declarative init: no `value` attribute, but markup pre-marked one or more
|
|
213
|
+
* options `selected` — native <select> semantics. This is the ONE place
|
|
214
|
+
* selection is read FROM the DOM rather than treated as a rendering of
|
|
215
|
+
* state (see getSelectedValues()): it seeds initial state, once. After
|
|
216
|
+
* this, `value` rules — flipping an option's `selected` property later
|
|
217
|
+
* does nothing, same as native.
|
|
218
|
+
*
|
|
219
|
+
* Called from initializeState() at connect AND from the child observer:
|
|
220
|
+
* a real HTML parser upgrades a custom element the moment its start tag is
|
|
221
|
+
* seen, BEFORE its children exist — `<ty-select>` connects with zero
|
|
222
|
+
* `<ty-option>` children when it's part of the page's own markup (as
|
|
223
|
+
* opposed to an already-built fragment attached via innerHTML/appendChild,
|
|
224
|
+
* where children exist at connect). Calling this only at connect would
|
|
225
|
+
* silently miss every option parsed after that instant, which is the
|
|
226
|
+
* common case for markup that is literally in the page's initial HTML.
|
|
227
|
+
*
|
|
228
|
+
* Safe to call repeatedly: no-ops once `value` has ever been explicitly
|
|
229
|
+
* set (attribute or property) or state is already non-empty — including
|
|
230
|
+
* after a real `.clear()`, which removes `selected` from every option via
|
|
231
|
+
* syncSelectedTags, so there is nothing left here to (re-)seed from.
|
|
232
|
+
*/
|
|
233
|
+
private resolveDeclarativeSelection;
|
|
211
234
|
/**
|
|
212
235
|
* Get all option elements from the component (ty-option preferred, ty-tag accepted)
|
|
213
236
|
*/
|
|
@@ -223,7 +246,15 @@ export declare class TySelect extends TyComponent<SelectState> {
|
|
|
223
246
|
*/
|
|
224
247
|
private deselectTag;
|
|
225
248
|
/**
|
|
226
|
-
* Get array of currently selected values
|
|
249
|
+
* Get the array of currently selected values.
|
|
250
|
+
*
|
|
251
|
+
* Reads STATE (`_state.selectedValues`), not the DOM: `value` is the
|
|
252
|
+
* selection, and an option's `selected` attribute is a rendering of it
|
|
253
|
+
* (written by syncSelectedTags below), never the other way round. This
|
|
254
|
+
* matters whenever options don't fully reflect the selection — external
|
|
255
|
+
* search legitimately has a selected value with no matching option
|
|
256
|
+
* present; a DOM scan would report empty and desync .value, `required`
|
|
257
|
+
* validation and the click-toggle logic from what's actually selected.
|
|
227
258
|
*/
|
|
228
259
|
private getSelectedValues;
|
|
229
260
|
private syncSelectedTags;
|
package/lib/components/select.js
CHANGED
|
@@ -94,9 +94,7 @@ dialog.close();
|
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
this.render();
|
|
97
|
-
requestAnimationFrame(() => {
|
|
98
97
|
this.initializeState();
|
|
99
|
-
});
|
|
100
98
|
this._childObserver = new MutationObserver(() => {
|
|
101
99
|
this.ensureOptionRoles();
|
|
102
100
|
if (this._state.selectedValues.length > 0) {
|
|
@@ -268,13 +266,23 @@ this.updateSelectionDisplay();
|
|
|
268
266
|
this.updateMobileSelectedState();
|
|
269
267
|
}
|
|
270
268
|
else {
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
this.resolveDeclarativeSelection();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
resolveDeclarativeSelection() {
|
|
273
|
+
if (this.getAttribute("value") || this._state.selectedValues.length > 0) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const preSelectedTags = this.getTagElements()
|
|
273
277
|
.filter((tag) => tag.hasAttribute("selected"))
|
|
274
278
|
.map((tag) => this.getTagData(tag).value);
|
|
275
279
|
if (preSelectedTags.length > 0) {
|
|
276
280
|
this.updateComponentValue(preSelectedTags, false);
|
|
281
|
+
return;
|
|
277
282
|
}
|
|
283
|
+
if (document.readyState === "loading") {
|
|
284
|
+
document.addEventListener("DOMContentLoaded", () => { if (this.isConnected)
|
|
285
|
+
this.resolveDeclarativeSelection(); }, { once: true });
|
|
278
286
|
}
|
|
279
287
|
}
|
|
280
288
|
getTagElements() {
|
|
@@ -295,10 +303,7 @@ deselectTag(tag) {
|
|
|
295
303
|
tag.removeAttribute("selected");
|
|
296
304
|
}
|
|
297
305
|
getSelectedValues() {
|
|
298
|
-
return this.
|
|
299
|
-
.filter((tag) => tag.hasAttribute("selected"))
|
|
300
|
-
.map((tag) => this.getTagData(tag).value)
|
|
301
|
-
.filter((value) => value !== "");
|
|
306
|
+
return [...this._state.selectedValues];
|
|
302
307
|
}
|
|
303
308
|
syncSelectedTags(selectedValues) {
|
|
304
309
|
const selectedSet = new Set(selectedValues);
|
|
@@ -612,7 +617,7 @@ this.closeDropdown();
|
|
|
612
617
|
}
|
|
613
618
|
return;
|
|
614
619
|
}
|
|
615
|
-
if (
|
|
620
|
+
if (currentValues.includes(tagValue)) {
|
|
616
621
|
const newValues = currentValues.filter((v) => v !== tagValue);
|
|
617
622
|
this.updateComponentValue(newValues, true, "remove", tagValue);
|
|
618
623
|
}
|
|
@@ -1234,10 +1239,11 @@ clearBtn.hidden = !(count > 0 && this._clearable && !this._disabled && !this._re
|
|
|
1234
1239
|
}
|
|
1235
1240
|
const wantClone = !this._multiple && count > 0 ? values[0] : null;
|
|
1236
1241
|
this.syncSelectedClone(wantClone);
|
|
1237
|
-
|
|
1242
|
+
const hasClone = this._selectedClone !== null;
|
|
1243
|
+
stub.classList.toggle("has-clone", hasClone);
|
|
1238
1244
|
const textEl = stub.querySelector(".dropdown-placeholder");
|
|
1239
1245
|
if (textEl) {
|
|
1240
|
-
textEl.hidden =
|
|
1246
|
+
textEl.hidden = hasClone;
|
|
1241
1247
|
const showLabels = count > 0 && this._multiple && !this._compact;
|
|
1242
1248
|
if (showLabels) {
|
|
1243
1249
|
textEl.textContent = this.getSelectedItems(values)
|
|
@@ -1245,6 +1251,10 @@ textEl.textContent = this.getSelectedItems(values)
|
|
|
1245
1251
|
.join(", ");
|
|
1246
1252
|
textEl.classList.remove("placeholder-shown");
|
|
1247
1253
|
}
|
|
1254
|
+
else if (wantClone !== null) {
|
|
1255
|
+
textEl.textContent = values.join(", ");
|
|
1256
|
+
textEl.classList.remove("placeholder-shown");
|
|
1257
|
+
}
|
|
1248
1258
|
else {
|
|
1249
1259
|
textEl.textContent = this._placeholder;
|
|
1250
1260
|
textEl.classList.add("placeholder-shown");
|
|
@@ -1254,9 +1264,14 @@ textEl.classList.add("placeholder-shown");
|
|
|
1254
1264
|
syncSelectedClone(value) {
|
|
1255
1265
|
const existing = this.querySelector(':scope > [cloned][slot="selected"]');
|
|
1256
1266
|
if (existing?.getAttribute("value") === value) {
|
|
1267
|
+
const stale = value !== null &&
|
|
1268
|
+
existing.childNodes.length === 0 &&
|
|
1269
|
+
(this.getTagElements().find((el) => this.getTagData(el).value === value)?.childNodes.length ?? 0) > 0;
|
|
1270
|
+
if (!stale) {
|
|
1257
1271
|
this._selectedClone = existing;
|
|
1258
1272
|
return;
|
|
1259
1273
|
}
|
|
1274
|
+
}
|
|
1260
1275
|
existing?.remove();
|
|
1261
1276
|
if (value === null) {
|
|
1262
1277
|
this._selectedClone = null;
|
|
@@ -1270,6 +1285,10 @@ clone.setAttribute("slot", "selected");
|
|
|
1270
1285
|
clone.setAttribute("cloned", "true");
|
|
1271
1286
|
this.appendChild(clone);
|
|
1272
1287
|
this._selectedClone = clone;
|
|
1288
|
+
if (option.childNodes.length === 0 && document.readyState === "loading") {
|
|
1289
|
+
document.addEventListener("DOMContentLoaded", () => { if (this.isConnected)
|
|
1290
|
+
this.updateSelectionDisplay(); }, { once: true });
|
|
1291
|
+
}
|
|
1273
1292
|
}
|
|
1274
1293
|
else if (this._selectedClone?.getAttribute("value") === value) {
|
|
1275
1294
|
const restored = this._selectedClone.cloneNode(true);
|
|
@@ -29,6 +29,10 @@ if (!this.style.display)
|
|
|
29
29
|
this.style.display = 'contents';
|
|
30
30
|
this.bind();
|
|
31
31
|
this.renderChips();
|
|
32
|
+
if (document.readyState === 'loading') {
|
|
33
|
+
document.addEventListener('DOMContentLoaded', () => { if (this.isConnected)
|
|
34
|
+
this.renderChips(); }, { once: true });
|
|
35
|
+
}
|
|
32
36
|
this._childObserver = new MutationObserver(() => {
|
|
33
37
|
const template = this.template();
|
|
34
38
|
if (template !== this._lastTemplate) {
|
package/lib/components/tabs.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ensureStyles } from '../utils/styles.js';
|
|
|
2
2
|
import { tabsStyles } from '../styles/tabs.js';
|
|
3
3
|
const eventHandlers = new WeakMap();
|
|
4
4
|
const resizeObservers = new WeakMap();
|
|
5
|
+
const pendingInitialSnap = new WeakSet();
|
|
5
6
|
function getTabsAttributes(el) {
|
|
6
7
|
return {
|
|
7
8
|
width: el.getAttribute('width') || '100%',
|
|
@@ -171,17 +172,13 @@ const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
|
|
|
171
172
|
const buttonsContainer = shadowRoot.querySelector('.tab-buttons');
|
|
172
173
|
if (!button || !buttonsContainer)
|
|
173
174
|
return null;
|
|
174
|
-
const buttonRect = button.getBoundingClientRect();
|
|
175
175
|
const left = button.offsetLeft;
|
|
176
176
|
const top = button.offsetTop;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
top,
|
|
180
|
-
width: buttonRect.width,
|
|
181
|
-
height: buttonRect.height,
|
|
182
|
-
};
|
|
177
|
+
const width = button.offsetWidth;
|
|
178
|
+
const height = button.offsetHeight;
|
|
179
|
+
return { left, top, width, height };
|
|
183
180
|
}
|
|
184
|
-
function updateMarker(el, activeId) {
|
|
181
|
+
function updateMarker(el, activeId, forceSnap = false) {
|
|
185
182
|
const shadowRoot = el.shadowRoot;
|
|
186
183
|
if (!shadowRoot)
|
|
187
184
|
return;
|
|
@@ -191,7 +188,7 @@ return;
|
|
|
191
188
|
const position = calculateMarkerPosition(el, shadowRoot, activeId);
|
|
192
189
|
if (!position)
|
|
193
190
|
return;
|
|
194
|
-
const snap = !marker.style.left || marker.offsetWidth === 0;
|
|
191
|
+
const snap = forceSnap || !marker.style.left || marker.offsetWidth === 0;
|
|
195
192
|
if (snap)
|
|
196
193
|
marker.style.transition = 'none';
|
|
197
194
|
marker.style.left = `${position.left}px`;
|
|
@@ -216,7 +213,7 @@ el.style.setProperty('--active-index', String(newIndex));
|
|
|
216
213
|
updateTransform(el, newIndex);
|
|
217
214
|
updateAriaAttributes(el, shadowRoot, tabId);
|
|
218
215
|
updatePanelInteraction(el, tabId);
|
|
219
|
-
updateMarker(el, tabId);
|
|
216
|
+
updateMarker(el, tabId, pendingInitialSnap.delete(el));
|
|
220
217
|
shadowRoot.querySelectorAll('.tab-overflow-item').forEach((item) => {
|
|
221
218
|
item.setAttribute('data-active', String(item.dataset.tabId === tabId));
|
|
222
219
|
});
|
|
@@ -482,6 +479,7 @@ updatePanelInteraction(el, activeId);
|
|
|
482
479
|
}
|
|
483
480
|
}
|
|
484
481
|
else {
|
|
482
|
+
pendingInitialSnap.add(el);
|
|
485
483
|
shadowRoot.innerHTML = `
|
|
486
484
|
<div class="tabs-container" data-placement="${placement}"${fixed ? ' data-fixed' : ''}>
|
|
487
485
|
${renderTabButtons(el, tabs, activeId)}
|
package/lib/version.d.ts
CHANGED
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.0.0-
|
|
1
|
+
export const VERSION = '1.0.0-TC54';
|