Skip to content

fix(chat): move clear-animation lifecycle to the view layer#7092

Merged
Haroenv merged 7 commits into
masterfrom
fix/chat-clear-reduced-motion
Jun 25, 2026
Merged

fix(chat): move clear-animation lifecycle to the view layer#7092
Haroenv merged 7 commits into
masterfrom
fix/chat-clear-reduced-motion

Conversation

@Haroenv

@Haroenv Haroenv commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a bug where clicking Clear in <Chat> doesn't actually clear the conversation when the user has prefers-reduced-motion: reduce enabled — the messages visually disappear but reappear on reload — and fixes the underlying layering issue that caused it.

Root cause

The clear flow was transition-driven, and the headless connector owned a view-only concern:

  • clearMessages() in connectChat.ts only set isClearing, which adds a CSS class that fades the content out via an opacity transition.
  • The actual clear — setMessages([]) (which also persists [] to sessionStorage), conversation-id reset, error/feedback reset — ran from onClearTransitionEnd, a DOM-event-shaped method exposed on the connector's public render state, wired only to the messages' onTransitionEnd (gated on propertyName === 'opacity').

Under reduced motion the global stylesheet disables the transition:

@media (prefers-reduced-motion: reduce) {
  [class^=ais-], [class^=ais-] * { transition: none !important; animation: none !important; }
}

With transition: none, the opacity change is instant and no transitionend event fires, so onClearTransitionEnd never runs: messages are never cleared from state or sessionStorage, isClearing stays stuck at true, and the conversation comes back on reload.

Fix — move the animation lifecycle into the view layer

Rather than special-casing reduced motion in the connector, this removes the layering violation:

  • Connector now exposes a single synchronous clearMessages commit and no longer surfaces isClearing / onClearTransitionEnd on its render state. (Breaking change to the chat connector's render state — chat is a brand-new, unreleased feature.)
  • Shared Chat UI component owns the fade-out choreography. It's the common owner of the header (the Clear button) and the messages (the fading content), so isClearing lives there as local state via an injected useState — supplied through the existing Hooks seam, exactly like memo already is. Each flavor passes its own runtime's hook (React → react, JS → preact/hooks; Vue has no chat). Hooks can't be imported directly into the shared component because React renders it with React's reconciler.
  • When the user prefers reduced motion (matchMedia) — or when no state hook is supplied — the component commits immediately instead of waiting for a transition that never fires. This also makes the headless connector trivially SSR/Node-safe again (no window).

The React and JS widgets are updated to inject useState and drop the removed render-state fields.

Tests

  • connectChat: clearMessages clears messages, resets the conversation id, and is a no-op on empty — synchronously, with no transition step. Removed the now-obsolete isClearing/onClearTransitionEnd assertions.
  • Chat component (new clearing block): fades out then commits on transitionend; commits immediately under reduced motion; commits immediately with no state hook (graceful degradation).

All chat suites pass (272 tests across instantsearch-ui-components, instantsearch.js, react-instantsearch). Typecheck and lint are clean (the only remaining monorepo type errors are pre-existing instantsearch-cli/commander issues on master).

Test plan

  • Enable "reduce motion" in the OS, open the chat example (JS + React), send a message, click Clear, reload → conversation should be empty
  • With motion enabled, the fade-out animation still plays before clearing
  • "New conversation" from the error state still clears

🤖 Generated with Claude Code

@codacy-production

codacy-production Bot commented Jun 25, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

View in Codacy

TIP This summary will be updated as you push new changes.

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown
More templates

algoliasearch-helper

npm i https://pkg.pr.new/algolia/instantsearch/algoliasearch-helper@7092

instantsearch-ui-components

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch-ui-components@7092

instantsearch.css

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.css@7092

instantsearch.js

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.js@7092

react-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch@7092

react-instantsearch-core

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-core@7092

react-instantsearch-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-nextjs@7092

react-instantsearch-router-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-router-nextjs@7092

vue-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/vue-instantsearch@7092

commit: 08723f8

Clicking "Clear" relied on the opacity `transitionend` event to actually
remove the messages. Under `prefers-reduced-motion: reduce` the global
stylesheet sets `transition: none !important`, so the transition — and
therefore `transitionend` — never fires: the messages looked cleared
(opacity jumped to 0) but stayed in state and in sessionStorage, and
reappeared on reload. `isClearing` was also left stuck at `true`.

The root cause is a layering issue: the headless connector owned a
view-only concern. `clearMessages` only set `isClearing` (a CSS-driven
fade), and the real clear (`setMessages([])`, conversation reset, error
and feedback reset) ran from `onClearTransitionEnd` — a DOM-event-shaped
method on the connector's public render state. The connector should not
know about CSS transitions.

Move the fade-out choreography into the shared `Chat` UI component, which
is the common owner of the header (the Clear button) and the messages
(the fading content). It owns `isClearing` via an injected `useState`
hook — supplied through the existing `Hooks` seam, the same way `memo`
already is, so each flavor passes its own runtime's hook (React → react,
JS → preact/hooks); Vue has no chat. When the user prefers reduced motion
(or no state hook is supplied), the component commits the clear
immediately instead of waiting for a transition that never fires.

