Skip to main content

Patterns: Widgets

The CoreWidgetBubbleWidget/EmbedWidget class hierarchy, the createRoot bridge from class methods to React, and the inline-HTML-string anti-pattern that pervades these classes. Load via the patterns-architecture context 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's createStore())
  • 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, handles onPreStepsComplete callback)
  • URL-parameter auto-open behavior (checkWebchatInitState reads ?hws=)
  • Override of onClickForNode / onClickForIntent to also call openWC()

EmbedWidget extends CoreWidget

Adds the inline embedded mode surface (no floating button):

  • Container append (appendHTMLElementsForTheBot)
  • Header HTML variants (getChatHeaderHTML returns header-only / HCN-only / combined)
  • isUserInteraction implementation (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:

FileLineMount targetWhat renders
src/widgets/coreWidget.ts262_listeningOverlayContainer (a <span>)<ListeningOverlay label={...} /> — TTS listening indicator
src/widgets/bubbleWidget.tsx309<span class="webchat-locale-selector"> inside the chat header<LocaleDropdown ... />
src/widgets/embedWidget.tsx148same selector inside embed header<LocaleDropdown ... />
src/csat/csatManager.tsx95<span class="webchat__csat__container"> appended below transcript<CustomCSATPopup ... />
src/ratingForm/ratingFormManager.tsx95same selector (shared with CSAT)<CustomRatingFormPopup ... />
src/notifications/notificationManager.tsx50<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 createRoot return value if you may need to call .unmount() later (the CoreWidget._listeningOverlayRoot field 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 createRoot calls 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:

FileLine(s)
src/widgets/bubbleWidget.tsx119, 269, 465, 481, 545
src/widgets/embedWidget.tsx111, 129
src/preChatSteps/preChatStepsManager.ts114, 171
src/components/popUpBubbleWidget.ts32
src/components/BackButtonDecorator.tsx64
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({...})) from react-dom/server is pulled into a browser bundle just to convert a React icon to an HTML string for innerHTML injection
  • 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 createRoot mount + 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 outerHTML string are worse than the original.
  • The react-dom/server icon-serialization workaround at src/widgets/bubbleWidget.tsx:232 and src/components/popUpBubbleWidget.ts:29 is 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.