Initial commit

This commit is contained in:
Tom Lerendu 2021-06-16 14:09:26 +01:00
commit 7878e653a0
253 changed files with 24552 additions and 0 deletions

View file

@ -0,0 +1,35 @@
import React from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import List, { ListProps } from './List';
import ContextMenuDecorator from '../../../../.storybook/decorators/ContextMenuDecorator';
import DropdownMenuDecorator from '../../../../.storybook/decorators/DropdownMenuDecorator';
import * as ListItem from './ListItem.stories';
export default {
title: 'List / List',
component: List,
decorators: [
DropdownMenuDecorator,
ContextMenuDecorator,
],
} as Meta;
const Template: Story<ListProps> = (args) => (
<List>
{args.children}
</List>
);
export const Primary = Template.bind({ });
Primary.args = {
children: (
<>
<ListItem.Primary {...ListItem.Primary.args as any} />
<ListItem.WithLeftClickActions {...ListItem.WithLeftClickActions.args as any} />
<ListItem.WithRightClickActions {...ListItem.WithRightClickActions.args as any} />
<ListItem.WithSingleLeftClickAction {...ListItem.WithSingleLeftClickAction.args as any} />
<ListItem.WithSubtitle {...ListItem.WithSubtitle.args as any} />
</>
),
};

View file

@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';
export interface ListProps {
children: ReactNode,
}
export default function List({
children,
}: ListProps) {
return (
<ul>
{children}
</ul>
);
}

View file

@ -0,0 +1,59 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { Story, Meta } from '@storybook/react/types-6-0';
import ListItem, { ListItemProps } from './ListItem';
import ContextMenuDecorator from '../../../../.storybook/decorators/ContextMenuDecorator';
import DropdownMenuDecorator from '../../../../.storybook/decorators/DropdownMenuDecorator';
export default {
title: 'List / List Item',
component: ListItem,
decorators: [
DropdownMenuDecorator,
ContextMenuDecorator,
],
} as Meta;
const Template: Story<ListItemProps> = (args) => (
<ListItem {...args} />
);
export const Primary = Template.bind({ });
Primary.args = {
title: 'List item title',
};
export const WithSubtitle = Template.bind({ });
WithSubtitle.args = {
...Primary.args,
subtitle: 'List item subtitle',
};
export const WithSingleLeftClickAction = Template.bind({ });
WithSingleLeftClickAction.args = {
...Primary.args,
onClick: action('Single left click'),
};
export const WithLeftClickActions = Template.bind({ });
WithLeftClickActions.args = {
...Primary.args,
primaryClickActions: [
{ label: 'Action 1', onClick: action('Action 1 click') },
{ label: 'Action 2', onClick: action('Action 2 click') },
],
};
export const WithRightClickActions = Template.bind({ });
WithRightClickActions.args = {
...Primary.args,
secondaryClickActions: [
{ label: 'Action 1', onClick: action('Action 1 click') },
{ label: 'Action 2', onClick: action('Action 2 click') },
],
};

View file

@ -0,0 +1,15 @@
import React from 'react';
import { render, testId } from '../../../tests/enzyme';
import ListItem from './ListItem';
describe('ListItem', () => {
it('displays the title', () => {
const wrapper = render(<ListItem title="Test Title" />);
expect(wrapper.find(testId('title')).text()).toEqual('Test Title');
});
it('displays the subtitle', () => {
const wrapper = render(<ListItem title="Test Title" subtitle="Subtitle" />);
expect(wrapper.find(testId('subtitle')).text()).toEqual('Subtitle');
});
});

View file

@ -0,0 +1,120 @@
import React, {
ReactElement,
useContext,
useState,
} from 'react';
import { FaEllipsisH } from 'react-icons/fa';
import tw from 'twin.macro';
import { ContextMenuContext } from '../../../providers/ContextMenuProvider';
import { DropdownMenuContext } from '../../../providers/DropdownMenuProvider';
import ContextMenuAction from '../../../providers/context-menu-action';
export interface ListItemProps {
title: ReactElement | string,
subtitle?: ReactElement | string,
isSelected?: boolean,
onClick?: (event: React.MouseEvent) => void,
primaryClickActions?: ContextMenuAction[],
secondaryClickActions?: ContextMenuAction[],
}
export default function ListItem({
title,
subtitle,
isSelected: isSelectedExternal,
onClick,
primaryClickActions,
secondaryClickActions,
}: ListItemProps) {
const contextMenu = useContext(ContextMenuContext);
const dropdownMenu = useContext(DropdownMenuContext);
const [isSelected, setIsSelected] = useState<boolean>(false);
const [isDropdownSelected, setIsDropdownSelected] = useState<boolean>(false);
return (
<li
className="group"
css={[
tw`flex border-b last:border-b-0 border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-100 hover:dark:bg-gray-800`,
(isSelected || isSelectedExternal) && tw`bg-gray-100 dark:bg-gray-800`,
]}
>
<button
type="button"
onClick={async (event) => {
if (primaryClickActions?.length) {
setIsSelected(true);
await contextMenu.openForMouseEvent(
event,
primaryClickActions,
);
setIsSelected(false);
}
onClick?.(event);
}}
onContextMenu={async (event) => {
if (secondaryClickActions?.length) {
setIsSelected(true);
await contextMenu.openForMouseEvent(
event,
secondaryClickActions,
);
setIsSelected(false);
}
}}
tw="w-full flex flex-row items-center py-2 px-4"
>
<div tw="flex flex-col flex-grow text-left">
<div
tw="text-gray-800 dark:text-gray-200"
data-testid="title"
>
{title}
</div>
{subtitle && (
<div
tw="pt-2 text-gray-500 dark:text-gray-400 text-xs"
data-testid="subtitle"
>
{subtitle}
</div>
)}
</div>
{!!secondaryClickActions?.length && (
<div
role="presentation"
css={[
tw`ml-2 text-xs text-gray-700 dark:text-gray-300 p-1 ml-2 hover:bg-gray-300 hover:dark:bg-gray-700`,
!isDropdownSelected && tw`invisible group-hover:visible`,
isDropdownSelected && tw`bg-gray-300 dark:bg-gray-900 visible`,
]}
onClick={async (event) => {
event.stopPropagation();
setIsSelected(true);
setIsDropdownSelected(true);
await dropdownMenu.openForElement(
event.currentTarget as HTMLElement,
secondaryClickActions!,
);
setIsSelected(false);
setIsDropdownSelected(false);
}}
onContextMenu={(event) => {
event.stopPropagation();
event.preventDefault();
}}
>
<FaEllipsisH />
</div>
)}
</button>
</li>
);
}
ListItem.defaultProps = {
primaryClickActions: [],
secondaryClickActions: [],
};