Skip to content

Commit

Permalink
Merge pull request #18 from stoplightio/SL-652/menu-component
Browse files Browse the repository at this point in the history
SL-652/menu component
  • Loading branch information
casserni authored Nov 20, 2018
2 parents d25d77b + f7e1587 commit 1c3fe79
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 4 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ TODO
- Heading
- List
- Portal
- Menu
- Text
- Table

Expand Down Expand Up @@ -71,6 +72,7 @@ yarn storybook
```

### Linking to another package

```bash
# install dependencies
yarn setup
Expand Down
10 changes: 7 additions & 3 deletions src/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { ITextProps, Text } from './Text';
import { styled } from './utils';

// TODO: expose SubMenu component
// TODO: allow custom renderMenu
// TODO: allow custom renderMenuItem?
// TODO: add icon support to menu items
// TODO: Context Menu should probably leverage/use MENU

/**
* CONTEXT MENU
Expand Down Expand Up @@ -56,14 +60,14 @@ interface IMenuProps extends IBoxProps {
onShow?: (event: any) => void;
}

export const Menu = styled<IMenuProps, 'div'>(Box as any).attrs({
const Menu = styled<IMenuProps, 'div'>(Box as any).attrs({
as: () => (props: IMenuProps) => {
const { menuItems = [], ...rest } = props;

return (
<ReactContextMenu {...rest}>
{menuItems.map((item: IMenuItemProps) => {
return <MenuItem {...item} {...item.attributes} />;
return <ContextMenuItem {...item} {...item.attributes} />;
})}
</ReactContextMenu>
);
Expand Down Expand Up @@ -100,7 +104,7 @@ interface IMenuItemProps extends ITextProps {
attributes?: ITextProps;
}

export const MenuItem = styled<IMenuItemProps, 'div'>(Text as any).attrs({
export const ContextMenuItem = styled<IMenuItemProps, 'div'>(Text as any).attrs({
as: () => (props: IMenuItemProps) => {
const { className, title, divider, disabled, onClick, preventClose } = props;

Expand Down
82 changes: 82 additions & 0 deletions src/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from 'react';

import { Box, IBoxProps } from './Box';
import { Flex } from './Flex';
import { Icon, IIcon } from './Icon';

// TODO allow dividers in the menu
// TODO allow custom renderMenu
// TODO allow custom renderMenuItem
// TODO allow custom renderTrigger to make this a popup type component

/**
* MENU
*/
export interface IMenuProps {
menuItems: IMenuItemProps[];

attributes?: IBoxProps;
}

export const Menu = (props: IMenuProps) => {
const { menuItems = [], attributes = {} } = props;

return (
<Box
fg="menu.fg"
bg="menu.bg"
borderColor="menu.border"
border="xs"
radius="md"
display="inline-block"
{...attributes}
>
{menuItems.map((item: IMenuItemProps, index: number) => (
<MenuItem key={index} {...item} />
))}
</Box>
);
};

/**
* MENU ITEM
*/

export interface IMenuItemProps {
className?: string;
children?: any;

title?: any;
subtitle?: any;
icon?: IIcon;
disabled?: boolean;

onClick?: (event: React.MouseEvent) => any;
attributes?: IBoxProps;
}

export const MenuItem = (props: IMenuItemProps) => {
const { icon, title, subtitle, disabled, onClick, attributes = {} } = props;

return (
<Flex
items="center"
px="lg"
py="md"
text="md"
cursor={disabled ? 'not-allowed' : onClick ? 'pointer' : 'default'}
opacity={disabled && 0.6}
onClick={onClick}
{...attributes}
>
{icon && <Icon icon={icon} pr={(title || subtitle) && 'xl'} />}

{(title || subtitle) && (
<span>
{title && <Box>{title}</Box>}
{subtitle && <Box text="sm">{subtitle}</Box>}
</span>
)}
</Flex>
);
};
75 changes: 75 additions & 0 deletions src/__tests__/Menu.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @jest-environment jsdom
*/
import { mount, shallow } from 'enzyme';
import 'jest-enzyme';
import * as React from 'react';

