Skip to content

Commit db6a956

Browse files
authored
feat: add chatbot example to focusgroup examples (#124)
1 parent 6d8c936 commit db6a956

File tree

5 files changed

+324
-8
lines changed

5 files changed

+324
-8
lines changed

focusgroup/chatbot/chat.css

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* Message list */
2+
[role=feed] {
3+
display: flex;
4+
flex-direction: column;
5+
gap: 1rem;
6+
}
7+
8+
.message {
9+
position:relative;
10+
width: fit-content;
11+
max-width: 80%;
12+
padding: 0.5rem 1rem;
13+
border: 1px solid var(--border);
14+
border-radius: 4px;
15+
}
16+
17+
.catbot {
18+
align-self: flex-start;
19+
background: var(--hover-bg);
20+
color: var(--text);
21+
}
22+
23+
.user {
24+
align-self: flex-end;
25+
background: var(--selected-bg);
26+
color: var(--selected-text);
27+
}
28+
29+
/* Message action toolbar */
30+
.message-actions {
31+
display: none;
32+
position: absolute;
33+
top: 0;
34+
right: 0.5rem;
35+
transform: translateY(-80%);
36+
background-color: var(--bg);
37+
padding: 0.25rem;
38+
border-radius: 6px;
39+
}
40+
41+
.message:focus-within .message-actions,
42+
.message:hover .message-actions {
43+
display: flex;
44+
gap: 2px;
45+
align-items: center;
46+
}
47+
48+
/* Chat input */
49+
.chat-form {
50+
display: flex;
51+
gap: 0.25rem;
52+
align-items: stretch;
53+
margin-top: 1rem;
54+
}
55+
56+
.chat-form input {
57+
flex: 1 1 auto;
58+
padding: 0.25rem 0.5rem;
59+
border: 1px solid var(--border);
60+
border-radius: 4px;
61+
}
62+
63+
.chat-form button[type=submit] {
64+
flex: 0 0 auto;
65+
}

focusgroup/chatbot/chat.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"use strict";
2+
3+
import ChatResponses from "./messages.js";
4+
5+
/* ============================================================
6+
Focusgroup Demo - Chatbot
7+
============================================================
8+
focusgroup handles arrow-key navigation between messages and toolbar buttons.
9+
This file handles the remaining application logic:
10+
- Sending random cat facts in response to user messages
11+
- Manual "memory" using focusgroupstart
12+
13+
Requires shared.js to be loaded first.
14+
============================================================ */
15+
16+
// Short fake delay between sending a message and receiving a response
17+
const RESPONSE_DELAY = 500;
18+
19+
// Simple unique ID suffix for messages
20+
let ID_INDEX = 1;
21+
22+
const renderMessageToolbar = () => {
23+
const toolbarEl = document.createElement("div");
24+
toolbarEl.classList.add("message-actions");
25+
toolbarEl.setAttribute('focusgroup', 'toolbar nomemory');
26+
27+
const actions = ["Copy", "Like", "Dislike", "Share"];
28+
actions.forEach(action => {
29+
const actionEl = document.createElement("button");
30+
actionEl.textContent = action;
31+
toolbarEl.appendChild(actionEl);
32+
});
33+
34+
return toolbarEl;
35+
}
36+
37+
const renderMessage = (message, isBotMessage) => {
38+
const messageEl = document.createElement("article");
39+
messageEl.textContent = message;
40+
messageEl.classList.add("message", isBotMessage ? "catbot" : "user");
41+
messageEl.tabIndex = 0;
42+
43+
// Using a self-referencing aria-labelledby to force messages to be
44+
// explicitly labeled by their inner text content
45+
// This is needed to support Windows screen reader users arrowing in forms/focus mode
46+
messageEl.id = `message-${ID_INDEX++}`;
47+
messageEl.setAttribute('aria-labelledby', messageEl.id);
48+
49+
// Add an actions toolbar and update focusgroupstart to the latest message for bot messages
50+
if (isBotMessage) {
51+
const messageToolbar = renderMessageToolbar();
52+
messageEl.prepend(messageToolbar);
53+
54+
const previousStart = document.querySelector("[focusgroupstart]");
55+
if (previousStart) {
56+
previousStart.removeAttribute("focusgroupstart");
57+
}
58+
messageEl.setAttribute("focusgroupstart", "");
59+
}
60+
61+
return messageEl;
62+
};
63+
64+
const postMessage = (message, response) => {
65+
const messageList = document.getElementById("message-list");
66+
const userMessage = renderMessage(message, false);
67+
messageList.appendChild(userMessage);
68+
69+
window.setTimeout(() => {
70+
const botMessage = renderMessage(response, true);
71+
messageList.appendChild(botMessage);
72+
73+
// screen reader notification that reads the text of the bot response
74+
botMessage.ariaNotify(message);
75+
}, RESPONSE_DELAY);
76+
};
77+
78+
// pick a random item from the ChatResponses, excluding previously used messages
79+
const getRandomCatFact = (excludeMessages) => {
80+
if (excludeMessages.length >= ChatResponses.length) {
81+
excludeMessages.length = 0; // reset if we've used all messages
82+
}
83+
const availableMessages = ChatResponses.filter(msg => !excludeMessages.includes(msg));
84+
const randomCatFact = availableMessages[Math.floor(Math.random() * availableMessages.length)];
85+
excludeMessages.push(randomCatFact);
86+
return randomCatFact;
87+
}
88+
89+
const onMessageFocusChange = (event) => {
90+
if (event.target.classList.contains("message")) {
91+
const previousStart = document.querySelector("[focusgroupstart]");
92+
if (previousStart) {
93+
previousStart.removeAttribute("focusgroupstart");
94+
}
95+
96+
event.target.setAttribute("focusgroupstart", "");
97+
}
98+
};
99+
100+
const initChat = () => {
101+
// prevent duplicate responses until all have been used
102+
const usedMessages = [];
103+
104+
// attach submit event to chat input
105+
const chatInputForm = document.getElementById("chatinput");
106+
chatInputForm.addEventListener("submit", (event) => {
107+
event.preventDefault();
108+
const chatInput = chatInputForm.querySelector("input");
109+
if (chatInput.value.trim() !== "") {
110+
postMessage(chatInput.value.trim(), getRandomCatFact(usedMessages));
111+
chatInput.value = "";
112+
}
113+
});
114+
115+
// listen to focus changes on the message list to update focusgroupstart
116+
const messageList = document.getElementById("message-list");
117+
messageList.addEventListener("focusin", onMessageFocusChange);
118+
}
119+
120+
document.addEventListener("DOMContentLoaded", function () {
121+
initChat();
122+
});

focusgroup/chatbot/index.html

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Focusgroup: Chat Pattern</title>
8+
<link rel="icon" type="image/png" href="https://edgestatic.azureedge.net/welcome/static/favicon.png">
9+
<link rel="stylesheet" href="../style.css">
10+
<link rel="stylesheet" href="chat.css">
11+
</head>
12+
13+
<body>
14+
<header class="page-header">
15+
<h1>Chatbot Pattern</h1>
16+
<p>This demonstrates two different uses of <code>focusgroup</code>:
17+
<ol>
18+
<li>A block-level customized arrow navigation region between messages</li>
19+
<li>A toolbar of actions that appears over a focused or hovered message.</li>
20+
</ol>
21+
</p>
22+
<nav><a href="../index.html">← All Demos</a></nav>
23+
</header>
24+
25+
<main class="page-content">
26+
27+
<!-- ============================================================ -->
28+
<!-- Demo Chatbot UI -->
29+
<!-- ============================================================ -->
30+
<section class="demo-section">
31+
<h2 id="catbot-start">Catbot</h2>
32+
<p>
33+
An unassuming chatbot that responds to any message with a randomly-chosen cat fact from Wikipedia.
34+
</p>
35+
<p>
36+
The message list is a <code>focusgroup="toolbar block nomemory"</code> with <code>role="feed"</code> applied on top, since there are no out-of-the-box semantics that exactly fit a chat message list. This example also demonstrates using <code>focusgroupstart</code> to manually handle memory with the exception of updating to the latest message when a new one arrives.
37+
</p>
38+
<div class="attr-label">focusgroup="toolbar block nomemory"</div>
39+
40+
<div class="try-it">
41+
<strong>Try it:</strong> Use <kbd></kbd> and <kbd></kbd> to
42+
navigate. Press <kbd>Tab</kbd> when on a specific message to reach its actions toolbar, or any links within the message. Try typing a new message into the input and tabbing back to the message list to see the latest catbot response focused.
43+
</div>
44+
45+
<div class="demo-container">
46+
<div focusgroup="toolbar block nomemory" aria-label="Chat with catbot" role="feed" id="message-list">
47+
</div>
48+
<form id="chatinput" class="chat-form" novalidate>
49+
<input type="text" aria-label="message catbot" placeholder="Ask me about cats!">
50+
<button type="submit">Send</button>
51+
</form>
52+
</div>
53+
54+
<ul class="notice-list">
55+
<li>Up/Down arrows navigate between messages</li>
56+
<li><kbd>PageUp</kbd> and <kbd>PageDown</kbd> are not built into
57+
<code>focusgroup</code>. Add JavaScript handlers if your chat
58+
pattern requires them</li>
59+
<li>The message that functions as the tab entry point state (<code>focusgroupstart</code>)
60+
is managed by JavaScript. This enables manual "memory" that updates when a new message is posted.</li>
61+
<li>Because there is no well-established semantic pattern for chat message lists,
62+
<code>role="feed"</code> and <code>role="article"</code> are manually added in this example.
63+
The <code>toolbar</code> token is used as the base, since it doesn't automatically apply
64+
roles to its children.
65+
</li>
66+
<li>Press <kbd>Tab</kbd> from bot responses to reach the inner message actions toolbar.</li>
67+
<li>Left/Right arrows navigate between actions in the inner message actions toolbar.</li>
68+
</ul>
69+
70+
<details class="source-code">
71+
<summary>View source</summary>
72+
<pre><code>&lt;div focusgroup="toolbar block nomemory"
73+
aria-label="Chat with catbot"
74+
role="feed" id="message-list"&gt;
75+
&lt;div class="message user" role="article" tabindex="0"&gt;
76+
When were cats domesticated?
77+
&lt;/div&gt;
78+
&lt;div class="message catbot" role="article" tabindex="0" focusgroupstart&gt;
79+
&lt;div class="message-actions" focusgroup="toolbar nomemory"&gt;
80+
&lt;button&gt;Copy&lt;/button&gt;
81+
&lt;button&gt;Like&lt;/button&gt;
82+
&lt;button&gt;Dislike&lt;/button&gt;
83+
&lt;button&gt;Share&lt;/button&gt;
84+
&lt;/div&gt;
85+
The domestic cat is the only domesticated species of the family Felidae.
86+
Advances in archaeology and genetics have shown that the domestication of the cat
87+
started in the Near East around 7500 BCE.
88+
&lt;/div&gt;
89+
&lt;/div&gt;</code></pre>
90+
</details>
91+
</section>
92+
93+
</main>
94+
95+
<script src="../shared.js"></script>
96+
<script type="module" src="chat.js"></script>
97+
</body>
98+
99+
</html>

focusgroup/chatbot/messages.js

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

focusgroup/index.html

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ <h2 id="quick-demo">Quick Demo</h2>
7070
<button type="button">Link</button>
7171
</div>
7272
</div>
73-
<div class="attr-label" aria-hidden="true">focusgroup="toolbar wrap"</div>
73+
<div class="attr-label">focusgroup="toolbar wrap"</div>
7474
<ul class="notice-list">
7575
<li>The entire toolbar is <strong>one tab stop</strong></li>
7676
<li>Left/Right arrows move focus between buttons</li>
@@ -92,55 +92,65 @@ <h2 id="explore-the-demos">Explore the Demos</h2>
9292
<h3>Toolbar</h3>
9393
<p>Horizontal and vertical toolbars with arrow-key
9494
navigation, entry points, and axis locking.</p>
95-
<span class="attr-label" aria-hidden="true">focusgroup="toolbar"</span>
95+
<span class="attr-label">focusgroup="toolbar"</span>
9696
</a>
9797
</li>
9898
<li>
9999
<a href="tablist.html" class="card-link">
100100
<h3>Tablist</h3>
101101
<p>Tab control with inline wrapping, <code>nomemory</code> for
102102
selected tab, and vertical variant.</p>
103-
<span class="attr-label" aria-hidden="true">focusgroup="tablist"</span>
103+
<span class="attr-label">focusgroup="tablist"</span>
104104
</a>
105105
</li>
106106
<li>
107107
<a href="menu.html" class="card-link">
108108
<h3>Menu &amp; Menubar</h3>
109109
<p>Vertical menus and menubar with popover submenus
110110
using nested focusgroups.</p>
111-
<span class="attr-label" aria-hidden="true">focusgroup="menubar"</span>
111+
<span class="attr-label">focusgroup="menubar"</span>
112112
</a>
113113
</li>
114114
<li>
115115
<a href="radiogroup.html" class="card-link">
116116
<h3>Radio Group</h3>
117117
<p>Custom radio button groups with arrow-key
118118
navigation, compared to native radios.</p>
119-
<span class="attr-label" aria-hidden="true">focusgroup="radiogroup"</span>
119+
<span class="attr-label">focusgroup="radiogroup"</span>
120120
</a>
121121
</li>
122122
<li>
123123
<a href="listbox.html" class="card-link">
124124
<h3>Listbox</h3>
125125
<p>Selectable list with vertical navigation and
126126
selection management.</p>
127-
<span class="attr-label" aria-hidden="true">focusgroup="listbox block"</span>
127+
<span class="attr-label">focusgroup="listbox block"</span>
128128
</a>
129129
</li>
130130
<li>
131131
<a href="accordion.html" class="card-link">
132132
<h3>Accordion</h3>
133133
<p>Accordion headers with block-axis navigation and
134134
opt-out panels.</p>
135-
<span class="attr-label" aria-hidden="true">focusgroup="toolbar block"</span>
135+
<span class="attr-label">focusgroup="toolbar block"</span>
136+
</a>
137+
</li>
138+
<li>
139+
<a href="chatbot" class="card-link">
140+
<h3>Chatbot</h3>
141+
<p>A feed of arrow-navigable messages with block-axis navigation.
142+
The messages have custom-scriped focusgroupstart management.
143+
Bot responses have a toolbar that appears on hover or focus.
144+
</p>
145+
<span class="attr-label">focusgroup="toolbar block nomemory"</span>
136146
</a>
137147
</li>
138148
<li>
139149
<a href="additional-concepts.html" class="card-link">
140150
<h3>Additional Concepts</h3>
141151
<p>Nested focusgroups, opt-out, deep descendants,
142152
reading-flow, and feature detection.</p>
143-
<span class="attr-label" aria-hidden="true">nested · opt-out · reading-flow</span>
153+
<span class="attr-label">nested · opt-out · reading-flow</span>
144154
</a>
145155
</li>
146156
</ul>

0 commit comments

Comments
 (0)