diff --git a/packages/dewee-webchat/src/widget/element.ts b/packages/dewee-webchat/src/widget/element.ts index f6114d4..c2a3d73 100644 --- a/packages/dewee-webchat/src/widget/element.ts +++ b/packages/dewee-webchat/src/widget/element.ts @@ -1069,7 +1069,11 @@ export class DeweeChatElement extends ElementBase { const destination = document.createElement("p"); destination.textContent = [card.target.type, card.target.mode].filter(Boolean).join(" ยท "); const status = document.createElement("p"); status.setAttribute("role", "status"); const effective = card.status === "pending" && Date.parse(card.expires_at) <= Date.now() ? "expired" : card.status; - status.textContent = this.strings.credential.status[effective]; + if (this.credentialBusy.has(card.id)) { + root.setAttribute("aria-busy", "true"); + const spinner = document.createElement("span"); spinner.className = "spin"; spinner.setAttribute("aria-hidden", "true"); + status.append(spinner, ` ${this.strings.credential.status.applying}`); + } else status.textContent = this.strings.credential.status[effective]; const list = document.createElement("ul"); for (const field of card.fields) { const li = document.createElement("li"); li.textContent = `${field.label} (${field.required ? this.strings.credential.required : this.strings.credential.optional})`; list.appendChild(li); } const expiry = document.createElement("p"); expiry.textContent = `${this.strings.credential.expires}: ${new Date(card.expires_at).toLocaleString()}`; diff --git a/packages/dewee-webchat/src/widget/styles.ts b/packages/dewee-webchat/src/widget/styles.ts index 87a72ee..c6170b9 100644 --- a/packages/dewee-webchat/src/widget/styles.ts +++ b/packages/dewee-webchat/src/widget/styles.ts @@ -243,6 +243,7 @@ details > summary svg.ic { width: 14px; height: 14px; flex: none; } .btn:focus-visible { outline: 3px solid color-mix(in srgb, var(--dw-accent) 45%, transparent); outline-offset: 2px; } .credential-request .bubble { overflow-wrap: anywhere; max-width: 100%; } .credential-request p { margin: 8px 0; } +.credential-request .spin { display: inline-block; vertical-align: middle; } .credential-request ul { margin: 8px 0; padding-inline-start: 20px; } .credential-actions { display: flex; flex-wrap: wrap; gap: 8px; } diff --git a/packages/dewee-webchat/test/widget-credential-request.test.ts b/packages/dewee-webchat/test/widget-credential-request.test.ts index ed33156..90878b0 100644 --- a/packages/dewee-webchat/test/widget-credential-request.test.ts +++ b/packages/dewee-webchat/test/widget-credential-request.test.ts @@ -66,7 +66,10 @@ it("aborts a pending handoff and closes its popup when the conversation changes" const popup={closed:false,opener:window,document:document.implementation.createHTMLDocument("handoff"),location:{replace:vi.fn()},close:vi.fn()}; vi.spyOn(window,"open").mockReturnValue(popup as unknown as Window); el.shadowRoot!.querySelector(".credential-actions button")!.click(); - await vi.waitFor(()=>expect(requestSignal).toBeDefined()); el.newConversation(); + await vi.waitFor(()=>expect(requestSignal).toBeDefined()); + expect(el.shadowRoot!.querySelector(".credential-request")?.getAttribute("aria-busy")).toBe("true"); + expect(el.shadowRoot!.querySelector(".credential-request .spin")).not.toBeNull(); + el.newConversation(); expect(requestSignal!.aborted).toBe(true); expect(popup.close).toHaveBeenCalledOnce(); finish(Response.json({credential_request_id:id,url:`https://app.example/credential-requests/open?token=${secret}`,expires_at:new Date(Date.now()+600000).toISOString()})); await new Promise(resolve=>setTimeout(resolve,0)); expect(popup.location.replace).not.toHaveBeenCalled();