Skip to content

Commit 7f940d7

Browse files
Address review: focus, invalid wrapper cleanup, observer, docs, demo
- Avoid stealing focus on load and on global arrow-key navigation; drop autofocus from connectedCallback, update(), and _move(). - When the tool is invalid (<2 options), clear wrapper attrs and unhide option roots; track hidden/display writes in hideExcludedOptions for the observer. - Resync when data-roid-tool or data-roid-option attributes change even if removed; keep hidden filtered to Roid nodes. - Cache document.fonts.ready at module load; clarify SKILL.txt tooltip and finalization attrs; hide inactive demo variants until JS runs. Made-with: Cursor
1 parent e268592 commit 7f940d7

3 files changed

Lines changed: 48 additions & 23 deletions

File tree

SKILL.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Use exactly one preview wrapper per page:
116116

117117
| Input | Required | Meaning |
118118
|---|---|---|
119-
| `data-roid-tool` | Yes | Short label for the decision surface (shown as a hint on the position counter) |
119+
| `data-roid-tool` | Yes | Short label for the decision surface (exposed as the **tooltip** on the index counter and in the bar’s `aria-label`; omitting it falls back to a default) |
120120
| `data-roid-option` | Yes, 2+ siblings | One **direct child** per variant; attribute value = label in Roids |
121121

122122
### Rules
@@ -193,7 +193,7 @@ When the user picks a winner:
193193
1. Keep only the chosen layout direction.
194194
2. Remove all unused layout options.
195195
3. Remove `<script src="https://tryroids.com/roid-tool.js"></script>`.
196-
4. Remove `data-roid-tool`, `data-roid-option`, and `data-roid-active` unless the user explicitly wants them preserved.
196+
4. Remove `data-roid-tool`, `data-roid-option`, `data-roid-active`, and runtime-written wrapper state (`data-roid-active-option`, `data-roid-active-option-index`) unless the user explicitly wants them preserved.
197197
5. Remove preview-only labels, wrappers, comments, and temporary styles.
198198
6. Convert the winning result into normal production code.
199199

demo.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,17 @@
4747
<main
4848
data-roid-tool="Demo section"
4949
>
50-
<article class="a" data-roid-option="A — Indigo stack">
50+
<article class="a" data-roid-option="A — Indigo stack" data-roid-active>
5151
<h1>Variant A</h1>
5252
<p>Indigo / violet mood. Your content would go here.</p>
5353
</article>
5454

55-
<article class="b" data-roid-option="B — Forest card">
55+
<article class="b" data-roid-option="B — Forest card" hidden>
5656
<h1>Variant B</h1>
5757
<p>Green / natural mood. Same slot, different direction.</p>
5858
</article>
5959

60-
<article class="c" data-roid-option="C — Rose panel">
60+
<article class="c" data-roid-option="C — Rose panel" hidden>
6161
<h1>Variant C</h1>
6262
<p>Rose / warm accent. Pick a winner and ship one layout.</p>
6363
</article>

roid-tool.js

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
const trim = (v, fb) => (v ?? "").trim() || fb;
1717

18+
const fontsReadyPromise =
19+
typeof document !== "undefined" ? document.fonts?.ready : null;
20+
1821
/** True when focus is in a control that should keep Left/Right for itself. */
1922
function isArrowKeyReservedForTarget(el) {
2023
if (!(el instanceof Element)) return false;
@@ -87,14 +90,38 @@
8790
}
8891

