Skip to content

Commit 396a2d5

Browse files
Add demos for focusgroup (#118)
* Demos for focusgroup * Remove trailing whitespace delete. * Formatting. * Fix menu example, formatting. * Update focusgroup/README.md * remove unnecessary file. --------- Co-authored-by: Patrick Brosset <patrickbrosset@gmail.com>
1 parent 239d30f commit 396a2d5

File tree

16 files changed

+3037
-0
lines changed

16 files changed

+3037
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ sync'd July 30, 2025
167167
| CSS Custom Highlight API | How to programmatically create and remove custom highlights on a web page. | [/custom-highlight-api/](https://github.com/MicrosoftEdge/Demos/tree/main/custom-highlight-api) | [CSS Custom Highlight API demo](https://microsoftedge.github.io/Demos/custom-highlight-api/) |
168168
| EditContext API demo | Demo page showing how the EditContext API can be used to build an advanced text editor. | [/edit-context/](https://github.com/MicrosoftEdge/Demos/tree/main/edit-context) | [HTML editor demo](https://microsoftedge.github.io/Demos/edit-context/) |
169169
| EyeDropper API | How to use the EyeDropper API to create a color sampling tool from JavaScript. | [/eyedropper/](https://github.com/MicrosoftEdge/Demos/tree/main/eyedropper) | [EyeDropper API demos](https://microsoftedge.github.io/Demos/eyedropper/) |
170+
| Focusgroup demos | Interactive demos for the HTML `focusgroup` attribute for declarative arrow-key keyboard navigation in composite widgets. | [/focusgroup/](https://github.com/MicrosoftEdge/Demos/tree/main/focusgroup) | [Focusgroup demos](https://microsoftedge.github.io/Demos/focusgroup/) |
170171
| IndexedDB: `getAllRecords()` | Shows the benefits of the proposed `getAllRecords()` IndexedDB method to more conveniently and quickly read IDB records. | [/idb-getallrecords/](https://github.com/MicrosoftEdge/Demos/tree/main/idb-getallrecords) | [IndexedDB: getAllRecords()](https://microsoftedge.github.io/Demos/idb-getallrecords/) demo |
171172
| Notifications demo | Using incoming call notifications. | [/incoming-call-notifications/](https://github.com/MicrosoftEdge/Demos/tree/main/incoming-call-notifications) | [Notifications demo](https://microsoftedge.github.io/Demos/incoming-call-notifications/) |
172173
| JSON dummy data | Simple JSON files. Used for [View a JSON file or server response with formatting](https://learn.microsoft.com/microsoft-edge/web-platform/json-viewer). | [/json-dummy-data/](https://github.com/MicrosoftEdge/Demos/tree/main/json-dummy-data) | [JSON dummy data](https://microsoftedge.github.io/Demos/json-dummy-data/) (Readme) |

focusgroup/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Focusgroup demos
2+
3+
➡️ **[Open the demo](https://microsoftedge.github.io/Demos/focusgroup/)** ⬅️
4+
5+
Interactive demos for the HTML `focusgroup` attribute, which provides declarative
6+
arrow-key keyboard navigation for composite widgets by implementing the _roving tabindex_ pattern natively without JavaScript.
7+
8+
## Demos
9+
10+
- [Toolbar](https://microsoftedge.github.io/Demos/focusgroup/toolbar.html) — Horizontal/vertical toolbar with arrow-key navigation
11+
- [Tablist](https://microsoftedge.github.io/Demos/focusgroup/tablist.html) — Tab control with inline wrapping and no-memory
12+
- [Menu](https://microsoftedge.github.io/Demos/focusgroup/menu.html) — Vertical menu and menubar with nested submenus
13+
- [Radio Group](https://microsoftedge.github.io/Demos/focusgroup/radiogroup.html) — Radio button group navigation
14+
- [Listbox](https://microsoftedge.github.io/Demos/focusgroup/listbox.html) — Selectable list navigation
15+
- [Accordion](https://microsoftedge.github.io/Demos/focusgroup/accordion.html) — Accordion with block-axis navigation and opt-out panels
16+
- [Additional Concepts](https://microsoftedge.github.io/Demos/focusgroup/additional-concepts.html) — Nested focusgroups, opt-out, shadow DOM, reading-flow
17+
18+
## Learn more
19+
20+
- [Focusgroup Explainer (Open UI)](https://open-ui.org/components/scoped-focusgroup.explainer/)
21+
- [ARIA Authoring Practices Guide (APG)](https://www.w3.org/WAI/ARIA/apg/)
22+
23+
## Requirements
24+
25+
May require enabling the **Experimental Web Platform features** flag at `about://flags` in Microsoft Edge or another Chromium-based browser.

focusgroup/accordion.html

Lines changed: 388 additions & 0 deletions
Large diffs are not rendered by default.

focusgroup/accordion.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/* ============================================================
2+
Focusgroup Demo — Accordion
3+
============================================================
4+
focusgroup handles arrow-key navigation between headers.
5+
This file handles the remaining application logic:
6+
- Toggling aria-expanded and the hidden attribute on panels
7+
8+
For accordions with the "accordion-focus-into" class,
9+
expanding a section also moves focus into the panel. This
10+
mitigates the risk of long panel content being skipped
11+
when arrow keys jump between headers. The panel is a
12+
focusable scrollable region (tabindex="0") with
13+
focusgroup="none", so arrow keys scroll its content.
14+
15+
Requires shared.js to be loaded first.
16+
============================================================ */
17+
18+
(function () {
19+
"use strict";
20+
21+
function initAccordions() {
22+
document.querySelectorAll(".accordion").forEach(function (accordion) {
23+
var focusInto = accordion.classList.contains("accordion-focus-into");
24+
25+
accordion.querySelectorAll("h3 button[aria-controls]").forEach(function (btn) {
26+
btn.addEventListener("click", function () {
27+
var expanded = btn.getAttribute("aria-expanded") === "true";
28+
btn.setAttribute("aria-expanded", String(!expanded));
29+
btn.classList.toggle("expanded", !expanded);
30+
31+
var panel = document.getElementById(btn.getAttribute("aria-controls"));
32+
if (panel) {
33+
panel.hidden = expanded;
34+
35+
// Mitigation: move focus into the panel on expand
36+
// so arrow keys scroll its content instead of
37+
// jumping to the next accordion header.
38+
if (!expanded && focusInto) {
39+
panel.focus();
40+
}
41+
}
42+
});
43+
});
44+
});
45+
}
46+
47+
document.addEventListener("DOMContentLoaded", function () {
48+
initAccordions();
49+
});
50+
})();
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<title>Focusgroup: Additional Concepts</title>
9+
<link rel="icon" type="image/png" href="https://edgestatic.azureedge.net/welcome/static/favicon.png">
10+
<link rel="stylesheet" href="style.css">
11+
</head>
12+
13+
<body>
14+
<header class="page-header">
15+
<h1>Additional Concepts</h1>
16+
<p>Nested focusgroups, opt-out, deep descendants, reading-flow, and
17+
feature detection.</p>
18+
<nav><a href="index.html">← All Demos</a></nav>
19+
</header>
20+
21+
<main class="page-content">
22+
23+
<!-- ============================================================ -->
24+
<!-- Demo 1: Nested Focusgroups -->
25+
<!-- ============================================================ -->
26+
<section class="demo-section">
27+
<h2>Nested Focusgroups</h2>
28+
<p>
29+
A nested focusgroup creates an independent navigation scope
30+
within an outer focusgroup.
31+
Each group has its own axis, wrapping, memory, and entry point.
32+
<kbd>Tab</kbd> moves between groups;
33+
arrows navigate within.
34+
</p>
35+
<div class="attr-label">focusgroup="toolbar inline" (outer) +
36+
focusgroup="toolbar inline wrap" (inner)</div>
37+
38+
<div class="try-it">
39+
<strong>Try it:</strong> Use <kbd></kbd> in the outer toolbar.
40+
When focus reaches the nested group, press <kbd>Tab</kbd>
41+
to enter it. Use arrows inside, then <kbd>Tab</kbd> to exit
42+
back to the outer toolbar.
43+
</div>
44+
45+
<div class="demo-container">
46+
<div focusgroup="toolbar inline" aria-label="Main toolbar" class="toolbar-horizontal">
47+
<button type="button">Save</button>
48+
<button type="button" focusgroupstart>Print</button>
49+
<div focusgroup="toolbar inline wrap" aria-label="Text formatting" class="nested-toolbar">
50+
<button type="button">Bold</button>
51+
<button type="button">Italic</button>
52+
<button type="button">Underline</button>
53+
</div>
54+
<button type="button">Close</button>
55+
<button type="button">Exit</button>
56+
</div>
57+
</div>
58+
59+
<ul class="notice-list">
60+
<li>The nested toolbar is an <strong>independent scope</strong>
61+
inside the outer toolbar</li>
62+
<li><kbd>Tab</kbd> moves between the outer and inner
63+
focusgroups</li>
64+
<li>Arrow keys navigate within whichever group currently has
65+
focus</li>
66+
<li>Each group can have its own wrapping/memory/axis
67+
settings</li>
68+
</ul>
69+
70+
<details class="source-code">
71+
<summary>View source</summary>
72+
<pre><code>&lt;div focusgroup="toolbar inline" aria-label="Main
73+
toolbar"&gt;
74+
&lt;button type="button"&gt;Save&lt;/button&gt;
75+
&lt;button type="button" focusgroupstart&gt;Print&lt;/button&gt;
76+
&lt;div focusgroup="toolbar inline wrap" aria-label="Text formatting"&gt;
77+
&lt;button type="button"&gt;Bold&lt;/button&gt;
78+
&lt;button type="button"&gt;Italic&lt;/button&gt;
79+
&lt;button type="button"&gt;Underline&lt;/button&gt;
80+
&lt;/div&gt;
81+
&lt;button type="button"&gt;Close&lt;/button&gt;
82+
&lt;button type="button"&gt;Exit&lt;/button&gt;
83+
&lt;/div&gt;</code></pre>
84+
</details>
85+
</section>
86+
87+
<!-- ============================================================ -->
88+
<!-- Demo 2: Opt-out with focusgroup="none" -->
89+
<!-- ============================================================ -->
90+
<section class="demo-section">
91+
<h2>Opt-Out Segments with <code>focusgroup="none"</code></h2>
92+
<p>
93+
Use <code>focusgroup="none"</code> to exclude specific elements
94+
from arrow navigation
95+
while keeping them tabbable. Arrow keys skip right over them.
96+
</p>
97+
<div class="attr-label">focusgroup="toolbar inline"</div>
98+
99+
<div class="try-it">
100+
<strong>Try it:</strong> Use <kbd></kbd> to navigate through
101+
the toolbar. Notice that "Help" and "Shortcuts" are
102+
<strong>skipped</strong> by arrow keys but still reachable
103+
via <kbd>Tab</kbd>.
104+
</div>
105+
106+
<div class="demo-container">
107+
<div focusgroup="toolbar inline" aria-label="Segmented toolbar" class="toolbar-horizontal">
108+
<button type="button">New</button>
109+
<button type="button">Open</button>
110+
<button type="button">Save</button>
111+
<span focusgroup="none" style="display: contents;">
112+
<button type="button" style="opacity: 0.6;">Help</button>
113+
<button type="button" style="opacity: 0.6;">Shortcuts</button>
114+
</span>
115+
<button type="button">Close</button>
116+
<button type="button">Exit</button>
117+
</div>
118+
</div>
119+
120+
<ul class="notice-list">
121+
<li>Arrow keys navigate: New → Open → Save → Close → Exit
122+
(skipping Help &amp; Shortcuts)</li>
123+
<li><kbd>Tab</kbd> still reaches Help and Shortcuts</li>
124+
<li>The dimmed buttons visually indicate they're in a
125+
<code>focusgroup="none"</code> zone
126+
</li>
127+
</ul>
128+
129+
<details class="source-code">
130+
<summary>View source</summary>
131+
<pre><code>&lt;div focusgroup="toolbar inline"
132+
aria-label="Segmented toolbar"&gt;
133+
&lt;button type="button"&gt;New&lt;/button&gt;
134+
&lt;button type="button"&gt;Open&lt;/button&gt;
135+
&lt;button type="button"&gt;Save&lt;/button&gt;
136+
&lt;span focusgroup="none"&gt;
137+
&lt;button type="button"&gt;Help&lt;/button&gt;
138+
&lt;button type="button"&gt;Shortcuts&lt;/button&gt;
139+
&lt;/span&gt;
140+
&lt;button type="button"&gt;Close&lt;/button&gt;
141+
&lt;button type="button"&gt;Exit&lt;/button&gt;
142+
&lt;/div&gt;</code></pre>
143+
</details>
144+
</section>
145+
146+
<!-- ============================================================ -->
147+
<!-- Demo 3: Deep Descendant Discovery -->
148+
<!-- ============================================================ -->
149+
<section class="demo-section">
150+
<h2>Deep Descendant Discovery</h2>
151+
<p>
152+
Focusgroup items don't need to be direct children. The browser
153+
discovers focusable
154+
descendants at any depth (unless they are inside a nested
155+
focusgroup or <code>focusgroup="none"</code>).
156+
</p>
157+
<div class="attr-label">focusgroup="toolbar inline"</div>
158+
159+
<div class="try-it">
160+
<strong>Try it:</strong> Use <kbd></kbd> <kbd></kbd> — arrow
161+
navigation works even though buttons are deeply nested
162+
inside <code>&lt;div&gt;</code> and
163+
<code>&lt;span&gt;</code> wrappers.
164+
</div>
165+
166+
<div class="demo-container">
167+
<div focusgroup="toolbar inline" aria-label="Nested wrappers" class="toolbar-horizontal">
168+
<div>
169+
<span><button type="button">Alpha</button></span>
170+
<span><button type="button">Beta</button></span>
171+
<span><button type="button">Gamma</button></span>
172+
</div>
173+
</div>
174+
</div>
175+
176+
<ul class="notice-list">
177+
<li>Buttons are nested inside
178+
<code>&lt;div&gt;&lt;span&gt;</code> wrappers
179+
</li>
180+
<li>Focusgroup discovers them at any depth in the DOM tree</li>
181+
<li>No flat list requirement — wrapper elements for styling are
182+
fine</li>
183+
</ul>
184+
185+
<details class="source-code">
186+
<summary>View source</summary>
187+
<pre><code>&lt;div focusgroup="toolbar inline"
188+
aria-label="Nested wrappers"&gt;
189+
&lt;div&gt;
190+
&lt;span&gt;&lt;button
191+
type="button"&gt;Alpha&lt;/button&gt;&lt;/span&gt;
192+
&lt;span&gt;&lt;button type="button"&gt;Beta&lt;/button&gt;&lt;/span&gt;
193+
&lt;span&gt;&lt;button
194+
type="button"&gt;Gamma&lt;/button&gt;&lt;/span&gt;
195+
&lt;/div&gt;
196+
&lt;/div&gt;</code></pre>
197+
</details>
198+
</section>
199+
200+
<!-- ============================================================ -->
201+
<!-- Demo 4: CSS reading-flow Integration -->
202+
<!-- ============================================================ -->
203+
<section class="demo-section">
204+
<h2>CSS <code>reading-flow</code> Integration</h2>
205+
<p>
206+
When CSS changes the visual order (e.g., <code>flex-direction:
207+
row-reverse</code>),
208+
<code>reading-flow: flex-visual</code> tells the focusgroup to
209+
follow the <strong>visual</strong>
210+
order rather than the DOM source order.
211+
</p>
212+
<div class="attr-label">focusgroup="toolbar" + reading-flow:
213+
flex-visual</div>
214+
215+
<div class="try-it">
216+
<strong>Try it:</strong> Use <kbd></kbd> to navigate. With
217+
<code>reading-flow: flex-visual</code>, arrow navigation
218+
follows the visual left-to-right order (C → B → A), not the
219+
DOM order (A → B → C).
220+
</div>
221+
222+
<div class="demo-container">
223+
<div focusgroup="toolbar" aria-label="Visual order" style="display: flex; flex-direction: row-reverse;
224+
reading-flow: flex-visual; gap: 0.35rem;">
225+
<button type="button">A (DOM first)</button>
226+
<button type="button">B (DOM second)</button>
227+
<button type="button">C (DOM third)</button>
228+
</div>
229+
</div>
230+
231+
<ul class="notice-list">
232+
<li>DOM order: A, B, C — but <code>flex-direction:
233+
row-reverse</code> visually renders C, B, A</li>
234+
<li><code>reading-flow: flex-visual</code> tells focusgroup to
235+
follow visual order</li>
236+
<li>Right arrow from C goes to B, then A (visual
237+
left-to-right)</li>
238+
<li>Without <code>reading-flow</code>, arrow keys would follow
239+
DOM order (A → B → C), which is visually backwards</li>
240+
</ul>
241+
242+
<details class="source-code">
243+
<summary>View source</summary>
244+
<pre><code>&lt;div focusgroup="toolbar" aria-label="Visual
245+
order"
246+
style="display: flex; flex-direction: row-reverse;
247+
reading-flow: flex-visual;"&gt;
248+
&lt;button type="button"&gt;A (DOM first)&lt;/button&gt;
249+
&lt;button type="button"&gt;B (DOM second)&lt;/button&gt;
250+
&lt;button type="button"&gt;C (DOM third)&lt;/button&gt;
251+
&lt;/div&gt;</code></pre>
252+
</details>
253+
</section>
254+
255+
<!-- ============================================================ -->
256+
<!-- Demo 5: Feature Detection -->
257+
<!-- ============================================================ -->
258+
<section class="demo-section">
259+
<h2>Feature Detection</h2>
260+
<p>
261+
Check whether the browser supports <code>focusgroup</code>
262+
before relying on it.
263+
If unsupported, you can fall back to a JavaScript-based roving
264+
tabindex or show a notice.
265+
</p>
266+
267+
<div class="demo-container">
268+
<div id="feature-detect-result" style="padding: 0.75rem; border-radius: 4px;"></div>
269+
</div>
270+
271+
<details class="source-code">
272+
<summary>View source</summary>
273+
<pre><code>&lt;script&gt;
274+
if ('focusgroup' in HTMLElement.prototype) {
275+
// focusgroup is supported — use it!
276+
console.log('focusgroup is supported');
277+
} else {
278+
// Not supported — fall back to JS roving tabindex
279+
console.log('focusgroup is NOT supported');
280+
}
281+
&lt;/script&gt;</code></pre>
282+
</details>
283+
</section>
284+
285+
</main>
286+
287+
<script src="shared.js"></script>
288+
<script>
289+
// Live feature detection result
290+
(function () {
291+
var result = document.getElementById("feature-detect-result");
292+
if (!result) return;
293+
if ("focusgroup" in HTMLElement.prototype) {
294+
result.textContent = "✅ focusgroup IS supported in this
295+
browser.";
296+
result.style.background = "var(--accent-light)";
297+
result.style.color = "var(--success)";
298+
} else {
299+
result.textContent = "❌ focusgroup is NOT supported in this
300+
browser.Enable experimental flags to try these demos.";
301+
result.style.background = "var(--warning-bg)";
302+
result.style.color = "var(--warning-text)";
303+
}
304+
})();
305+
</script>
306+
</body>
307+
308+
</html>

0 commit comments

Comments
 (0)