-
Notifications
You must be signed in to change notification settings - Fork 0
Add keyboard accessibility demo page #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 & 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> |
| 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); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| 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 || ""; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copy textarea gets initial content, not user inputThe expression
Comment on lines
+113
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In 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); | ||
There was a problem hiding this comment.
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-randombutton, thekeydownevent fires the button's handler callingupdateKnob, then bubbles up to the parent.knobdiv which also has akeydownlistener, causingupdateKnobto be called a second time. The handler lacksevent.stopPropagation()to prevent this double execution.