Patterns: Widgets
The
CoreWidget→BubbleWidget/EmbedWidgetclass hierarchy, thecreateRootbridge from class methods to React, and the inline-HTML-string anti-pattern that pervades these classes. Load via thepatterns-architecturecontext group.
Hierarchy
CoreWidget (abstract, src/widgets/coreWidget.ts)
├── BubbleWidget (src/widgets/bubbleWidget.tsx)
└── EmbedWidget (src/widgets/embedWidget.tsx)
src/app.ts selects between BubbleWidget and EmbedWidget based on WidgetStyle.BUBBLE vs WidgetStyle.EMBEDDED and instantiates one per botDivId.
CoreWidget (abstract)
Owns the Direct Line lifecycle and runtime state. Non-React: it manipulates DOM directly and delegates rendering to botframework-webchat's renderWebChat() rather than React's reconciler.
Abstract methods (must be implemented by subclasses):
protected abstract isUserInteraction(activity: IMessageActivity): boolean;
public abstract removeFromDOM(): void;
Owned responsibilities:
- Direct Line connection setup and teardown (
setUpDirectLineConnection,closeDirectLineConnection) - Redux store creation (
instanceStore, via botframework-webchat'screateStore()) - MutationObserver-based post-render DOM wiring
- Sub-managers:
HistoryManager,NotificationManager,TranscriptManager,LocaleManager - Speech (TTS/STT) state and the listening overlay React mount
- Send-box state (
updateSendBox,disableSendBoxActionIsActive, mic/send button toggle) - Idle-reminder timeout scheduling
- Live-chat state flags (
isInLiveChat) - Public API delegation (
getTranscript,redirectChatTo,removeDataFromBrowserStorage, etc.)
Protected/private state lives on the instance: directLine, instanceStore, observer, historyManager, notificationManager, transcriptManager, webchatInstance, webStorageInstance, localeManager, _speechActivated, _listeningOverlayRoot, _listeningOverlayContainer, idleReminderTimeouts, isInLiveChat, disableSendBoxActionIsActive, activeBotAvatar.
BubbleWidget extends CoreWidget
Adds the floating button mode-specific surface:
- Bubble button DOM (
createBotDivBubble) - Chat panel DOM (
createHTMLElementsForTheBot) - Open/close toggle (
toggleWC,openWC) - Auto-pop-out scheduling (
popOutWidgetAfterSeconds) - HCN menu button injection (
createPopupMenu) - Pre-chat steps integration (constructs
PreChatManager, handlesonPreStepsCompletecallback) - URL-parameter auto-open behavior (
checkWebchatInitStatereads?hws=) - Override of
onClickForNode/onClickForIntentto also callopenWC()
EmbedWidget extends CoreWidget
Adds the inline embedded mode surface (no floating button):
- Container append (
appendHTMLElementsForTheBot) - Header HTML variants (
getChatHeaderHTMLreturns header-only / HCN-only / combined) isUserInteractionimplementation (identical to BubbleWidget — both exclude greeting/reminder activities)
Bridging Class Methods to React: createRoot
CoreWidget and its subclasses are not React components — they are plain classes manipulating DOM. When they need to render React UI, they create a DOM mount point imperatively and call createRoot(span).render(<X />).
The five createRoot mounts in widget code:
| File | Line | Mount target | What renders |
|---|---|---|---|
src/widgets/coreWidget.ts | 262 | _listeningOverlayContainer (a <span>) | <ListeningOverlay label={...} /> — TTS listening indicator |
src/widgets/bubbleWidget.tsx | 309 | <span class="webchat-locale-selector"> inside the chat header | <LocaleDropdown ... /> |
src/widgets/embedWidget.tsx | 148 | same selector inside embed header | <LocaleDropdown ... /> |
src/csat/csatManager.tsx | 95 | <span class="webchat__csat__container"> appended below transcript | <CustomCSATPopup ... /> |
src/ratingForm/ratingFormManager.tsx | 95 | same selector (shared with CSAT) | <CustomRatingFormPopup ... /> |
src/notifications/notificationManager.tsx | 50 | <span class="webchat__notification__container"> | <CustomNotifications ... /> |
Pattern
// Inside a class method:
const span = document.createElement("span");
span.className = "hbf-some-mount-point";
parentElement.appendChild(span);
createRoot(span).render(<MyComponent {...props} />);
Rules:
- Save the
createRootreturn value if you may need to call.unmount()later (theCoreWidget._listeningOverlayRootfield is the canonical example). - The mount span class name should match the existing convention:
webchat__*for botframework-webchat-styled containers,hbf-*for new Helvia-internal ones. - Do not add multiple
createRootcalls to the same span — call.render()again on the existing root if props change.
Inline HTML String Anti-Pattern
BubbleWidget and EmbedWidget build most of their DOM via outerHTML / innerHTML template-literal strings rather than document.createElement chains:
// src/widgets/bubbleWidget.tsx:269 (representative)
div.outerHTML = `
<div class='hbf-bot-div bubble-widget-chat-container chat-is-hide'>
<div class='chat-header'>
<div class='chat-header-logo'>...</div>
<div class='chat-header-title'>${title}</div>
<div class='chat-header-icon'>${renderToStaticMarkup(CloseIcon({ width: 36 }))}</div>
</div>
<div class='loading-container'>
<div class='spinner'><div class='dot1'></div><div class='dot2'></div></div>
</div>
</div>
`;
Inventory of inline-HTML sites:
| File | Line(s) |
|---|---|
src/widgets/bubbleWidget.tsx | 119, 269, 465, 481, 545 |
src/widgets/embedWidget.tsx | 111, 129 |
src/preChatSteps/preChatStepsManager.ts | 114, 171 |
src/components/popUpBubbleWidget.ts | 32 |
src/components/BackButtonDecorator.tsx | 64 |
src/widgets/coreWidget.ts (mic / banner) | 1379, 1390, plus :850 banner div |
Why it is an anti-pattern:
- No type checking on the embedded HTML
- Class-name strings are duplicated between the template and the SCSS file with no compiler link
renderToStaticMarkup(CloseIcon({...}))fromreact-dom/serveris pulled into a browser bundle just to convert a React icon to an HTML string forinnerHTMLinjection- Event listeners must be attached after the fact via
querySelector+addEventListener, separate from the template
Going forward:
- Do not extend the pattern. When adding a new piece of widget UI, prefer a
createRootmount + a React component (the locale dropdown, listening overlay, CSAT popup, rating form, and notifications all use this approach). - When you must touch existing inline-HTML code (a bug fix in the bubble panel, a new HCN button), modify it in place rather than rewriting the surrounding template — partial conversions to React inside an
outerHTMLstring are worse than the original. - The
react-dom/servericon-serialization workaround atsrc/widgets/bubbleWidget.tsx:232andsrc/components/popUpBubbleWidget.ts:29is a symptom of the inline-HTML pattern; both go away if the template is converted to JSX.
When to Add to CoreWidget vs a Subclass
Add to CoreWidget when the responsibility is shared by both bubble and embedded modes:
- Direct Line / connection state
- Redux store, history, transcript, locale, notifications
- Send-box state, mic toggle, listening overlay
- Speech, idle reminders, live-chat flags
- Public API methods (
getTranscript,redirectChatTo, etc.)
Add to BubbleWidget or EmbedWidget when the responsibility is mode-specific:
- Bubble button rendering, open/close toggle, auto-pop-out →
BubbleWidget - Pre-chat steps integration →
BubbleWidget(embed mode does not have pre-chat) - HCN menu injection →
BubbleWidget - Embed-specific header variants →
EmbedWidget
If a method exists on both subclasses with similar implementation, hoist it to CoreWidget (isUserInteraction is currently duplicated and should be a candidate). If a method exists only on CoreWidget but is an extension point (icon swap, header customization), keep it abstract and let subclasses implement.