8992
/** Hide direct children with data-roid-option that are not in the active list. */
90-
function hideExcludedOptions(toolEl, options) {
93+
function hideExcludedOptions(toolEl, options, tracked) {
9194
const keep = new Set(options.map((o) => o.element));
9295
for (const child of toolEl.children) {
9396
if (!child.hasAttribute("data-roid-option")) continue;
9497
if (keep.has(child)) continue;
95-
child.hidden = true;
9698
child.removeAttribute("data-roid-active");
97-
if (child.style.display !== "none") child.style.display = "none";
99+
if (!child.hidden) {
100+
tracked?.add(child);
101+
child.hidden = true;
102+
}
103+
if (child.style.display !== "none") {
104+
tracked?.add(child);
105+
child.style.display = "none";
106+
}
107+
}
108+
}
109+
110+
/** When the wrapper exists but has fewer than MIN_OPTIONS, clear bar state and undo hiding. */
111+
function resetInvalidWrapper() {
112+
const toolEl = document.querySelector(WRAPPER_SEL);
113+
if (!toolEl) return;
114+
const raw = Array.from(toolEl.children).filter((el) =>
115+
el.hasAttribute("data-roid-option")
116+
);
117+
if (raw.length >= MIN_OPTIONS) return;
118+
toolEl.removeAttribute("data-roid-active-option");
119+
toolEl.removeAttribute("data-roid-active-option-index");
120+
for (const child of toolEl.children) {
121+
if (!child.hasAttribute("data-roid-option")) continue;
122+
child.removeAttribute("data-roid-active");
123+
child.hidden = false;
124+
child.style.removeProperty("display");
98125
}
99126
}
100127

@@ -342,7 +369,6 @@
342369
connectedCallback() {
343370
document.addEventListener("keydown", this.onDocumentKeyDown, true);
344371
if (!this.dialog.open) this.dialog.show();
345-
this._focusDialog();
346372
}
347373

348374
disconnectedCallback() {
@@ -399,19 +425,12 @@
399425
}
400426
}
401427

402-
_focusDialog() {
403-
const ae = this.root.activeElement;
404-
if (ae instanceof HTMLElement && ae !== this.dialog) ae.blur();
405-
this.dialog.focus({ preventScroll: true });
406-
}
407-
408428
update(group, onSelect) {
409429
this.group = group;
410430
this.onSelect = onSelect;
411431
this._syncWrapperAttrs();
412432
this._render();
413433
if (!this.dialog.open) this.dialog.show();
414-
this._focusDialog();
415434
}
416435

417436
_measureMetaMinWidth() {
@@ -447,8 +466,8 @@
447466
const multi = n > 1;
448467
this.prev.disabled = this.next.disabled = !multi;
449468
this._measureMetaMinWidth();
450-
if (typeof document !== "undefined" && document.fonts?.ready) {
451-
document.fonts.ready.then(() => {
469+
if (fontsReadyPromise) {
470+
fontsReadyPromise.then(() => {
452471
if (this.group === g) this._measureMetaMinWidth();
453472
});
454473
}
@@ -462,7 +481,6 @@
462481
const opt = g.options[nextIdx];
463482
if (opt) {
464483
this.onSelect?.(opt);
465-
this._focusDialog();
466484
}
467485
}
468486

@@ -496,12 +514,13 @@
496514
function sync() {
497515
const state = parseTool();
498516
if (!state) {
517+
resetInvalidWrapper();
499518
host?.remove();
500519
host = null;
501520
return;
502521
}
503522

504-
hideExcludedOptions(state.toolEl, state.options);
523+
hideExcludedOptions(state.toolEl, state.options, hiddenTracked);
505524
applyVisibility(
506525
state,
507526
state.options[resolveActiveIndex(state)] ?? state.options[0],
@@ -539,11 +558,17 @@
539558
) {
540559
continue;
541560
}
561+
const name = rec.attributeName;
562+
if (
563+
name === "data-roid-tool" ||
564+
name === "data-roid-option"
565+
) {
566+
return schedule();
567+
}
542568
if (
569+
name === "hidden" &&
543570
rec.target instanceof Element &&
544-
(touchesRoid(rec.target) ||
545-
rec.target.matches(WRAPPER_SEL) ||
546-
rec.target.hasAttribute("data-roid-option"))
571+
touchesRoid(rec.target)
547572
) {
548573
return schedule();
549574
}

0 commit comments

Comments
 (0)