The connector now exposes a single synchronous `clearMessages` commit and
no longer surfaces `isClearing` / `onClearTransitionEnd` on its render
state. The React and JS widgets are updated to supply `useState` and to
drop the removed fields.

Tests:
- connectChat: `clearMessages` clears messages, resets the conversation
  id, and is a no-op on empty — synchronously, with no transition step.
- Chat component: fades out then commits on `transitionend`; commits
  immediately under reduced motion; commits immediately with no state
  hook (graceful degradation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Haroenv Haroenv force-pushed the fix/chat-clear-reduced-motion branch from 932df51 to 3c3cd59 Compare June 25, 2026 12:04
@Haroenv Haroenv changed the title fix(chat): clear messages immediately when reduced motion is preferred fix(chat): move clear-animation lifecycle to the view layer Jun 25, 2026
@Haroenv Haroenv requested a review from Copilot June 25, 2026 12:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Moves the “clear conversation” lifecycle out of the headless connectChat connector and into the shared Chat UI component, fixing the reduced-motion case where the clear transition never ends (so the clear never commits).

Changes:

  • Connector: clearMessages() now synchronously commits the clear and no longer exposes isClearing / onClearTransitionEnd on render state.
  • UI component: Chat owns the fade-out choreography via an injected useState hook, committing immediately under reduced motion (or without a hook).
  • Wrappers/tests: React + InstantSearch.js widgets inject useState; tests updated/added for the new clearing behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/react-instantsearch/src/widgets/Chat.tsx Injects useState into createChatComponent; removes connector-driven clearing fields and updates canClear.
packages/instantsearch.js/src/widgets/chat/chat.tsx Injects Preact useState into createChatComponent; removes connector-driven clearing fields and updates canClear.
packages/instantsearch.js/src/connectors/chat/connectChat.ts Makes clearMessages() a synchronous commit; removes isClearing and onClearTransitionEnd from render state.
packages/instantsearch.js/src/connectors/chat/tests/connectChat-test.ts Updates connector tests to match the synchronous clear behavior and removed render-state fields.
packages/instantsearch-ui-components/src/components/chat/Chat.tsx Implements view-layer clearing state/transition orchestration with reduced-motion immediate commit fallback.
packages/instantsearch-ui-components/src/components/chat/tests/Chat.test.tsx Adds tests for fade-out + transitionend commit, reduced-motion immediate commit, and “no hook” immediate commit.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/instantsearch.js/src/connectors/chat/connectChat.ts
Address review feedback on the clear flow:

- Drop the empty-messages early return in the connector's `clearMessages`.
  It also stops an in-flight stream and resets the error status and
  conversation id, which can be set even with no messages (e.g. a failed
  resume/reconnect) — guarding on message count alone could leave the chat
  stuck in an error state whose "New conversation" button (rendered on
  `status === 'error'`, independent of message count) no-ops.

- Stop any in-flight stream eagerly when clear is clicked with the
  animation path: the view component now calls `stop()` immediately and
  defers only the message removal to the transition end, so the assistant
  stops responding right away rather than after the fade-out. Previously
  the connector stopped the stream synchronously on click; moving the
  lifecycle to the view layer had deferred that too.

Tests:
- connectChat: replace the now-obsolete "no-op on empty messages" test
  with one asserting clear exits the error state and rotates the
  conversation id even with no messages.
- Chat component: add a test asserting clear stops streaming immediately
  and commits the removal on transition end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread packages/instantsearch.js/src/connectors/chat/connectChat.ts Outdated
Haroenv and others added 5 commits June 25, 2026 14:47
`clearMessages` mutated `messages`/`status` (which synchronously re-render
via ChatState callbacks) before resetting `feedbackState` and the
conversation id (which emit nothing). The re-render therefore captured a
stale id and stale feedback, and nothing re-rendered afterward. Reorder so
the non-reactive resets run first.

Also trim narrative comments introduced earlier in this PR down to the
non-obvious "why".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clearing animation genuinely needs a state hook, so make `useState` a
required injection rather than an optional one tolerated by a `noopUseState`
fallback and a `useClearingState`/`hasStateHook` dance. Both flavor wrappers
(React, instantsearch.js) already pass it, and we own the contract — the
optionality bought nothing. This matches the existing precedent in
createDisplayResultsToolComponent, which already requires `useMemo`.

`memo` stays optional here; tightening it (and ChatMessages' optional memo
plus its fallback) is a separate, pre-existing cleanup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Haroenv Haroenv marked this pull request as ready for review June 25, 2026 15:23
@Haroenv Haroenv requested review from a team, FabienMotte and afrencalg and removed request for a team June 25, 2026 15:23

@FabienMotte FabienMotte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! 🎣

@Haroenv Haroenv merged commit bf5857e into master Jun 25, 2026
15 checks passed
@Haroenv Haroenv deleted the fix/chat-clear-reduced-motion branch June 25, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants