Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Controls Playground</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="page-header">
<h1>Accessible Controls Playground</h1>
<p>Keyboard-friendly demo for knobs, tabs, toggles, copy buttons, and ignition.</p>
</header>

<main>
<section aria-labelledby="tabs-heading" class="card">
<div class="section-header">
<h2 id="tabs-heading">Control Tabs</h2>
<p>Use left/right arrow keys or Enter/Space to switch tabs.</p>
</div>
<div role="tablist" aria-label="control tabs" class="tabs" id="control-tabs">
<button role="tab" aria-selected="true" aria-controls="tab-content-1" id="tab-1" tabindex="0" class="tab">Knobs</button>
<button role="tab" aria-selected="false" aria-controls="tab-content-2" id="tab-2" tabindex="-1" class="tab">Modes</button>
</div>
<div class="tab-panels">
<div role="tabpanel" id="tab-content-1" aria-labelledby="tab-1" class="tab-panel" tabindex="0">
<p class="muted">Focus a knob and press Enter or Space to randomize its value.</p>
<div class="knob-grid">
<div class="knob" data-knob="temperature" tabindex="0" aria-label="Temperature knob">
<div class="knob-label">Temperature</div>
<div class="knob-value" aria-live="polite">0.50</div>
<button type="button" class="knob-random">Randomize</button>
</div>
<div class="knob" data-knob="guidance" tabindex="0" aria-label="Guidance knob">
<div class="knob-label">Guidance</div>
<div class="knob-value" aria-live="polite">7.50</div>
<button type="button" class="knob-random">Randomize</button>
</div>
</div>
</div>
<div role="tabpanel" id="tab-content-2" aria-labelledby="tab-2" class="tab-panel" aria-hidden="true" tabindex="0" hidden>
<p class="muted">Toggle modes with Enter or Space. Focus styles stay visible while tabbing.</p>
<div class="toggle-row">
<button type="button" class="mode-toggle" aria-pressed="true" aria-label="Auto ignite">Auto ignite</button>
<button type="button" class="mode-toggle" aria-pressed="false" aria-label="Manual ignite">Manual ignite</button>
</div>
</div>
</div>
</section>

<section aria-labelledby="copy-heading" class="card">
<div class="section-header">
<h2 id="copy-heading">Copy &amp; Ignite</h2>
<p>All controls are keyboard operable.</p>
</div>
<div class="copy-row">
<label class="muted" for="prompt">Prompt text</label>
<textarea id="prompt" rows="3" aria-label="Prompt">A vibrant nebula with cascading stardust.</textarea>
<div class="copy-actions">
<button type="button" class="copy" data-target="#prompt">Copy prompt</button>
<button type="button" class="copy" data-target="#status">Copy status</button>
</div>
</div>
<div class="ignite-row">
<div id="status" class="status" aria-live="polite">Awaiting ignition.</div>
<button type="button" id="ignite" class="primary">Ignite</button>
</div>
</section>
</main>

<script src="script.js"></script>
</body>
</html>
171 changes: 171 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
function randomInRange(min, max, decimals = 2) {
const value = Math.random() * (max - min) + min;
return value.toFixed(decimals);
}

function updateKnob(knob) {
const knobName = knob.dataset.knob || "value";
const min = knobName === "guidance" ? 1 : 0;
const max = knobName === "guidance" ? 12 : 1;
const nextValue = randomInRange(min, max, knobName === "guidance" ? 1 : 2);
const valueEl = knob.querySelector(".knob-value");
valueEl.textContent = nextValue;
}

function handleKnobActivation(event) {
const isClick = event.type === "click";
const isKeyboard = event.type === "keydown" && (event.key === "Enter" || event.key === " ");

if (!isClick && !isKeyboard) return;
if (isKeyboard) {
event.preventDefault();
}

const knob = event.currentTarget.closest(".knob");
if (knob) {
updateKnob(knob);
}
}

function setupKnobs() {
const knobs = document.querySelectorAll(".knob");
knobs.forEach((knob) => {
knob.addEventListener("keydown", handleKnobActivation);
const randomButton = knob.querySelector(".knob-random");
if (randomButton) {
randomButton.addEventListener("click", handleKnobActivation);
randomButton.addEventListener("keydown", handleKnobActivation);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keydown event bubbles causing double knob randomization

When pressing Enter or Space on a .knob-random button, the keydown event fires the button's handler calling updateKnob, then bubbles up to the parent .knob div which also has a keydown listener, causing updateKnob to be called a second time. The handler lacks event.stopPropagation() to prevent this double execution.

Fix in Cursor Fix in Web

}
});
}

function setActiveTab(nextTab) {
const tablist = document.getElementById("control-tabs");
const tabs = tablist.querySelectorAll("[role='tab']");
const panels = document.querySelectorAll("[role='tabpanel']");

tabs.forEach((tab) => {
const isActive = tab === nextTab;
tab.setAttribute("aria-selected", isActive);
tab.tabIndex = isActive ? 0 : -1;
});

panels.forEach((panel) => {
const isActive = panel.id === nextTab.getAttribute("aria-controls");
panel.toggleAttribute("hidden", !isActive);
panel.setAttribute("aria-hidden", (!isActive).toString());
});

nextTab.focus();
}

function setupTabs() {
const tablist = document.getElementById("control-tabs");
const tabs = tablist.querySelectorAll("[role='tab']");

tabs.forEach((tab) => {
tab.addEventListener("click", () => setActiveTab(tab));
tab.addEventListener("keydown", (event) => {
const { key } = event;
const isActivation = key === "Enter" || key === " ";
const isPrev = key === "ArrowLeft" || key === "ArrowUp";
const isNext = key === "ArrowRight" || key === "ArrowDown";

if (isActivation) {
event.preventDefault();
setActiveTab(tab);
return;
}

if (!isPrev && !isNext) return;
event.preventDefault();
const tabArray = Array.from(tabs);
const currentIndex = tabArray.indexOf(tab);
const targetIndex = (tabArray.length + currentIndex + (isNext ? 1 : -1)) % tabArray.length;
setActiveTab(tabArray[targetIndex]);
});
});
}

function toggleMode(button) {
const toggles = document.querySelectorAll(".mode-toggle");
toggles.forEach((toggle) => {
toggle.setAttribute("aria-pressed", (toggle === button).toString());
});
}

function setupModes() {
const toggles = document.querySelectorAll(".mode-toggle");
toggles.forEach((toggle) => {
toggle.addEventListener("click", () => toggleMode(toggle));
toggle.addEventListener("keydown", (event) => {
const isActivation = event.key === "Enter" || event.key === " ";
if (!isActivation) return;
event.preventDefault();
toggleMode(toggle);
});
});
}

async function copyFromTarget(button) {
const targetSelector = button.dataset.target;
if (!targetSelector) return;
const target = document.querySelector(targetSelector);
if (!target) return;
const text = target.textContent || target.value || "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy textarea gets initial content, not user input

The expression target.textContent || target.value checks textContent first, but for textarea elements, textContent returns the original HTML content between the tags, not the current user-entered value. Since the prompt textarea has initial content, textContent is always truthy, so target.value is never evaluated. This causes the "Copy prompt" button to copy the original text instead of any edits the user made.

Fix in Cursor Fix in Web

Comment on lines +113 to +115
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Copy prompt copies stale default text

In copyFromTarget (script.js lines 113‑115), the text to copy is taken from target.textContent || target.value, so the textarea’s initial text node is always used instead of the current value. After a user edits the prompt and clicks “Copy prompt”, the clipboard still receives the original default prompt because textContent on a <textarea> does not update with user input. This breaks the copy feature whenever the prompt is modified before copying.

Useful? React with 👍 / 👎.

try {
await navigator.clipboard.writeText(text);
button.textContent = "Copied!";
setTimeout(() => (button.textContent = "Copy" + (button.dataset.target.includes("prompt") ? " prompt" : " status")), 1200);
} catch (error) {
console.error("Copy failed", error);
button.textContent = "Copy failed";
}
}

function setupCopyButtons() {
const copyButtons = document.querySelectorAll(".copy");
copyButtons.forEach((button) => {
button.addEventListener("click", () => copyFromTarget(button));
button.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
copyFromTarget(button);
}
});
});
}

function setupIgnite() {
const igniteButton = document.getElementById("ignite");
const status = document.getElementById("status");

const ignite = () => {
status.textContent = "Igniting sequence...";
igniteButton.disabled = true;
igniteButton.textContent = "Igniting";
setTimeout(() => {
status.textContent = "Ignition complete. Ready to launch!";
igniteButton.disabled = false;
igniteButton.textContent = "Ignite";
}, 1200);
};

igniteButton.addEventListener("click", ignite);
igniteButton.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
ignite();
}
});
}

function init() {
setupKnobs();
setupTabs();
setupModes();
setupCopyButtons();
setupIgnite();
}

document.addEventListener("DOMContentLoaded", init);
Loading