Skip to content

Commit

Permalink
feat(components): add GridList (#1475)
Browse files Browse the repository at this point in the history
* feat(components): add `GridList`

* feat: add test

* feat: add stories

* chore: add changeset

* fix: add `Menu` destructive fill
  • Loading branch information
Niznikr authored Nov 13, 2024
1 parent 0c7ee47 commit 93f59e6
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-buttons-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@launchpad-ui/components": patch
---

Add `GridList`
18 changes: 18 additions & 0 deletions packages/components/__tests__/GridList.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest';

import { render, screen } from '../../../test/utils';
import { GridList, GridListItem } from '../src';

describe('GridList', () => {
it('renders', async () => {
render(
<GridList aria-label="Items">
<GridListItem>Item one</GridListItem>
<GridListItem>Item two</GridListItem>
<GridListItem>Item three</GridListItem>
</GridList>,
);

expect(await screen.findByRole('grid')).toBeVisible();
});
});
81 changes: 81 additions & 0 deletions packages/components/src/GridList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { forwardRefType } from '@react-types/shared';
import type { ForwardedRef } from 'react';
import type { GridListItemProps, GridListProps } from 'react-aria-components';

import { cva } from 'class-variance-authority';
import { forwardRef } from 'react';
import {
GridList as AriaGridList,
GridListItem as AriaGridListItem,
composeRenderProps,
} from 'react-aria-components';

import { Checkbox } from './Checkbox';
import { IconButton } from './IconButton';
import styles from './styles/GridList.module.css';

const list = cva(styles.list);
const item = cva(styles.item);

const _GridList = <T extends object>(
props: GridListProps<T>,
ref: ForwardedRef<HTMLDivElement>,
) => {
return (
<AriaGridList
{...props}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
list({ ...renderProps, className }),
)}
/>
);
};

/**
* A grid list displays a list of interactive items, with support for keyboard navigation, single or multiple selection, and row actions.
*
* https://react-spectrum.adobe.com/react-aria/GridList.html
*/
const GridList = (forwardRef as forwardRefType)(_GridList);

const _GridListItem = <T extends object>(props: GridListItemProps<T>, ref: ForwardedRef<T>) => {
const textValue =
props.textValue || (typeof props.children === 'string' ? props.children : undefined);
return (
<AriaGridListItem
textValue={textValue}
{...props}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
item({ ...renderProps, className }),
)}
>
{composeRenderProps(
props.children,
(children, { allowsDragging, selectionMode, selectionBehavior }) => (
<>
{allowsDragging && (
/* @ts-expect-error RAC adds label */
<IconButton slot="drag" icon="grip-horiz" size="small" variant="minimal" />
)}
{selectionMode === 'multiple' && selectionBehavior === 'toggle' && (
<Checkbox slot="selection" />
)}
{children}
</>
),
)}
</AriaGridListItem>
);
};

/**
* A GridListItem represents an individual item in a GridList.
*
* https://react-spectrum.adobe.com/react-aria/GridList.html
*/
const GridListItem = (forwardRef as forwardRefType)(_GridListItem);

export { GridList, GridListItem };
export type { GridListProps, GridListItemProps };
2 changes: 1 addition & 1 deletion packages/components/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const _Row = <T extends object>(
>
{allowsDragging && (
<Cell>
{/* @ts-ignore RAC adds label */}
{/* @ts-expect-error RAC adds label */}
<IconButton slot="drag" icon="grip-horiz" variant="minimal" size="small" />
</Cell>
)}
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type { FieldErrorProps } from './FieldError';
export type { FieldGroupProps } from './FieldGroup';
export type { FileTriggerProps } from './FileTrigger';
export type { FormProps } from './Form';
export type { GridListProps, GridListItemProps } from './GridList';
export type { GroupProps } from './Group';
export type { HeadingProps } from './Heading';
export type { InputProps } from './Input';
Expand Down Expand Up @@ -102,6 +103,7 @@ export { FieldError } from './FieldError';
export { FieldGroup } from './FieldGroup';
export { FileTrigger } from './FileTrigger';
export { Form } from './Form';
export { GridList, GridListItem } from './GridList';
export { Group } from './Group';
export { Header } from './Header';
export { Heading } from './Heading';
Expand Down
43 changes: 43 additions & 0 deletions packages/components/src/styles/GridList.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.list {
display: flex;
flex-direction: column;

&[data-empty] {
align-items: center;
justify-content: center;
}
}

.item {
composes: interactive from './base.module.css';
display: flex;
column-gap: var(--lp-spacing-300);
align-items: center;
border-bottom: 1px solid var(--lp-color-border-ui-secondary);
outline: none;
padding: var(--lp-spacing-300) var(--lp-spacing-500);
position: relative;
transform: translateZ(0);

&[data-hovered] {
background-color: var(--lp-color-bg-ui-secondary);
}

&[data-focus-visible] {
outline: 2px solid var(--lp-color-shadow-interactive-focus);
outline-offset: -2px;
z-index: 1;
}

&[data-dragging] {
opacity: 0.6;
}

&[data-disabled] {
opacity: 0.6;
}

& button[data-rac]:not([slot]) {
margin-left: auto;
}
}
4 changes: 4 additions & 0 deletions packages/components/src/styles/Menu.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,8 @@
&[data-focus-visible] {
background-color: var(--lp-color-bg-interactive-destructive-hover);
}

& [data-icon] {
fill: currentColor;
}
}
4 changes: 4 additions & 0 deletions packages/components/src/styles/Table.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
}

