Skip to content

Maintenance: fix duplicated messages, autoscroll, React 18/19 and styled-components 6 support - #392

Merged
LucasBassetti merged 18 commits into
masterfrom
LucasBassetti/maintenance
Sep 26, 2026
Merged

LucasBassetti merged 18 commits into
masterfrom
LucasBassetti/maintenance

Conversation

@LucasBassetti

Copy link
Copy Markdown
Owner

Maintenance pass over the open issues and PRs: the toolchain works again on current Node, and the most reported bugs are fixed, each with a regression test.

Duplicated messages (React 18 StrictMode)

Steps started a timer in componentDidMount and never cleared it. StrictMode mounts, unmounts and mounts again, so every step triggered twice and the conversation doubled at each step (1 → 2 → 4). Custom components that called triggerNextStep twice, or double clicks on options, did the same.

  • The step timer is cleared on unmount.
  • Each step can trigger the next step only once.
  • Repeated option clicks are ignored.

Fixes #346, fixes #370, fixes #350, fixes #386, fixes #278, fixes #202

Autoscroll

DOMNodeInserted was removed from Chrome (127+), which broke autoscroll there. It now uses a MutationObserver. A new message always scrolls to the bottom. Other DOM changes (tooltips, ripples, content loaded inside a custom step) only scroll if the user hasn't scrolled up.

Fixes #380, fixes #133, fixes #232, fixes #321, fixes #288, fixes #266

Other bugs

Features

Packaging and tooling

Test plan

  • npm run lint, npm run test:coverage (90 passing, 97% statements), npm run test:types, npm run build
  • npm run test:dom with React 19.3
  • Full suite with styled-components 4.4.1, 5.3.11 and 6.5.3, with no unknown-prop warnings
  • The new regression tests fail on the old code and pass with the fixes
  • Example app in headless Chromium under StrictMode: no duplicates, autoscroll works, no console warnings

LucasBassetti and others added 17 commits September 26, 2026 15:30
The module-level singleton made every new Recognition return the first
one, so a remounted or second ChatBot kept calling the callbacks of the
first (possibly unmounted) component.
- A first user step was rendered as an empty bubble and immediately
  triggered the next step without waiting for the user.
- Only read localStorage when cache is enabled and tolerate storage that
  throws (Safari private mode, quota exceeded). Ports #351.
… 18 tests

- Move react/react-dom and lint tooling out of runtime dependencies
- Widen peer ranges to react/react-dom >=16.3 and styled-components >=4
- Replace Travis/Node 8 CI with GitHub Actions on Node 20/22/24
- Stop tracking dist/ and coverage.lcov (dist is built on publish)
- Fix flatted import path for flatted 3
Steps started their delay timer in componentDidMount and never cleared
it, so React 18 StrictMode (mount, unmount, mount) triggered every step
twice and the conversation doubled at each step. Custom components that
called triggerNextStep more than once and double clicks on options had
the same effect.

- clear the step timer on unmount
- let each step trigger the next step at most once
- ignore repeated clicks on an options step
- Replace the removed DOMNodeInserted event with a MutationObserver, so
  the conversation scrolls to the last message again (Chrome 127+)
- Don't restart speech recognition forever when it ends with no text
- Don't access window when checking recognition support (SSR)
- Copy options of update steps instead of changing the original step
- Fix extraControl prop type (a single element, not an object of elements)
- Use default parameters instead of defaultProps in icons (React 19)
styled-components 6 forwards every prop to DOM elements, which made React
warn about user, isFirst, floating, opened, invalid and others. Filter the
style-only props with shouldForwardProp; styled-components 4 and 5.0
ignore the option and keep filtering them as before. Tested with
styled-components 4.4, 5.3 and 6.5.
rgba() returned null for named, rgb() or hsl() colors and the pulse
animation threw. Fall back to color-mix() for those colors.
- Rebuild the step definitions when steps, avatars, delays or botName
  change, so steps that were not rendered yet use the new values
- Select options without a value by their label (all of them matched
  the first option before)
- Start the conversation from the parsed first step, so it gets the
  default settings too, and don't mutate the steps prop
- Render DOM element components (e.g. <div />) without injecting step
  props, which React warned about
- Don't speak cached steps again, and read speechSynthesis on every
  message so it can be toggled after mount
- Ignore triggerNextStep calls with a value that matches no option
Autoscroll:
- scroll to the bottom on every new message
- for other DOM changes (loading bubbles, tooltips, ripples, content
  loaded inside custom steps) only scroll if the user didn't scroll up

Accessibility (based on #293):
- label the input, the submit/voice button and the open/close buttons
- make the open/close buttons focusable and usable with the keyboard
- use type="button" so the chatbot doesn't submit a surrounding form
- use a valid input type and fix the invalid avatar width (40 -> 40px)
Types for the ChatBot props, every step kind, trigger/message functions,
handleEnd and the props injected in custom step components. Checked in CI
with tsc against tests/types.
The conversation used to run while the floating chatbot was still
closed, so users opened it to find every message already there.
- LoadingStep leaked its delay prop to the DOM with styled-components 6
- The example uses createRoot and StrictMode and a real conversation
  (input, options without value and a custom step)
Also points the website and author links to the current domains (same
change as #391).
- Build for ES5 (including flatted) so webpack 4 and older browsers can
  parse the bundle, as before
- Stop tracking dist/ and build it in prepare, so installs from git get a
  fresh bundle
- Remove engines, it would block installs on older Node versions
- Build steps again lazily, when a step is triggered, instead of on every
  parent render; keep the last valid steps if the new ones are invalid
- Submit the recognized text when voice recognition ends (the batched
  speaking state restarted the recognition instead under createRoot)
- Keep the value of a first custom step and evaluate its message on a
  copy, so looping back to it evaluates the message again
- Restore option values from old caches
- Don't focus the input of a closed floating chatbot
- Allow boolean validators in the TypeScript declarations
@LucasBassetti
LucasBassetti merged commit eb3d2cf into master Sep 26, 2026
4 checks passed
@LucasBassetti
LucasBassetti deleted the LucasBassetti/maintenance branch September 26, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment