Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 2 additions & 0 deletions packages/collection-toolbar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@leafygreen-ui/emotion": "workspace:^",
"@leafygreen-ui/lib": "workspace:^",
"@leafygreen-ui/tokens": "workspace:^",
"@leafygreen-ui/compound-component": "workspace:^",
"@leafygreen-ui/typography": "workspace:^",
"@lg-tools/test-harnesses": "workspace:^"
},
"peerDependencies": {
Expand Down
14 changes: 13 additions & 1 deletion packages/collection-toolbar/src/CollectionToolbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ const meta: StoryMetaType<typeof CollectionToolbar> = {
variant: Object.values(Variant),
},
},
docs: {
description: {
component:
'CollectionToolbar is a component that displays a toolbar for a collection.',
},
},
},
argTypes: {
darkMode: storybookArgTypes.darkMode,
Expand All @@ -30,11 +36,17 @@ const meta: StoryMetaType<typeof CollectionToolbar> = {
},
args: {
children: 'Collection Toolbar',
size: {
control: 'select',
options: Object.values(Size),
},
},
};

export default meta;

export const LiveExample: StoryFn<typeof CollectionToolbar> = props => (
<CollectionToolbar {...props} />
<CollectionToolbar {...props}>
<CollectionToolbar.Title>Collection Title</CollectionToolbar.Title>
</CollectionToolbar>
);
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import { Variant } from '../shared.types';
import { getTestUtils } from '../testing/getTestUtils';

import { CollectionToolbar } from '.';

