Skip to content

Commit 7e3fc4f

Browse files
committed
Fix submit button during submission and add accessibility helper
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
1 parent 07ce918 commit 7e3fc4f

File tree

2 files changed

+152
-36
lines changed

2 files changed

+152
-36
lines changed

rdmo_chatbot/plugin/static/chatbot/css/copilot.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@
6969
height: 2em;
7070
}
7171

72+
.sr-only {
73+
position: absolute;
74+
width: 1px;
75+
height: 1px;
76+
padding: 0;
77+
margin: -1px;
78+
overflow: hidden;
79+
clip: rect(0, 0, 0, 0);
80+
white-space: nowrap;
81+
border: 0;
82+
}
83+
7284
/* remove the watermark at the bottom of the copilot */
7385

7486
.watermark {

rdmo_chatbot/plugin/static/chatbot/js/copilot.js

Lines changed: 140 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ function truncate(string, maxLength = 32) {
66
return string.length > maxLength ? string.slice(0, maxLength) + '…' : string;
77
}
88

9+
const getCookie = (name) => {
10+
return document.cookie
11+
.split(';')
12+
.map((cookie) => cookie.trim())
13+
.filter((cookie) => cookie.startsWith(`${name}=`))
14+
.map((cookie) => decodeURIComponent(cookie.split('=')[1]))
15+
.shift()
16+
}
17+
918
const getLangCode = async (args) => {
1019
return language
1120
}
@@ -142,22 +151,34 @@ const openContactModal = async (args) => {
142151

143152
const submitButton = contactModal.querySelector('#chatbot-contact-submit')
144153

145-
$(submitButton).click(async () => {
146-
const payload = {
147-
subject: subjectInput.value,
148-
message: messageInput.value
149-
}
154+
$(submitButton).off('click').on('click', async () => {
155+
submitButton.disabled = true
150156

151-
await fetch(url, {
152-
method: 'POST',
153-
body: JSON.stringify(payload),
154-
headers: {
155-
'Content-Type': 'application/json',
156-
'X-CSRFToken': Cookies.get('csrftoken')
157+
try {
158+
const payload = {
159+
subject: subjectInput.value,
160+
message: messageInput.value
161+
}
162+
163+
const response = await fetch(url, {
164+
method: 'POST',
165+
body: JSON.stringify(payload),
166+
headers: {
167+
'Content-Type': 'application/json',
168+
'X-CSRFToken': getCookie('csrftoken')
169+
}
170+
})
171+
172+
if (!response.ok) {
173+
throw new Error(`Failed to send contact email (${response.status})`)
157174
}
158-
})
159175

160-
$(contactModal).modal('hide')
176+
$(contactModal).modal('hide')
177+
} catch (error) {
178+
console.error(error)
179+
} finally {
180+
submitButton.disabled = false
181+
}
161182
})
162183

163184
$(contactModal).modal('show')
@@ -184,41 +205,124 @@ const copilotEventHandler = async (event) => {
184205

185206
window.copilotEventHandler = copilotEventHandler
186207

187-
document.addEventListener("DOMContentLoaded", () => {
188-
const observer = new MutationObserver((mutations, obs) => {
189-
const copilot = document.getElementById("chainlit-copilot")
190-
const shadow = copilot.shadowRoot
208+
const observedShadows = new WeakSet()
209+
210+
const ensureDialogAccessibility = ({
211+
container,
212+
titleText,
213+
descriptionText,
214+
titleId,
215+
descriptionId
216+
}) => {
217+
if (!container) {
218+
return
219+
}
220+
221+
const resolvedTitleId = titleId || `${container.id || 'dialog'}-title`
222+
const resolvedDescriptionId =
223+
descriptionId || `${container.id || 'dialog'}-description`
224+
225+
if (!container.querySelector(`#${resolvedTitleId}`)) {
226+
const dialogTitle = document.createElement('h2')
227+
dialogTitle.id = resolvedTitleId
228+
dialogTitle.setAttribute('data-radix-dialog-title', '')
229+
dialogTitle.textContent = titleText
230+
dialogTitle.classList.add('sr-only')
191231

192-
const modal = shadow.getElementById("new-chat-dialog")
193-
const confirmButton = shadow.getElementById("confirm")
232+
container.prepend(dialogTitle)
233+
}
234+
235+
if (!container.getAttribute('aria-labelledby')) {
236+
container.setAttribute('aria-labelledby', resolvedTitleId)
237+
}
238+
239+
if (descriptionText) {
240+
if (!container.querySelector(`#${resolvedDescriptionId}`)) {
241+
const dialogDescription = document.createElement('p')
242+
dialogDescription.id = resolvedDescriptionId
243+
dialogDescription.textContent = descriptionText
244+
dialogDescription.classList.add('sr-only')
245+
246+
container.prepend(dialogDescription)
247+
}
248+
249+
if (!container.getAttribute('aria-describedby')) {
250+
container.setAttribute('aria-describedby', resolvedDescriptionId)
251+
}
252+
}
253+
}
194254

195-
if (modal && confirmButton && !confirmButton.dataset.hasHandler) {
196-
const handler = async (event) => {
197-
event.stopPropagation()
255+
const patchNewChatDialog = (shadow) => {
256+
const modal = shadow.getElementById("new-chat-dialog")
257+
const confirmButton = shadow.getElementById("confirm")
198258

259+
if (!modal || !confirmButton) {
260+
return
261+
}
262+
263+
const content = modal.matches?.('[data-radix-dialog-content]')
264+
? modal
265+
: modal.querySelector?.('[data-radix-dialog-content]') || modal
266+
267+
ensureDialogAccessibility({
268+
container: content,
269+
titleText: gettext('Start a new chat'),
270+
descriptionText: gettext(
271+
'This will reset the current conversation and start a new chat.'
272+
),
273+
titleId: 'chainlit-new-chat-title',
274+
descriptionId: 'chainlit-new-chat-description'
275+
})
276+
277+
if (!confirmButton.dataset.hasHandler) {
278+
confirmButton.dataset.hasHandler = "true"
279+
280+
confirmButton.addEventListener(
281+
"click",
282+
() => {
199283
window.sendChainlitMessage({
200284
type: "system_message",
201285
output: "",
202286
metadata: {
203-
"action": "reset_history",
204-
"project": parseInt(projectId)
287+
action: "reset_history",
288+
project: projectId
205289
}
206290
})
291+
},
292+
{ capture: true }
293+
)
294+
}
295+
}
207296

208-
// remove this listener so we don’t fire again
209-
confirmButton.removeEventListener("click", handler)
297+
const applyCopilotPatches = () => {
298+
const copilot = document.getElementById("chainlit-copilot")
299+
if (!copilot) {
300+
return
301+
}
210302

211-
// trigger the original click (React handles it)
212-
setTimeout(() => confirmButton.click(), 500)
303+
const shadow = copilot.shadowRoot
213304

214-
// mark handler as attached to avoid duplicates
215-
confirmButton.dataset.hasHandler = "true"
216-
}
305+
if (!shadow) {
306+
return
307+
}
217308

218-
// attach the listener
219-
confirmButton.addEventListener("click", handler)
220-
}
221-
})
309+
patchNewChatDialog(shadow)
310+
311+
if (!observedShadows.has(shadow)) {
312+
const shadowObserver = new MutationObserver(() => {
313+
patchNewChatDialog(shadow)
314+
})
315+
316+
shadowObserver.observe(shadow, { childList: true, subtree: true })
317+
observedShadows.add(shadow)
318+
}
319+
}
320+
321+
document.addEventListener("DOMContentLoaded", () => {
322+
const observer = new MutationObserver(applyCopilotPatches)
323+
324+
// Run once in case the widget is already rendered before we start observing.
325+
applyCopilotPatches()
222326

223327
observer.observe(document.body, { childList: true, subtree: true })
224-
});
328+
});

0 commit comments

Comments
 (0)