fix(showcase): break wn-breadcrumb slotchange infinite loop (WHYNOT-WP-0002 T11)
Some checks failed
ci / check (push) Has been cancelled
ci / release (push) Has been cancelled

WnBreadcrumb._onSlot inserted separator <span>s into its own light DOM on
slotchange but cleaned up in the shadow DOM, so they were never removed — each
insertion re-fired slotchange, looping the main thread and wedging the showcase
page. Made _onSlot idempotent: exclude own separators when reading items, and
mutate only when separators are not already correct.

- Un-fixme the showcase visual test; add a warm-up full-page capture so
  deviceScaleFactor-2 sub-pixel snapping settles before the assertion. All 5
  visual tests pass.
- Remove the dead Google-Fonts @import from colors_and_type.css (token stacks are
  system-ui; webfont unused + a CI-flake source; no visual change).
- Unblocks WHYNOT-WP-0003 T08 (showcase = per-version visual catalog); both T11
  and T08 marked done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 20:10:41 +02:00
parent 76e516f6d9
commit a89bb563a0
6 changed files with 79 additions and 24 deletions

View File

@@ -236,11 +236,23 @@ export class WnEmptyState extends WnBase {
export class WnBreadcrumb extends WnBase {
_onSlot(e) {
const slot = e.target;
const items = slot.assignedElements({ flatten: true });
// Build the rendered tree: each item + a separator after it.
const wrapper = this.shadowRoot?.querySelector('.wn-breadcrumb__list');
if (!wrapper) return;
wrapper.querySelectorAll('.wn-breadcrumb__sep').forEach(s => s.remove());
// Separators are inserted into the LIGHT DOM (so they sit in document order
// between the slotted items), which re-fires this slotchange. We must
// therefore be idempotent: exclude our own separators when reading items,
// and skip all mutation once the separators are already correct — otherwise
// each insertion retriggers slotchange and the main thread loops forever.
const items = slot.assignedElements({ flatten: true })
.filter((el) => !el.classList.contains("wn-breadcrumb__sep"));
const existing = [...this.querySelectorAll(":scope > .wn-breadcrumb__sep")];
if (existing.length === Math.max(0, items.length - 1)) {
// Structure already correct — only refresh the "current" marker, do not
// touch the child list (no mutation ⇒ no slotchange re-fire ⇒ loop ends).
items.forEach((el, i) => el.classList.toggle("wn-breadcrumb__current", i === items.length - 1));
return;
}
existing.forEach((s) => s.remove());
items.forEach((el, i) => {
el.classList.toggle("wn-breadcrumb__current", i === items.length - 1);
if (i > 0) {
@@ -248,8 +260,6 @@ export class WnBreadcrumb extends WnBase {
sep.className = "wn-breadcrumb__sep";
sep.setAttribute("aria-hidden", "true");
sep.textContent = "/";
// Use light-DOM-relative insertion: items are still in light DOM,
// so DOM-order separators between them belong in light DOM too.
el.parentNode.insertBefore(sep, el);
}
});