describe('packages/collection-toolbar', () => {
test('renders correctly', () => {
render(<CollectionToolbar>Collection Toolbar</CollectionToolbar>);
expect(screen.getByText('Collection Toolbar')).toBeInTheDocument();
render(<CollectionToolbar />);
const utils = getTestUtils();
expect(utils.getCollectionToolbar()).toBeInTheDocument();
});

test('applies className to the root element', () => {
render(<CollectionToolbar className="test-class" />);
const utils = getTestUtils();
expect(utils.getCollectionToolbar()?.className).toContain('test-class');
});

describe('variant: collapsible', () => {
test('renders title when variant is collapsible', () => {
render(
<CollectionToolbar variant={Variant.Collapsible}>
<CollectionToolbar.Title>Test Title</CollectionToolbar.Title>
</CollectionToolbar>,
);
expect(screen.getByText('Test Title')).toBeInTheDocument();
});

test('does not render title when variant is not collapsible', () => {
render(
<CollectionToolbar variant={Variant.Default}>
<CollectionToolbar.Title>Test Title</CollectionToolbar.Title>
</CollectionToolbar>,
);
expect(screen.queryByText('Test Title')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { css, cx } from '@leafygreen-ui/emotion';

import { Size, Variant } from './CollectionToolbar.types';
import { Size, Variant } from '../shared.types';

export const baseStyles = css``;

export const getCollectionToolbarStyles = ({
size,
variant,
className,
}: {
size?: Size;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,61 @@
import React from 'react';
import React, { forwardRef } from 'react';

import { getCollectionToolbarStyles } from './CollectionToolbar.styles';
import {
CollectionToolbarProps,
CompoundComponent,
findChild,
} from '@leafygreen-ui/compound-component';

import { Title } from '../components';
import { CollectionToolbarProvider } from '../Context/CollectionToolbarProvider';
import {
CollectionToolbarSubComponentProperty,
Size,
Variant,
} from './CollectionToolbar.types';
} from '../shared.types';
import { getLgIds } from '../utils';

import { getCollectionToolbarStyles } from './CollectionToolbar.styles';
import { CollectionToolbarProps } from './CollectionToolbar.types';

export const CollectionToolbar = CompoundComponent(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want to include a forwardRef for this component?

Copy link
Collaborator

@stephl3 stephl3 Jan 6, 2026

Choose a reason for hiding this comment

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

This is wrapped in forwardRef now, but we're still missing a ref being passed to the root <div>

// eslint-disable-next-line react/display-name
forwardRef<HTMLDivElement, CollectionToolbarProps>(
({
size = Size.Default,
variant = Variant.Default,
className,
children,
'data-lgid': dataLgId,
darkMode,
...rest
}) => {
const lgIds = getLgIds(dataLgId);
const title = findChild(
children,
CollectionToolbarSubComponentProperty.Title,
);

export function CollectionToolbar({
size = Size.Default,
variant = Variant.Default,
className,
children,
}: CollectionToolbarProps) {
return (
<div className={getCollectionToolbarStyles({ size, variant, className })}>
{children}
</div>
);
}
const showTitle = title && variant === Variant.Collapsible;

CollectionToolbar.displayName = 'CollectionToolbar';
return (
<CollectionToolbarProvider
darkMode={darkMode}
size={size}
lgIds={lgIds}
>
<div
data-lgid={lgIds.root}
className={getCollectionToolbarStyles({ size, variant, className })}
{...rest}
>
{showTitle && title}
</div>
</CollectionToolbarProvider>
);
},
),
{
displayName: 'CollectionToolbar',
Title,
},
);
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import React from 'react';
import { ComponentPropsWithRef } from 'react';

import { Size as ImportedSize } from '@leafygreen-ui/tokens';
import { DarkModeProps, LgIdProps } from '@leafygreen-ui/lib';

export const Variant = {
Compact: 'compact',
Default: 'default',
Collapsible: 'collapsible',
} as const;
export type Variant = (typeof Variant)[keyof typeof Variant];
import { Size, Variant } from '../shared.types';

export const Size = {
Default: ImportedSize.Default,
Small: ImportedSize.Small,
} as const;
export type Size = (typeof Size)[keyof typeof Size];

export interface CollectionToolbarProps {
size?: typeof ImportedSize.Default | typeof ImportedSize.Small;
export interface CollectionToolbarProps
extends ComponentPropsWithRef<'div'>,
DarkModeProps,
LgIdProps {
/**
* The size of the CollectionToolbar and it's sub-components.
*
* @default `'default'`
*/
size?: typeof Size.Default | typeof Size.Small;
/**
* The variant of the CollectionToolbar. Determines the layout of the CollectionToolbar.
*
* @default `'default'`
*/
variant?: Variant;
className?: string;
children?: React.ReactNode;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
export { CollectionToolbar } from './CollectionToolbar';
export {
type CollectionToolbarProps,
Size,
Variant,
} from './CollectionToolbar.types';
export { type CollectionToolbarProps } from './CollectionToolbar.types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, {
createContext,
PropsWithChildren,
useContext,
useMemo,
} from 'react';

import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';
import { DarkModeProps } from '@leafygreen-ui/lib';

import { Size } from '../shared.types';
import { getLgIds, type GetLgIdsReturnType } from '../utils';

export interface CollectionToolbarContextProps extends DarkModeProps {
/**
* The size of the CollectionToolbar and it's sub-components.
*
* @default `'default'`
*/
size?: Size;
/**
* LGIDs for CollectionToolbar components
*/
lgIds: GetLgIdsReturnType;
}

export const CollectionToolbarContext =
createContext<CollectionToolbarContextProps>({
size: Size.Default,
lgIds: getLgIds(),
});

export const useCollectionToolbarContext = () => {
const context = useContext(CollectionToolbarContext);
if (!context)
throw new Error(
'useCollectionToolbarContext must be used within a CollectionToolbarProvider',
);
return useContext(CollectionToolbarContext);
};

export const CollectionToolbarProvider = ({
children,
darkMode,
size,
lgIds,
}: PropsWithChildren<CollectionToolbarContextProps>) => {
const collectionToolbarProviderData = useMemo(() => {
return {
size,
lgIds,
};
}, [size, lgIds]);

return (
<LeafyGreenProvider darkMode={darkMode}>
<CollectionToolbarContext.Provider value={collectionToolbarProviderData}>
{children}
</CollectionToolbarContext.Provider>
</LeafyGreenProvider>
);
};
42 changes: 42 additions & 0 deletions packages/collection-toolbar/src/components/Title/Title.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import { CollectionToolbarSubComponentProperty } from '../../shared.types';
import { getTestUtils } from '../../testing/getTestUtils';

import Title from './Title';

describe('packages/collection-toolbar/CollectionToolbar/components/Title', () => {
test('renders children correctly', () => {
render(<Title>Test Title</Title>);
const utils = getTestUtils();
expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(utils.getTitle()).toBeInTheDocument();
});

test('renders as an h3 element as default', () => {
render(<Title>Test Title</Title>);
expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument();
});

test('renders as a p element when using as prop', () => {
render(<Title as="p">Test Title</Title>);
expect(screen.getByText('Test Title').tagName).toBe('P');
});

test('applies className to the rendered element', () => {
render(<Title className="custom-class">Test Title</Title>);
expect(screen.getByText('Test Title')).toHaveClass('custom-class');
});

test('does not allow darkMode prop', () => {
// @ts-expect-error: darkMode prop is not allowed
render(<Title darkMode={true}>Test Title</Title>);
const utils = getTestUtils();
expect(utils.getTitle()).not.toHaveClass('dark-mode');
});

test('has the correct static property for compound component identification', () => {
expect(Title[CollectionToolbarSubComponentProperty.Title]).toBe(true);
});
});
30 changes: 30 additions & 0 deletions packages/collection-toolbar/src/components/Title/Title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

import { CompoundSubComponent } from '@leafygreen-ui/compound-component';
import { H3 } from '@leafygreen-ui/typography';

import { useCollectionToolbarContext } from '../../Context/CollectionToolbarProvider';
import { CollectionToolbarSubComponentProperty } from '../../shared.types';

import { TitleProps } from './Title.types';

/**
* Title is a compound component that renders a title for CollectionToolbar.
* It will only render if the CollectionToolbar variant is set to Collapsible.
*/
const Title = CompoundSubComponent(
({ className, children, ...rest }: TitleProps) => {
const { lgIds } = useCollectionToolbarContext();
return (
<H3 data-lgid={lgIds.title} className={className} {...rest}>
{children}
</H3>
);
},
{
displayName: 'Title',
key: CollectionToolbarSubComponentProperty.Title,
},
);

export default Title;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { DarkModeProps } from '@leafygreen-ui/lib';
import { H3Props } from '@leafygreen-ui/typography';

export interface TitleProps extends Omit<H3Props, keyof DarkModeProps> {}
2 changes: 2 additions & 0 deletions packages/collection-toolbar/src/components/Title/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Title } from './Title';
export type { TitleProps } from './Title.types';
1 change: 1 addition & 0 deletions packages/collection-toolbar/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Title, type TitleProps } from './Title';
4 changes: 2 additions & 2 deletions packages/collection-toolbar/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export {
CollectionToolbar,
type CollectionToolbarProps,
Size,
Variant,
} from './CollectionToolbar';
export { type TitleProps as CollectionToolbarTitleProps } from './components';
export { Size, Variant } from './shared.types';
Loading
Loading