Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gold-goats-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lg-chat/input-bar': patch
---

Remove redundant `z-index: 2;` in `InputBar` content wrapping node.
17 changes: 11 additions & 6 deletions chat/chat-layout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,25 +165,30 @@ All other props are passed through to the underlying `<div>` element.
Hook that returns the current chat layout context:

```tsx
const { isPinned, togglePin } = useChatLayoutContext();
const { isPinned, togglePin, isSideNavHovered, setIsSideNavHovered } =
useChatLayoutContext();
```

**Returns:**

| Property | Type | Description |
| ----------- | ------------ | ---------------------------------------- |
| `isPinned` | `boolean` | Whether the side nav is currently pinned |
| `togglePin` | `() => void` | Function to toggle the pinned state |
| Property | Type | Description |
| --------------------- | ------------------------------ | ----------------------------------------------- |
| `isPinned` | `boolean` | Whether the side nav is currently pinned |
| `togglePin` | `() => void` | Function to toggle the pinned state |
| `isSideNavHovered` | `boolean` | Whether the side nav is currently being hovered |
| `setIsSideNavHovered` | `(isHovered: boolean) => void` | Function to set the hover state of the side nav |

## Behavior

### State Management

- `ChatLayout` manages the `isPinned` state internally and provides it to all descendants via `ChatLayoutContext`
- `ChatLayout` manages the `isPinned` and `isSideNavHovered` state internally and provides it to all descendants via `ChatLayoutContext`
- When `togglePin` is called:
1. The `isPinned` state updates
2. Grid columns resize smoothly via CSS transition
3. The `onTogglePinned` callback fires (if provided) with the new state value
- Descendant components can consume the context to:
- Read the current `isPinned` state
- Call `togglePin()` to toggle the sidebar
- Read the current `isSideNavHovered` state
- Call `setIsSideNavHovered()` to update the hover state
1 change: 0 additions & 1 deletion chat/chat-layout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@leafygreen-ui/compound-component": "workspace:^",
"@leafygreen-ui/emotion": "workspace:^",
"@leafygreen-ui/icon": "workspace:^",
"@leafygreen-ui/icon-button": "workspace:^",
"@leafygreen-ui/lib": "workspace:^",
"@leafygreen-ui/palette": "workspace:^",
"@leafygreen-ui/polymorphic": "workspace:^",
Expand Down
80 changes: 79 additions & 1 deletion chat/chat-layout/src/ChatLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MessageFeed } from '@lg-chat/message-feed';
import { TitleBar } from '@lg-chat/title-bar';
import { StoryMetaType } from '@lg-tools/storybook-utils';
import { StoryFn, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/test';

import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';

Expand Down Expand Up @@ -106,14 +107,15 @@ const Template: StoryFn<ChatLayoutProps> = props => {
{testMessages.map(msg => (
<Message
key={msg.id}
isSender={msg.isSender}
messageBody={
msg.id === '2'
? `${msg.messageBody} ${chatItems
.find(item => item.id === activeId)
?.name.toLowerCase()}`
: msg.messageBody
}
isSender={msg.isSender}
sourceType="markdown"
/>
))}
</MessageFeed>
Expand All @@ -133,3 +135,79 @@ export const LiveExample: StoryObj<ChatLayoutProps> = {
},
},
};

export const PinnedLight: StoryObj<ChatLayoutProps> = {
render: Template,
args: {
darkMode: false,
initialIsPinned: true,
},
};

export const PinnedDark: StoryObj<ChatLayoutProps> = {
render: Template,
args: {
darkMode: true,
initialIsPinned: true,
},
};

export const UnpinnedLight: StoryObj<ChatLayoutProps> = {
render: Template,
args: {
darkMode: false,
initialIsPinned: false,
},
};

export const UnpinnedDark: StoryObj<ChatLayoutProps> = {
render: Template,
args: {
darkMode: true,
initialIsPinned: false,
},
};