import * as _solidIcons from '@fortawesome/free-solid-svg-icons';

import { Icon, IconLibrary, IThemeInterface, Menu, MenuItem } from '../index';
import { ThemeProvider } from '../utils';

describe('Menu', () => {
it('renders items', () => {
const children = <span>test</span>;

const wrapper = shallow(<Menu menuItems={[{ title: children }, { title: 'second elem' }]} />);

expect(wrapper.children().length).toBe(2);
});
});

describe('MenuItem', () => {
// TODO fixme fails because we need to register the icons
it('renders proper Icon', () => {
const theme = { base: {} } as IThemeInterface;
const { fas } = _solidIcons;

IconLibrary.add(fas);

const wrapper = mount(
<ThemeProvider theme={theme}>
<MenuItem icon="globe" />
</ThemeProvider>
);

expect(wrapper.find(Icon)).toExist();
expect(wrapper.find(Icon)).toHaveProp('icon', 'globe');
wrapper.unmount();
});

it('propagates onClick event', () => {
const onClick = jest.fn();
const event = { type: 'click' };
const wrapper = shallow(<MenuItem onClick={onClick} />);

wrapper.simulate('click', event);

expect(onClick).toHaveBeenCalledWith(event);
});

it('renders proper title', () => {
const text = <h4>menu entry</h4>;
const wrapper = mount(<MenuItem title={text} />);

expect(wrapper).toContainReact(text);
wrapper.unmount();
});

it('renders subTitle', () => {
const subText = <h4>sub text</h4>;
const wrapper = mount(<MenuItem title="text" subtitle={subText} />);

expect(wrapper).toContainReact(subText);
wrapper.unmount();
});

it('does render subTitle only if title is missing', () => {
const subText = <h4>test</h4>;
const wrapper = mount(<MenuItem subtitle={subText} />);

expect(wrapper).toContainReact(subText);
wrapper.unmount();
});
});
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './Flex';
export * from './Heading';
export * from './Icon';
export * from './List';
export * from './Menu';
export * from './Table';
export * from './Text';
export * from './ThemeSection';
Expand Down
2 changes: 1 addition & 1 deletion src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ const getSpaceValue = scale => propVal => {
}

// check the theme config for a value, or just use the prop
val = scale[val] || val;
val = scale !== undefined ? scale[val] : val;
}

// if was negative string/add the '-' back
Expand Down
47 changes: 47 additions & 0 deletions stories/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';

import { withKnobs } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';

import { action } from '@storybook/addon-actions';
import { Menu } from '../src/';

storiesOf('Menu', module)
.addDecorator(withKnobs)
.add('with defaults', () => (
<Menu
menuItems={[
{ onClick: action('onClick'), title: <span>Has onClick</span> },
{ title: 'No onClick' },
{ title: 'Disabled Item', disabled: true },
]}
/>
))

.add('with icons', () => (
<Menu
menuItems={[
{ onClick: action('onClick'), title: <span>Has onClick</span>, icon: 'marker' },
{ title: 'No onClick', icon: 'image' },
{ title: 'Disabled Item', disabled: true, icon: 'times-circle' },
]}
/>
))
.add('with icons only', () => (
<Menu
menuItems={[
{ onClick: action('onClick'), icon: 'marker' },
{ icon: 'image' },
{ disabled: true, icon: 'times-circle' },
]}
/>
))
.add('with subtext', () => (
<Menu
menuItems={[
{ onClick: action('onClick'), title: <span>Has onClick</span>, subtitle: 'has subtitle', icon: 'marker' },
{ title: 'No onClick', icon: 'image' },
{ title: 'Disabled Item', disabled: true, icon: 'times-circle' },
]}
/>
));
1 change: 1 addition & 0 deletions stories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import './Image';
import './Input';
import './KitchenSink';
import './List';
import './Menu';
import './Table';
import './Text';
import './Textarea';
Expand Down

0 comments on commit 1c3fe79

Please sign in to comment.