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.
@@ -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 from tags (ALWAYS reads from DOM)
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;
@@ -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
- const allTags = this.getTagElements();
272
- const preSelectedTags = allTags
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.getTagElements()
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 (tag.hasAttribute("selected")) {
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
- stub.classList.toggle("has-clone", wantClone !== null);
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 = wantClone !== null;
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) {
@@ -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
- return {
178
- left,
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
@@ -4,4 +4,4 @@
4
4
  * This is automatically synced with package.json during build.
5
5
  * Use `npm version` to bump versions.
6
6
  */
7
- export declare const VERSION = "1.0.0-TC53";
7
+ export declare const VERSION = "1.0.0-TC54";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.0-TC53';
1
+ export const VERSION = '1.0.0-TC54';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyrell-components",
3
- "version": "1.0.0-TC53",
3
+ "version": "1.0.0-TC54",
4
4
  "description": "Tyrell Components - Framework-agnostic web components with semantic design system",
5
5
  "type": "module",
6
6
  "sideEffects": [