diff --git a/README.md b/README.md index 35c8a081..355f0d8c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ TODO - Heading - List - Portal + - Menu - Text - Table @@ -71,6 +72,7 @@ yarn storybook ``` ### Linking to another package + ```bash # install dependencies yarn setup diff --git a/src/ContextMenu.tsx b/src/ContextMenu.tsx index 6a07db51..037dd36f 100644 --- a/src/ContextMenu.tsx +++ b/src/ContextMenu.tsx @@ -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 @@ -56,14 +60,14 @@ interface IMenuProps extends IBoxProps { onShow?: (event: any) => void; } -export const Menu = styled(Box as any).attrs({ +const Menu = styled(Box as any).attrs({ as: () => (props: IMenuProps) => { const { menuItems = [], ...rest } = props; return ( {menuItems.map((item: IMenuItemProps) => { - return ; + return ; })} ); @@ -100,7 +104,7 @@ interface IMenuItemProps extends ITextProps { attributes?: ITextProps; } -export const MenuItem = styled(Text as any).attrs({ +export const ContextMenuItem = styled(Text as any).attrs({ as: () => (props: IMenuItemProps) => { const { className, title, divider, disabled, onClick, preventClose } = props; diff --git a/src/Menu.tsx b/src/Menu.tsx new file mode 100644 index 00000000..0c865f08 --- /dev/null +++ b/src/Menu.tsx @@ -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 ( + + {menuItems.map((item: IMenuItemProps, index: number) => ( + + ))} + + ); +}; + +/** + * 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 ( + + {icon && } + + {(title || subtitle) && ( + + {title && {title}} + {subtitle && {subtitle}} + + )} + + ); +}; diff --git a/src/__tests__/Menu.spec.tsx b/src/__tests__/Menu.spec.tsx new file mode 100644 index 00000000..b30d1838 --- /dev/null +++ b/src/__tests__/Menu.spec.tsx @@ -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 = test; + + const wrapper = shallow(); + + 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( + + + + ); + + 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(); + + wrapper.simulate('click', event); + + expect(onClick).toHaveBeenCalledWith(event); + }); + + it('renders proper title', () => { + const text =

menu entry

; + const wrapper = mount(); + + expect(wrapper).toContainReact(text); + wrapper.unmount(); + }); + + it('renders subTitle', () => { + const subText =

sub text

; + const wrapper = mount(); + + expect(wrapper).toContainReact(subText); + wrapper.unmount(); + }); + + it('does render subTitle only if title is missing', () => { + const subText =

test

; + const wrapper = mount(); + + expect(wrapper).toContainReact(subText); + wrapper.unmount(); + }); +}); diff --git a/src/index.tsx b/src/index.tsx index 696c4b17..03b3819f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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'; diff --git a/src/utils/rules.ts b/src/utils/rules.ts index 19af9d77..9435f936 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -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 diff --git a/stories/Menu.tsx b/stories/Menu.tsx new file mode 100644 index 00000000..3cf83305 --- /dev/null +++ b/stories/Menu.tsx @@ -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', () => ( + Has onClick }, + { title: 'No onClick' }, + { title: 'Disabled Item', disabled: true }, + ]} + /> + )) + + .add('with icons', () => ( + Has onClick, icon: 'marker' }, + { title: 'No onClick', icon: 'image' }, + { title: 'Disabled Item', disabled: true, icon: 'times-circle' }, + ]} + /> + )) + .add('with icons only', () => ( + + )) + .add('with subtext', () => ( + Has onClick, subtitle: 'has subtitle', icon: 'marker' }, + { title: 'No onClick', icon: 'image' }, + { title: 'Disabled Item', disabled: true, icon: 'times-circle' }, + ]} + /> + )); diff --git a/stories/index.ts b/stories/index.ts index b420b307..1920b035 100644 --- a/stories/index.ts +++ b/stories/index.ts @@ -10,6 +10,7 @@ import './Image'; import './Input'; import './KitchenSink'; import './List'; +import './Menu'; import './Table'; import './Text'; import './Textarea';