export const UnpinnedAndHoveredLight: StoryObj<ChatLayoutProps> = {
render: Template,
args: {
darkMode: false,
initialIsPinned: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Find the side nav
const sideNav = canvas.getByLabelText('Side navigation');

// Hover over the side nav
await userEvent.hover(sideNav);
},
parameters: {
chromatic: {
delay: 350,
},
},
};

export const UnpinnedAndHoveredDark: StoryObj<ChatLayoutProps> = {
render: Template,
args: {
darkMode: true,
initialIsPinned: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Find the side nav
const sideNav = canvas.getByLabelText('Side navigation');

// Hover over the side nav
await userEvent.hover(sideNav);
},
parameters: {
chromatic: {
delay: 350,
},
},
};
78 changes: 78 additions & 0 deletions chat/chat-layout/src/ChatLayout/ChatLayout.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,83 @@ describe('packages/chat-layout', () => {
const element = screen.getByTestId('chat-layout');
expect(element).toHaveClass('custom-class');
});

test('provides isSideNavHovered context with default value', () => {
const TestConsumer = () => {
const { isSideNavHovered } = useChatLayoutContext();
return <div>isSideNavHovered: {isSideNavHovered.toString()}</div>;
};

render(
<ChatLayout>
<TestConsumer />
</ChatLayout>,
);
expect(screen.getByText('isSideNavHovered: false')).toBeInTheDocument();
});

test('setIsSideNavHovered function updates isSideNavHovered state', async () => {
const TestConsumer = () => {
const { isSideNavHovered, setIsSideNavHovered } =
useChatLayoutContext();
return (
<>
<div>isSideNavHovered: {isSideNavHovered.toString()}</div>
<button onClick={() => setIsSideNavHovered(true)}>
Set Hovered
</button>
<button onClick={() => setIsSideNavHovered(false)}>
Set Not Hovered
</button>
</>
);
};

render(
<ChatLayout>
<TestConsumer />
</ChatLayout>,
);

expect(screen.getByText('isSideNavHovered: false')).toBeInTheDocument();

await userEvent.click(
screen.getByRole('button', { name: 'Set Hovered' }),
);
expect(screen.getByText('isSideNavHovered: true')).toBeInTheDocument();

await userEvent.click(
screen.getByRole('button', { name: 'Set Not Hovered' }),
);
expect(screen.getByText('isSideNavHovered: false')).toBeInTheDocument();
});

test('togglePin updates isPinned state correctly when hovered', async () => {
const TestConsumer = () => {
const { isPinned, togglePin, isSideNavHovered } =
useChatLayoutContext();
return (
<>
<div>isPinned: {isPinned.toString()}</div>
<div>isSideNavHovered: {isSideNavHovered.toString()}</div>
<button onClick={togglePin}>Toggle</button>
</>
);
};

render(
<ChatLayout initialIsPinned={false}>
<TestConsumer />
</ChatLayout>,
);

expect(screen.getByText('isPinned: false')).toBeInTheDocument();
expect(screen.getByText('isSideNavHovered: false')).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', { name: 'Toggle' }));

expect(screen.getByText('isPinned: true')).toBeInTheDocument();
expect(screen.getByText('isSideNavHovered: false')).toBeInTheDocument();
});
});
});
35 changes: 24 additions & 11 deletions chat/chat-layout/src/ChatLayout/ChatLayout.styles.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { css, cx } from '@leafygreen-ui/emotion';
import { transitionDuration } from '@leafygreen-ui/tokens';

import {
COLLAPSED_SIDE_NAV_WIDTH_WITH_BORDER,
gridAreas,
SIDE_NAV_WIDTH_COLLAPSED,
SIDE_NAV_WIDTH_PINNED,
PINNED_SIDE_NAV_WIDTH_WITH_BORDER,
SIDE_NAV_TRANSITION_DURATION,
} from '../constants';

