diff --git a/docs/src/examples/components/Menu/Types/MenuExampleWithSubmenu.shorthand.tsx b/docs/src/examples/components/Menu/Types/MenuExampleWithSubmenu.shorthand.tsx new file mode 100644 index 0000000000..36879d0f8c --- /dev/null +++ b/docs/src/examples/components/Menu/Types/MenuExampleWithSubmenu.shorthand.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { Menu } from '@stardust-ui/react' + +const menuWidthVariable = { + menuItemWidth: 100, +} + +const fileSubmenu = { + items: [ + { key: 'new', content: 'New' }, + { key: 'open', content: 'Open' }, + { key: 'edit', content: 'Edit' }, + ], +} +const editSubmenu = { + items: [ + { key: 'undo', content: 'Undo' }, + { key: 'redo', content: 'Redo' }, + { key: 'cut', content: 'Cut' }, + { key: 'copy', content: 'Copy' }, + ], +} +const formatSubmenu = { + items: [{ key: 'font', content: 'Font' }, { key: 'text', content: 'Text' }], +} + +const items = [ + { key: 'file', content: File ▾, submenu: fileSubmenu }, + { key: 'edit', content: Edit ▾, submenu: editSubmenu }, + { key: 'format', content: Format ▾, submenu: formatSubmenu }, + { key: 'help', content: 'Help' }, +] + +const MenuExampleWithSubmenu = () => ( + +) + +export default MenuExampleWithSubmenu diff --git a/docs/src/examples/components/Menu/Types/index.tsx b/docs/src/examples/components/Menu/Types/index.tsx index 8ce62a8f8d..dbf7fcc014 100644 --- a/docs/src/examples/components/Menu/Types/index.tsx +++ b/docs/src/examples/components/Menu/Types/index.tsx @@ -19,6 +19,11 @@ const Types = () => ( description="A vertical menu displays elements vertically." examplePath="components/Menu/Types/MenuExampleVertical" /> + ) diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 60421a30ba..f75ce5d482 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -2,7 +2,12 @@ import * as _ from 'lodash' import * as PropTypes from 'prop-types' import * as React from 'react' -import { AutoControlledComponent, childrenExist, customPropTypes } from '../../lib' +import { + AutoControlledComponent, + childrenExist, + customPropTypes, + createShorthandFactory, +} from '../../lib' import MenuItem from './MenuItem' import { MenuBehavior } from '../../lib/accessibility' import { Accessibility } from '../../lib/accessibility/interfaces' @@ -163,4 +168,6 @@ class Menu extends AutoControlledComponent, any> { } } +Menu.create = createShorthandFactory(Menu, {}) + export default Menu diff --git a/src/components/Menu/MenuItem.tsx b/src/components/Menu/MenuItem.tsx index e33ce251d2..78cd03d960 100644 --- a/src/components/Menu/MenuItem.tsx +++ b/src/components/Menu/MenuItem.tsx @@ -3,8 +3,14 @@ import * as cx from 'classnames' import * as PropTypes from 'prop-types' import * as React from 'react' -import { childrenExist, createShorthandFactory, customPropTypes, UIComponent } from '../../lib' +import { + childrenExist, + createShorthandFactory, + customPropTypes, + AutoControlledComponent, +} from '../../lib' import Icon from '../Icon' +import Menu from '../Menu' import { MenuItemBehavior } from '../../lib/accessibility' import { Accessibility } from '../../lib/accessibility/interfaces' @@ -34,9 +40,16 @@ export interface IMenuItemProps { vertical?: boolean styles?: IComponentPartStylesInput variables?: ComponentVariablesInput + submenu?: ItemShorthand + submenuOpened?: boolean + defaultSubmenuOpened?: boolean +} + +interface MenuItemState { + submenuOpened: boolean } -class MenuItem extends UIComponent, any> { +class MenuItem extends AutoControlledComponent, MenuItemState> { static displayName = 'MenuItem' static className = 'ui-menu__item' @@ -59,6 +72,9 @@ class MenuItem extends UIComponent, any> { /** Shorthand for primary content. */ content: PropTypes.any, + /** Initial submenuOpened value. */ + defaultSubmenuOpened: PropTypes.bool, + /** Name or shorthand for Menu Item Icon */ icon: customPropTypes.itemShorthand, @@ -101,6 +117,12 @@ class MenuItem extends UIComponent, any> { /** Custom styles to be applied for component. */ styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + /** Shorthand for Menu Item submenu */ + submenu: customPropTypes.itemShorthand, + + /** Auto controlled prop that defines if submenu is opened */ + submenuOpened: PropTypes.bool, + /** Custom variables to be applied for component. */ variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), } @@ -110,6 +132,8 @@ class MenuItem extends UIComponent, any> { accessibility: MenuItemBehavior as Accessibility, } + static autoControlledProps = ['submenuOpened'] + static handledProps = [ 'accessibility', 'active', @@ -117,6 +141,7 @@ class MenuItem extends UIComponent, any> { 'children', 'className', 'content', + 'defaultSubmenuOpened', 'icon', 'iconOnly', 'index', @@ -124,18 +149,28 @@ class MenuItem extends UIComponent, any> { 'pills', 'pointing', 'styles', + 'submenu', + 'submenuOpened', 'type', 'underlined', 'variables', 'vertical', ] + getInitialAutoControlledState() { + return { submenuOpened: false } + } + handleClick = e => { + if (this.props.submenu) { + this.setState({ submenuOpened: !this.state.submenuOpened }) + } + _.invoke(this.props, 'onClick', e, this.props) } renderComponent({ ElementType, classes, accessibility, rest }) { - const { children, content, icon } = this.props + const { children, content, icon, submenu, variables } = this.props return ( @@ -154,6 +189,15 @@ class MenuItem extends UIComponent, any> { {content} )} + + {this.state.submenuOpened && + Menu.create(submenu, { + overrideProps: { + className: classes.submenu, + vertical: true, + variables, + }, + })} ) } diff --git a/src/themes/teams/components/Menu/menuItemStyles.ts b/src/themes/teams/components/Menu/menuItemStyles.ts index af6b552689..a22560f493 100644 --- a/src/themes/teams/components/Menu/menuItemStyles.ts +++ b/src/themes/teams/components/Menu/menuItemStyles.ts @@ -116,6 +116,7 @@ const menuItemStyles = { : { marginLeft: iconsMenuItemSpacing }), }, }), + ...(!vertical && variables.menuItemWidth && { width: pxToRem(variables.menuItemWidth) }), ...(pills && { ...(vertical ? { margin: `0 0 ${pxToRem(5)} 0` } : { margin: `0 ${pxToRem(8)} 0 0` }), borderRadius: pxToRem(5), @@ -238,6 +239,11 @@ const menuItemStyles = { }), } }, + submenu: { + position: 'absolute', + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + }, } export default menuItemStyles diff --git a/src/themes/teams/components/Menu/menuStyles.ts b/src/themes/teams/components/Menu/menuStyles.ts index 49fd533eee..119e3d425d 100644 --- a/src/themes/teams/components/Menu/menuStyles.ts +++ b/src/themes/teams/components/Menu/menuStyles.ts @@ -13,7 +13,7 @@ export default { display: 'flex', ...(vertical && { flexDirection: 'column', - ...(!fluid && { width: pxToRem(200) }), + ...(!fluid && { width: pxToRem(variables.menuItemWidth || 200) }), ...(iconOnly && { display: 'inline-block', width: 'auto', diff --git a/src/themes/teams/components/Menu/menuVariables.ts b/src/themes/teams/components/Menu/menuVariables.ts index 67dcd88908..c615322943 100644 --- a/src/themes/teams/components/Menu/menuVariables.ts +++ b/src/themes/teams/components/Menu/menuVariables.ts @@ -16,6 +16,7 @@ export interface IMenuVariables { iconsMenuItemSize?: string iconsMenuItemSpacing: number | string + menuItemWidth: number } export default (siteVars: any): IMenuVariables => { @@ -35,5 +36,6 @@ export default (siteVars: any): IMenuVariables => { iconsMenuItemSize: undefined, iconsMenuItemSpacing: 0, + menuItemWidth: undefined, } } diff --git a/test/specs/components/Menu/MenuItem-test.tsx b/test/specs/components/Menu/MenuItem-test.tsx index b58fb28ca9..86bef879a7 100644 --- a/test/specs/components/Menu/MenuItem-test.tsx +++ b/test/specs/components/Menu/MenuItem-test.tsx @@ -1,7 +1,8 @@ import * as React from 'react' import { isConformant, handlesAccessibility } from 'test/specs/commonTests' -import { getTestingRenderedComponent } from 'test/utils' +import { getTestingRenderedComponent, mountWithProvider } from 'test/utils' + import MenuItem from 'src/components/Menu/MenuItem' describe('MenuItem', () => { @@ -29,4 +30,19 @@ describe('MenuItem', () => { expect(menuItem.find('.ui-menu__item').is('li')).toBe(true) expect(menuItem.text()).toBe('Home') }) + + it('menu item renders submenu after click on it', () => { + const submenu = { + vertical: true, + items: [ + { key: 'new', content: 'New' }, + { key: 'open', content: 'Open' }, + { key: 'edit', content: 'Edit' }, + ], + } + const menuItem = mountWithProvider() + expect(menuItem.find('.ui-menu').length).toBe(0) + menuItem.find('a').simulate('click') + expect(menuItem.find('.ui-menu').is('ul')).toBe(true) + }) })