.body {
&[data-empty] {
font: var(--lp-text-body-2-semibold);
text-align: center;
}
}

.row {
Expand Down
122 changes: 122 additions & 0 deletions packages/components/stories/GridList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { Meta, StoryObj } from '@storybook/react';

import { useDragAndDrop } from 'react-aria-components';
import { useListData } from 'react-stately';

import {
DropIndicator,
GridList,
GridListItem,
IconButton,
Menu,
MenuItem,
MenuTrigger,
Popover,
Text,
} from '../src';

const meta: Meta<typeof GridList> = {
component: GridList,
// @ts-ignore
subcomponents: { GridListItem },
title: 'Components/Collections/GridList',
parameters: {
status: {
type: import.meta.env.STORYBOOK_PACKAGE_STATUS__COMPONENTS,
},
},
};

export default meta;

type Story = StoryObj<typeof GridList>;

const Options = () => (
<MenuTrigger>
<IconButton icon="more-vert" size="small" variant="minimal" aria-label="options" />
<Popover>
<Menu>
<MenuItem>Action one</MenuItem>
<MenuItem>
<Text slot="label">Action two</Text>
</MenuItem>
<MenuItem>Action three</MenuItem>
</Menu>
</Popover>
</MenuTrigger>
);

const renderGrid = (args: Story['args']) => (
<GridList {...args}>
<GridListItem>
Item one
<Options />
</GridListItem>
<GridListItem>
Item two
<Options />
</GridListItem>
<GridListItem>
Item three
<Options />
</GridListItem>
</GridList>
);

export const Example: Story = {
render: (args) => renderGrid(args),
args: {
selectionMode: 'single',
},
};

export const Selection: Story = {
render: (args) => renderGrid(args),
args: {
selectionMode: 'multiple',
},
};

export const DragAndDrop: Story = {
render: (args) => {
const list = useListData({
initialItems: [
{ id: 1, name: 'Item one' },
{ id: 2, name: 'Item two' },
{ id: 3, name: 'Item three' },
{ id: 4, name: 'Item four' },
{ id: 5, name: 'Item five' },
],
});

const { dragAndDropHooks } = useDragAndDrop({
getItems: (keys) => [...keys].map((key) => ({ 'text/plain': list.getItem(key).name })),
onReorder(e) {
if (e.target.dropPosition === 'before') {
list.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
list.moveAfter(e.target.key, e.keys);
}
},
renderDropIndicator: (target) => <DropIndicator target={target} />,
});

return (
<GridList dragAndDropHooks={dragAndDropHooks} {...args} items={list.items}>
{(item) => (
<GridListItem>
{item.name}
<Options />
</GridListItem>
)}
</GridList>
);
},
};

export const Empty: Story = {
args: {
children: [],
renderEmptyState: () => 'No results found',
},
};
1 change: 0 additions & 1 deletion packages/components/stories/ListBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type { Meta, StoryObj } from '@storybook/react';
import type { Selection as AriaSelection } from 'react-aria-components';

Expand Down
15 changes: 15 additions & 0 deletions packages/components/stories/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,18 @@ export const DragAndDrop: Story = {
'aria-label': 'table',
},
};

export const Empty: Story = {
args: {
children: (
<>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Description</Column>
<Column>ID</Column>
</TableHeader>
<TableBody renderEmptyState={() => 'No results found'}>{[]}</TableBody>
</>
),
},
};

0 comments on commit 93f59e6

Please sign in to comment.