const getBaseContainerStyles = (isPinned: boolean) => css`
display: grid;
grid-template-areas: '${gridAreas.sideNav} ${gridAreas.main}';
grid-template-columns: ${isPinned
? `${SIDE_NAV_WIDTH_PINNED}px`
: `${SIDE_NAV_WIDTH_COLLAPSED}px`} 1fr;
const baseContainerStyles = css`
overflow: hidden;
height: 100%;
width: 100%;
transition: grid-template-columns ${transitionDuration.default}ms ease-in-out;
max-height: 100vh;
max-width: 100vw;
display: grid;
grid-template-areas: '${gridAreas.sideNav} ${gridAreas.main}';
grid-template-columns: ${PINNED_SIDE_NAV_WIDTH_WITH_BORDER}px auto;
transition: grid-template-columns ${SIDE_NAV_TRANSITION_DURATION}ms
ease-in-out;
`;

const collapsedContainerStyles = css`
grid-template-columns: ${COLLAPSED_SIDE_NAV_WIDTH_WITH_BORDER}px auto;
`;

export const getContainerStyles = ({
Expand All @@ -24,4 +30,11 @@ export const getContainerStyles = ({
}: {
className?: string;
isPinned: boolean;
}) => cx(getBaseContainerStyles(isPinned), className);
}) =>
cx(
baseContainerStyles,
{
[collapsedContainerStyles]: !isPinned,
},
className,
);
15 changes: 13 additions & 2 deletions chat/chat-layout/src/ChatLayout/ChatLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';

import { getContainerStyles } from './ChatLayout.styles';
import { ChatLayoutProps } from './ChatLayout.types';
Expand All @@ -17,15 +17,26 @@ export function ChatLayout({
...rest
}: ChatLayoutProps) {
const [isPinned, setIsPinned] = useState(initialIsPinned);
const [isSideNavHovered, setIsSideNavHovered] = useState(false);

const togglePin = useCallback(() => {
const newValue = !isPinned;
setIsPinned(newValue);
onTogglePinned?.(newValue);
}, [isPinned, onTogglePinned]);

const value = useMemo(
() => ({
isPinned,
togglePin,
isSideNavHovered,
setIsSideNavHovered,
}),
[isPinned, togglePin, isSideNavHovered, setIsSideNavHovered],
);

return (
<ChatLayoutContext.Provider value={{ isPinned, togglePin }}>
<ChatLayoutContext.Provider value={value}>
<div className={getContainerStyles({ className, isPinned })} {...rest}>
{children}
</div>
Expand Down
12 changes: 12 additions & 0 deletions chat/chat-layout/src/ChatLayout/ChatLayoutContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,23 @@ export interface ChatLayoutContextProps {
* Function to toggle the pinned state of the side nav
*/
togglePin: () => void;

/**
* Whether the side nav is currently being hovered
*/
isSideNavHovered: boolean;

/**
* Function to set the hover state of the side nav
*/
setIsSideNavHovered: (isHovered: boolean) => void;
}

export const ChatLayoutContext = createContext<ChatLayoutContextProps>({
isPinned: true,
togglePin: () => {},
isSideNavHovered: false,
setIsSideNavHovered: () => {},
});

export const useChatLayoutContext = () => useContext(ChatLayoutContext);
12 changes: 12 additions & 0 deletions chat/chat-layout/src/ChatMain/ChatMain.styles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { css, cx } from '@leafygreen-ui/emotion';
import { spacing } from '@leafygreen-ui/tokens';

import { gridAreas } from '../constants';

const CHAT_WINDOW_MAX_WIDTH = 800;

const baseContainerStyles = css`
grid-area: ${gridAreas.main};
display: flex;
Expand All @@ -12,3 +15,12 @@ const baseContainerStyles = css`

export const getContainerStyles = ({ className }: { className?: string }) =>
cx(baseContainerStyles, className);

export const chatWindowWrapperStyles = css`
height: 100%;
width: 100%;
max-width: ${CHAT_WINDOW_MAX_WIDTH}px;
padding: 0 ${spacing[800]}px;
display: flex;
align-self: center;
`;
Loading
Loading