Skip to content

Commit d1812aa

Browse files
committed
feat(chat-layout): implement ChatMain component
1 parent 663fa6c commit d1812aa

File tree

11 files changed

+246
-10
lines changed

11 files changed

+246
-10
lines changed

chat/chat-layout/README.md

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,49 @@ npm install @lg-chat/chat-layout
2626

2727
## Overview
2828

29-
`@lg-chat/chat-layout` provides a CSS Grid-based layout system for building full-screen chat interfaces with a collapsible side nav.
29+
`@lg-chat/chat-layout` provides a CSS Grid-based layout system for building full-screen chat interfaces with a side nav that can be collapsed or pinned.
30+
31+
This package exports:
32+
33+
- `ChatLayout`: The grid container and context provider
34+
- `ChatMain`: Content area component that positions itself in the grid
35+
- `useChatLayoutContext`: Hook for accessing layout state
3036

3137
## Examples
3238

39+
### Basic
40+
41+
```tsx
42+
import { ChatLayout, ChatMain } from '@lg-chat/chat-layout';
43+
44+
function MyChatApp() {
45+
return (
46+
<ChatLayout>
47+
{/* ChatSideNav will go here */}
48+
<ChatMain>
49+
<div>Your chat content</div>
50+
</ChatMain>
51+
</ChatLayout>
52+
);
53+
}
54+
```
55+
56+
### With Initial State and Toggle Pinned Callback
57+
3358
```tsx
34-
import { ChatLayout } from '@lg-chat/chat-layout';
59+
import { ChatLayout, ChatMain } from '@lg-chat/chat-layout';
3560

3661
function MyChatApp() {
37-
const handleToggle = (isPinned: boolean) => {
62+
const handleTogglePinned = (isPinned: boolean) => {
3863
console.log('Side nav is now:', isPinned ? 'pinned' : 'collapsed');
3964
};
4065

4166
return (
42-
<ChatLayout initialIsPinned={true} onTogglePinned={handleToggle}>
43-
{/* ChatSideNav and ChatMain components will go here */}
67+
<ChatLayout initialIsPinned={false} onTogglePinned={handleTogglePinned}>
68+
{/* ChatSideNav will go here */}
69+
<ChatMain>
70+
<div>Your chat content</div>
71+
</ChatMain>
4472
</ChatLayout>
4573
);
4674
}
@@ -59,6 +87,16 @@ function MyChatApp() {
5987

6088
All other props are passed through to the underlying `<div>` element.
6189

90+
### ChatMain
91+
92+
| Prop | Type | Description | Default |
93+
| ---------- | ----------- | -------------------------- | ------- |
94+
| `children` | `ReactNode` | The main content to render | - |
95+
96+
All other props are passed through to the underlying `<div>` element.
97+
98+
**Note:** `ChatMain` must be used as a direct child of `ChatLayout` to work correctly within the grid system.
99+
62100
## Context API
63101

64102
### useChatLayoutContext

chat/chat-layout/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@
3333
"@leafygreen-ui/tokens": "workspace:^",
3434
"@lg-tools/test-harnesses": "workspace:^"
3535
},
36+
"peerDependencies": {
37+
"@lg-chat/leafygreen-chat-provider": "workspace:^"
38+
},
39+
"devDependencies": {
40+
"@lg-chat/chat-window": "workspace:^",
41+
"@lg-chat/input-bar": "workspace:^",
42+
"@lg-chat/message": "workspace:^",
43+
"@lg-chat/message-feed": "workspace:^",
44+
"@lg-chat/title-bar": "workspace:^"
45+
},
3646
"homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/chat/chat-layout",
3747
"repository": {
3848
"type": "git",

chat/chat-layout/src/ChatLayout.stories.tsx

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,88 @@
11
import React from 'react';
2+
import { ChatWindow } from '@lg-chat/chat-window';
3+
import { InputBar } from '@lg-chat/input-bar';
4+
import {
5+
LeafyGreenChatProvider,
6+
Variant,
7+
} from '@lg-chat/leafygreen-chat-provider';
8+
import { Message } from '@lg-chat/message';
9+
import { MessageFeed } from '@lg-chat/message-feed';
10+
import { TitleBar } from '@lg-chat/title-bar';
211
import { StoryMetaType } from '@lg-tools/storybook-utils';
312
import { StoryFn, StoryObj } from '@storybook/react';
413

5-
import { ChatLayout, type ChatLayoutProps } from '.';
14+
import { css } from '@leafygreen-ui/emotion';
15+
16+
import { ChatLayout, type ChatLayoutProps, ChatMain } from '.';
617

718
const meta: StoryMetaType<typeof ChatLayout> = {
819
title: 'Composition/Chat/ChatLayout',
920
component: ChatLayout,
1021
parameters: {
1122
default: 'LiveExample',
1223
},
24+
decorators: [
25+
Story => (
26+
<div
27+
style={{
28+
margin: '-100px',
29+
height: '100vh',
30+
width: '100vw',
31+
}}
32+
>
33+
<Story />
34+
</div>
35+
),
36+
],
1337
};
1438
export default meta;
1539

16-
const Template: StoryFn<ChatLayoutProps> = props => <ChatLayout {...props} />;
40+
const sideNavPlaceholderStyles = css`
41+
background-color: rgba(0, 0, 0, 0.05);
42+
padding: 16px;
43+
min-width: 200px;
44+
`;
45+
46+
const testMessages = [
47+
{
48+
id: '1',
49+
messageBody: 'Hello! How can I help you today?',
50+
isSender: false,
51+
},
52+
{
53+
id: '2',
54+
messageBody: 'I need help with my database query.',
55+
},
56+
{
57+
id: '3',
58+
messageBody:
59+
'Sure! I can help with that. What specific issue are you encountering?',
60+
isSender: false,
61+
},
62+
];
63+
64+
const Template: StoryFn<ChatLayoutProps> = props => (
65+
<LeafyGreenChatProvider variant={Variant.Compact}>
66+
<ChatLayout {...props}>
67+
<div className={sideNavPlaceholderStyles}>ChatSideNav Placeholder</div>
68+
<ChatMain>
69+
<TitleBar title="Chat Assistant" />
70+
<ChatWindow>
71+
<MessageFeed>
72+
{testMessages.map(msg => (
73+
<Message
74+
key={msg.id}
75+
messageBody={msg.messageBody}
76+
isSender={msg.isSender}
77+
/>
78+
))}
79+
</MessageFeed>
80+
<InputBar onMessageSend={() => {}} />
81+
</ChatWindow>
82+
</ChatMain>
83+
</ChatLayout>
84+
</LeafyGreenChatProvider>
85+
);
1786

1887
export const LiveExample: StoryObj<ChatLayoutProps> = {
1988
render: Template,

chat/chat-layout/src/ChatLayout/ChatLayout.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { ChatLayoutContext } from './ChatLayoutContext';
66

77
/**
88
* ChatLayout is a context provider that manages the pinned state of the side nav
9-
* and provides it to all child components.
10-
*
11-
* Context is primarily used by ChatSideNav and ChatMain.
9+
* and provides it to all child components. It uses CSS Grid to control the layout
10+
* and positioning the side nav and main content.
1211
*/
1312
export function ChatLayout({
1413
children,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import { ChatLayout } from '../ChatLayout';
5+
6+
import { ChatMain } from '.';
7+
8+
describe('packages/chat-layout/ChatMain', () => {
9+
describe('ChatMain', () => {
10+
test('renders children', () => {
11+
render(
12+
<ChatLayout>
13+
<ChatMain>
14+
<div>Main Content</div>
15+
</ChatMain>
16+
</ChatLayout>,
17+
);
18+
expect(screen.getByText('Main Content')).toBeInTheDocument();
19+
});
20+
21+
test('forwards HTML attributes to the div element', () => {
22+
render(
23+
<ChatLayout>
24+
<ChatMain data-testid="chat-main" aria-label="Chat content">
25+
Content
26+
</ChatMain>
27+
</ChatLayout>,
28+
);
29+
const element = screen.getByTestId('chat-main');
30+
expect(element).toHaveAttribute('aria-label', 'Chat content');
31+
});
32+
33+
test('forwards ref to the div element', () => {
34+
const ref = React.createRef<HTMLDivElement>();
35+
render(
36+
<ChatLayout>
37+
<ChatMain ref={ref}>Content</ChatMain>
38+
</ChatLayout>,
39+
);
40+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
41+
expect(ref.current?.tagName).toBe('DIV');
42+
});
43+
44+
test('applies custom className', () => {
45+
render(
46+
<ChatLayout>
47+
<ChatMain data-testid="chat-main" className="custom-class">
48+
Content
49+
</ChatMain>
50+
</ChatLayout>,
51+
);
52+
const element = screen.getByTestId('chat-main');
53+
expect(element).toHaveClass('custom-class');
54+
});
55+
});
56+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { css, cx } from '@leafygreen-ui/emotion';
2+
3+
import { gridAreas } from '../constants';
4+
5+
const baseContainerStyles = css`
6+
grid-area: ${gridAreas.main};
7+
display: flex;
8+
flex-direction: column;
9+
min-width: 0;
10+
height: 100%;
11+
`;
12+
13+
export const getContainerStyles = ({ className }: { className?: string }) =>
14+
cx(baseContainerStyles, className);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { forwardRef } from 'react';
2+
3+
import { getContainerStyles } from './ChatMain.styles';
4+
import { ChatMainProps } from './ChatMain.types';
5+
6+
/**
7+
* ChatMain represents the main content area of the chat layout.
8+
* It automatically positions itself in the second column of the parent
9+
* ChatLayout's CSS Grid, allowing the layout to control spacing for the sidebar.
10+
*/
11+
export const ChatMain = forwardRef<HTMLDivElement, ChatMainProps>(
12+
({ children, className, ...rest }, ref) => {
13+
return (
14+
<div ref={ref} className={getContainerStyles({ className })} {...rest}>
15+
{children}
16+
</div>
17+
);
18+
},
19+
);
20+
21+
ChatMain.displayName = 'ChatMain';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ComponentPropsWithRef, PropsWithChildren } from 'react';
2+
3+
import { DarkModeProps } from '@leafygreen-ui/lib';
4+
5+
export type ChatMainProps = ComponentPropsWithRef<'div'> &
6+
DarkModeProps &
7+
PropsWithChildren;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ChatMain } from './ChatMain';
2+
export { type ChatMainProps } from './ChatMain.types';

chat/chat-layout/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export {
44
type ChatLayoutProps,
55
useChatLayoutContext,
66
} from './ChatLayout';
7+
export { ChatMain, type ChatMainProps } from './ChatMain';

0 commit comments

Comments
 (0)