From 45120ed7cbb1e8821405e1f29bcc27b8bb49d840 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 27 Jan 2026 13:20:39 -0500 Subject: [PATCH 01/13] refactor(MessageFeed): convert to CompoundComponent and temp remove assistant name --- .../src/MessageFeed/MessageFeed.tsx | 212 +++++++++--------- chat/message/src/Message/Message.tsx | 11 - 2 files changed, 111 insertions(+), 112 deletions(-) diff --git a/chat/message-feed/src/MessageFeed/MessageFeed.tsx b/chat/message-feed/src/MessageFeed/MessageFeed.tsx index fedd7ce2b5..4668d2bbc5 100644 --- a/chat/message-feed/src/MessageFeed/MessageFeed.tsx +++ b/chat/message-feed/src/MessageFeed/MessageFeed.tsx @@ -8,6 +8,7 @@ import React, { } from 'react'; import { useInView } from 'react-intersection-observer'; +import { CompoundComponent } from '@leafygreen-ui/compound-component'; import LeafyGreenProvider, { useDarkMode, } from '@leafygreen-ui/leafygreen-provider'; @@ -21,109 +22,118 @@ import { } from './MessageFeed.styles'; import { MessageFeedProps } from '.'; -export const MessageFeed = forwardRef( - ( - { children, darkMode: darkModeProp, className, ...rest }: MessageFeedProps, - ref: ForwardedRef, - ) => { - const { darkMode, theme } = useDarkMode(darkModeProp); - - const scrollContainerRef = useRef(null); - const scrollTimerRef = useRef | null>(null); - - const [showScrollButton, setShowScrollButton] = useState(false); - - const { ref: topInterceptRef, inView: isTopInView } = useInView({ - initialInView: true, - root: scrollContainerRef.current, - threshold: 0, - }); - const { ref: bottomInterceptRef, inView: isBottomInView } = useInView({ - initialInView: true, - root: scrollContainerRef.current, - threshold: 0, - }); - - const scrollToLatest = useCallback(() => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTo( - 0, - scrollContainerRef.current.scrollHeight, - ); - } - }, []); - - useEffect(() => { - const scrollElement = scrollContainerRef.current; - if (!scrollElement) return; - - const isScrolledToEnd = () => { - if (!scrollContainerRef.current) return true; - const { scrollHeight, scrollTop, clientHeight } = - scrollContainerRef.current; - // Add a small buffer (2px) to account for floating point differences - return scrollHeight - scrollTop - clientHeight <= 2; - }; - - // Handle scroll events - const handleScroll = () => { - // Clear any existing timeout - if (scrollTimerRef.current) { - clearTimeout(scrollTimerRef.current); +export const MessageFeed = CompoundComponent( + // eslint-disable-next-line react/display-name + forwardRef( + ( + { + children, + darkMode: darkModeProp, + className, + ...rest + }: MessageFeedProps, + ref: ForwardedRef, + ) => { + const { darkMode, theme } = useDarkMode(darkModeProp); + + const scrollContainerRef = useRef(null); + const scrollTimerRef = useRef | null>(null); + + const [showScrollButton, setShowScrollButton] = useState(false); + + const { ref: topInterceptRef, inView: isTopInView } = useInView({ + initialInView: true, + root: scrollContainerRef.current, + threshold: 0, + }); + const { ref: bottomInterceptRef, inView: isBottomInView } = useInView({ + initialInView: true, + root: scrollContainerRef.current, + threshold: 0, + }); + + const scrollToLatest = useCallback(() => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo( + 0, + scrollContainerRef.current.scrollHeight, + ); } - - // Wait until scroll animation completes This avoids a brief flicker - // when the user scrolls to the bottom - const scrollDuration = 100; - scrollTimerRef.current = setTimeout(() => { - setShowScrollButton(!isScrolledToEnd()); - }, scrollDuration); - }; - - scrollElement.addEventListener('scroll', handleScroll); - - return () => { - scrollElement.removeEventListener('scroll', handleScroll); - if (scrollTimerRef.current) { - clearTimeout(scrollTimerRef.current); + }, []); + + useEffect(() => { + const scrollElement = scrollContainerRef.current; + if (!scrollElement) return; + + const isScrolledToEnd = () => { + if (!scrollContainerRef.current) return true; + const { scrollHeight, scrollTop, clientHeight } = + scrollContainerRef.current; + // Add a small buffer (2px) to account for floating point differences + return scrollHeight - scrollTop - clientHeight <= 2; + }; + + // Handle scroll events + const handleScroll = () => { + // Clear any existing timeout + if (scrollTimerRef.current) { + clearTimeout(scrollTimerRef.current); + } + + // Wait until scroll animation completes This avoids a brief flicker + // when the user scrolls to the bottom + const scrollDuration = 100; + scrollTimerRef.current = setTimeout(() => { + setShowScrollButton(!isScrolledToEnd()); + }, scrollDuration); + }; + + scrollElement.addEventListener('scroll', handleScroll); + + return () => { + scrollElement.removeEventListener('scroll', handleScroll); + if (scrollTimerRef.current) { + clearTimeout(scrollTimerRef.current); + } + }; + }, []); + + useEffect(() => { + if (!showScrollButton) { + scrollToLatest(); } - }; - }, []); - - useEffect(() => { - if (!showScrollButton) { - scrollToLatest(); - } - }, [children, showScrollButton, scrollToLatest]); - - return ( - -
-
- {/* Empty span element used to track if container can scroll up */} - - {children} - {/* Empty span element used to track if container can scroll down */} - + }, [children, showScrollButton, scrollToLatest]); + + return ( + +
+
+ {/* Empty span element used to track if container can scroll up */} + + {children} + {/* Empty span element used to track if container can scroll down */} + +
+
- -
- - ); + + ); + }, + ), + { + displayName: 'MessageFeed', }, ); - -MessageFeed.displayName = 'MessageFeed'; diff --git a/chat/message/src/Message/Message.tsx b/chat/message/src/Message/Message.tsx index 7ed4ebd07d..e3401d6ccb 100644 --- a/chat/message/src/Message/Message.tsx +++ b/chat/message/src/Message/Message.tsx @@ -1,7 +1,5 @@ import React, { forwardRef, useMemo } from 'react'; -import { useLeafyGreenChatContext } from '@lg-chat/leafygreen-chat-provider'; -import { AssistantAvatar } from '@leafygreen-ui/avatar'; import { CompoundComponent, filterChildren, @@ -10,7 +8,6 @@ import { import LeafyGreenProvider, { useDarkMode, } from '@leafygreen-ui/leafygreen-provider'; -import { Body } from '@leafygreen-ui/typography'; import { ActionCard, @@ -23,7 +20,6 @@ import { MessageContext } from '../MessageContext'; import { MessageSubcomponentProperty } from '../shared.types'; import { - avatarContainerStyles, getContainerStyles, getMessageContainerStyles, } from './Message.styles'; @@ -47,7 +43,6 @@ export const Message = CompoundComponent( fwdRef, ) => { const { darkMode, theme } = useDarkMode(darkModeProp); - const { assistantName } = useLeafyGreenChatContext(); const contextValue = useMemo( () => ({ @@ -90,12 +85,6 @@ export const Message = CompoundComponent( ref={fwdRef} {...rest} > - {!isSender && ( -
- - {assistantName} -
- )}
Date: Tue, 27 Jan 2026 13:33:15 -0500 Subject: [PATCH 02/13] refactor(MessageFeed): enhance component structure --- chat/message-feed/shared.types.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 chat/message-feed/shared.types.ts diff --git a/chat/message-feed/shared.types.ts b/chat/message-feed/shared.types.ts new file mode 100644 index 0000000000..cfff07008a --- /dev/null +++ b/chat/message-feed/shared.types.ts @@ -0,0 +1,13 @@ +/** + * Static property names used to identify MessageFeed compound components. + * These are implementation details for the compound component pattern and should not be exported. + */ +export const MessageFeedSubcomponentProperty = { + InitialMessage: 'isLGMessageFeedInitialMessage', +} as const; + +/** + * Type representing the possible static property names for MessageFeed subcomponents. + */ +export type MessageFeedSubcomponentProperty = + (typeof MessageFeedSubcomponentProperty)[keyof typeof MessageFeedSubcomponentProperty]; From 958d58ac39d50b8c32d2a340f9e67c6566c62069 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 27 Jan 2026 14:17:39 -0500 Subject: [PATCH 03/13] feat(MessageFeed): add MessageFeedContext --- .../MessageFeedContext.spec.tsx | 48 +++++++++++++++++++ .../MessageFeedContext/MessageFeedContext.tsx | 33 +++++++++++++ .../MessageFeedContext.types.ts | 8 ++++ .../src/MessageFeedContext/index.ts | 8 ++++ 4 files changed, 97 insertions(+) create mode 100644 chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx create mode 100644 chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx create mode 100644 chat/message-feed/src/MessageFeedContext/MessageFeedContext.types.ts create mode 100644 chat/message-feed/src/MessageFeedContext/index.ts diff --git a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx new file mode 100644 index 0000000000..f723d90df8 --- /dev/null +++ b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { renderHook } from '@leafygreen-ui/testing-lib'; + +import { + MessageFeedProvider, + useMessageFeedContext, +} from './MessageFeedContext'; +import { MessageFeedProviderProps } from './MessageFeedContext.types'; + +const renderMessageFeedProvider = ( + props?: Partial, +) => { + const defaultProps: MessageFeedProviderProps = { + shouldHideInitialMessage: false, + }; + + const { result } = renderHook(() => useMessageFeedContext(), { + wrapper: ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ), + }); + + return { result }; +}; +describe('useMessageFeedContext', () => { + test('should return the correct context', () => { + const { result } = renderMessageFeedProvider(); + expect(result.current.shouldHideInitialMessage).toBe(false); + }); + + test('should return the correct context when props are provided', () => { + const { result } = renderMessageFeedProvider({ + shouldHideInitialMessage: true, + }); + expect(result.current.shouldHideInitialMessage).toBe(true); + }); + + test('should throw an error when used outside the provider', () => { + expect(() => { + renderHook(() => useMessageFeedContext()); + }).toThrow( + 'useMessageFeedContext must be used within a MessageFeedContextProvider', + ); + }); +}); diff --git a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx new file mode 100644 index 0000000000..fecb46cd95 --- /dev/null +++ b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx @@ -0,0 +1,33 @@ +import React, { createContext, useContext } from 'react'; + +import { + MessageFeedContextType, + MessageFeedProviderProps, +} from './MessageFeedContext.types'; + +export const MessageFeedContext = createContext( + null, +); + +export const MessageFeedProvider = ({ + children, + shouldHideInitialMessage = false, +}: MessageFeedProviderProps) => { + return ( + + {children} + + ); +}; + +export const useMessageFeedContext = () => { + const context = useContext(MessageFeedContext); + + if (!context) { + throw new Error( + 'useMessageFeedContext must be used within a MessageFeedContextProvider', + ); + } + + return context; +}; diff --git a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.types.ts b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.types.ts new file mode 100644 index 0000000000..7a19beb1a7 --- /dev/null +++ b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.types.ts @@ -0,0 +1,8 @@ +import { PropsWithChildren } from 'react'; + +export interface MessageFeedContextType { + shouldHideInitialMessage: boolean; +} + +export interface MessageFeedProviderProps + extends PropsWithChildren {} diff --git a/chat/message-feed/src/MessageFeedContext/index.ts b/chat/message-feed/src/MessageFeedContext/index.ts new file mode 100644 index 0000000000..9fbbb90ecc --- /dev/null +++ b/chat/message-feed/src/MessageFeedContext/index.ts @@ -0,0 +1,8 @@ +export { + MessageFeedProvider, + useMessageFeedContext, +} from './MessageFeedContext'; +export type { + MessageFeedContextType, + MessageFeedProviderProps, +} from './MessageFeedContext.types'; From c701f8443f64028e774dcf069c6cae711de84853 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 27 Jan 2026 15:48:36 -0500 Subject: [PATCH 04/13] docs(MessageFeed): add changeset --- .changeset/dark-pots-tell.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/dark-pots-tell.md diff --git a/.changeset/dark-pots-tell.md b/.changeset/dark-pots-tell.md new file mode 100644 index 0000000000..6806104ea2 --- /dev/null +++ b/.changeset/dark-pots-tell.md @@ -0,0 +1,6 @@ +--- +'@lg-chat/message-feed': path +--- + +- [LG-5932](https://jira.mongodb.org/browse/LG-5932): Refactor to use `CompoundComponent` pattern +- [LG-5934](https://jira.mongodb.org/browse/LG-5934): add `MessageFeedProvider` and `useMessageFeedContext` \ No newline at end of file From a6d76169f2804c9beafd752be19d6ccf38f5cafd Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 27 Jan 2026 15:56:05 -0500 Subject: [PATCH 05/13] fix(MessageFeed): update error message for context provider --- .../src/MessageFeedContext/MessageFeedContext.spec.tsx | 2 +- chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx index f723d90df8..78f530d9fd 100644 --- a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx +++ b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx @@ -42,7 +42,7 @@ describe('useMessageFeedContext', () => { expect(() => { renderHook(() => useMessageFeedContext()); }).toThrow( - 'useMessageFeedContext must be used within a MessageFeedContextProvider', + 'useMessageFeedContext must be used within a MessageFeedProvider', ); }); }); diff --git a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx index fecb46cd95..101beb4999 100644 --- a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx +++ b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.tsx @@ -25,7 +25,7 @@ export const useMessageFeedContext = () => { if (!context) { throw new Error( - 'useMessageFeedContext must be used within a MessageFeedContextProvider', + 'useMessageFeedContext must be used within a MessageFeedProvider', ); } From e42718595ba32f94c2cef7a243caf769b5f4d4ad Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 27 Jan 2026 16:08:33 -0500 Subject: [PATCH 06/13] refactor(Message): undo message changes --- chat/message/src/Message/Message.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/chat/message/src/Message/Message.tsx b/chat/message/src/Message/Message.tsx index e3401d6ccb..7ed4ebd07d 100644 --- a/chat/message/src/Message/Message.tsx +++ b/chat/message/src/Message/Message.tsx @@ -1,5 +1,7 @@ import React, { forwardRef, useMemo } from 'react'; +import { useLeafyGreenChatContext } from '@lg-chat/leafygreen-chat-provider'; +import { AssistantAvatar } from '@leafygreen-ui/avatar'; import { CompoundComponent, filterChildren, @@ -8,6 +10,7 @@ import { import LeafyGreenProvider, { useDarkMode, } from '@leafygreen-ui/leafygreen-provider'; +import { Body } from '@leafygreen-ui/typography'; import { ActionCard, @@ -20,6 +23,7 @@ import { MessageContext } from '../MessageContext'; import { MessageSubcomponentProperty } from '../shared.types'; import { + avatarContainerStyles, getContainerStyles, getMessageContainerStyles, } from './Message.styles'; @@ -43,6 +47,7 @@ export const Message = CompoundComponent( fwdRef, ) => { const { darkMode, theme } = useDarkMode(darkModeProp); + const { assistantName } = useLeafyGreenChatContext(); const contextValue = useMemo( () => ({ @@ -85,6 +90,12 @@ export const Message = CompoundComponent( ref={fwdRef} {...rest} > + {!isSender && ( +
+ + {assistantName} +
+ )}
Date: Tue, 27 Jan 2026 16:36:59 -0500 Subject: [PATCH 07/13] chore(MessageFeed): add compound-component dependency --- chat/message-feed/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/chat/message-feed/package.json b/chat/message-feed/package.json index 6769451383..f5c14b60ee 100644 --- a/chat/message-feed/package.json +++ b/chat/message-feed/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@leafygreen-ui/button": "workspace:^", + "@leafygreen-ui/compound-component": "workspace:^", "@leafygreen-ui/emotion": "workspace:^", "@leafygreen-ui/icon": "workspace:^", "@leafygreen-ui/lib": "workspace:^", From eb31dd4b336b1e613a7f2622250a5042d2edf113 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 27 Jan 2026 16:39:04 -0500 Subject: [PATCH 08/13] chore(pnpm-lock): add compound-component to dependencies --- chat/message-feed/tsconfig.json | 5 ++++- pnpm-lock.yaml | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/chat/message-feed/tsconfig.json b/chat/message-feed/tsconfig.json index 711bfc73d2..71f0125853 100644 --- a/chat/message-feed/tsconfig.json +++ b/chat/message-feed/tsconfig.json @@ -32,6 +32,9 @@ { "path": "../../packages/button" }, + { + "path": "../../packages/compound-component" + }, { "path": "../../packages/emotion" }, @@ -51,4 +54,4 @@ "path": "../../packages/leafygreen-provider" } ] -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd14a209e1..8d4f00c145 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -647,6 +647,9 @@ importers: '@leafygreen-ui/button': specifier: workspace:^ version: link:../../packages/button + '@leafygreen-ui/compound-component': + specifier: workspace:^ + version: link:../../packages/compound-component '@leafygreen-ui/emotion': specifier: workspace:^ version: link:../../packages/emotion From 478ee3c6ab740a3fe47cae7bb17a1828b687a1b2 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 27 Jan 2026 16:53:02 -0500 Subject: [PATCH 09/13] refactor(MessageFeed): move shared types --- chat/message-feed/{ => src}/shared.types.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename chat/message-feed/{ => src}/shared.types.ts (100%) diff --git a/chat/message-feed/shared.types.ts b/chat/message-feed/src/shared.types.ts similarity index 100% rename from chat/message-feed/shared.types.ts rename to chat/message-feed/src/shared.types.ts From bd83d5fccf8276e74522071284cdbabb4d1c8c70 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 28 Jan 2026 10:22:20 -0500 Subject: [PATCH 10/13] chore(changeset): update dependency version for @lg-chat/message-feed --- .changeset/dark-pots-tell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dark-pots-tell.md b/.changeset/dark-pots-tell.md index 6806104ea2..9dde4f187d 100644 --- a/.changeset/dark-pots-tell.md +++ b/.changeset/dark-pots-tell.md @@ -1,5 +1,5 @@ --- -'@lg-chat/message-feed': path +'@lg-chat/message-feed': minor --- - [LG-5932](https://jira.mongodb.org/browse/LG-5932): Refactor to use `CompoundComponent` pattern From 644147166d3e4b1b4b319d8f0b78d6f58c676ab8 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 28 Jan 2026 11:00:28 -0500 Subject: [PATCH 11/13] fix(tsconfig): add missing newline at end of file --- chat/message-feed/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat/message-feed/tsconfig.json b/chat/message-feed/tsconfig.json index 71f0125853..54ed514790 100644 --- a/chat/message-feed/tsconfig.json +++ b/chat/message-feed/tsconfig.json @@ -54,4 +54,4 @@ "path": "../../packages/leafygreen-provider" } ] -} \ No newline at end of file +} From 9e0a42e8610ede48d249492189b76887c836e117 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 28 Jan 2026 11:46:27 -0500 Subject: [PATCH 12/13] fix(MessageFeedContext): handle error boundary for React 17 in context hook tests --- .../MessageFeedContext.spec.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx index 78f530d9fd..55bdbb9bc9 100644 --- a/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx +++ b/chat/message-feed/src/MessageFeedContext/MessageFeedContext.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { renderHook } from '@leafygreen-ui/testing-lib'; +import { isReact17, renderHook } from '@leafygreen-ui/testing-lib'; import { MessageFeedProvider, @@ -39,10 +39,22 @@ describe('useMessageFeedContext', () => { }); test('should throw an error when used outside the provider', () => { - expect(() => { - renderHook(() => useMessageFeedContext()); - }).toThrow( - 'useMessageFeedContext must be used within a MessageFeedProvider', - ); + /** + * The version of `renderHook` imported from "@testing-library/react-hooks" (used in React 17) + * has an error boundary, and doesn't throw errors as expected: + * https://github.com/testing-library/react-hooks-testing-library/blob/main/src/index.ts#L5 + */ + if (isReact17()) { + const { result } = renderHook(() => useMessageFeedContext()); + expect(result.error.message).toEqual( + 'useMessageFeedContext must be used within a MessageFeedProvider', + ); + } else { + expect(() => { + renderHook(() => useMessageFeedContext()); + }).toThrow( + 'useMessageFeedContext must be used within a MessageFeedProvider', + ); + } }); }); From 5681c08500e47cf8b7dac847cac0e20b561683e9 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 28 Jan 2026 12:13:04 -0500 Subject: [PATCH 13/13] testing