diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54048534ff..94479e015c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
+### BREAKING CHANGES
+- renamed Teams theme menu variables the contains props names as prefixes @mnajdova ([#539](https://github.com/stardust-ui/react/pull/539))
+
### Fixes
- Ensure `Popup` properly flips values of `offset` prop in RTL @kuzhelov ([#612](https://github.com/stardust-ui/react/pull/612))
- Fix `List` - items should be selectable @sophieH29 ([#566](https://github.com/stardust-ui/react/pull/566))
@@ -25,6 +28,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `color` prop to `Text` component @Bugaa92 ([#597](https://github.com/stardust-ui/react/pull/597))
- Add `color` prop to `Header` and `HeaderDescription` components @Bugaa92 ([#628](https://github.com/stardust-ui/react/pull/628))
- Add and export compose icons in Teams theme @joheredi ([#639](https://github.com/stardust-ui/react/pull/639))
+- Add `menu` prop to `MenuItem` @mnajdova ([#539](https://github.com/stardust-ui/react/pull/539))
## [v0.15.0](https://github.com/stardust-ui/react/tree/v0.15.0) (2018-12-17)
diff --git a/docs/src/components/CodeSnippet.tsx b/docs/src/components/CodeSnippet.tsx
index 357a5c3239..182a4e6c90 100644
--- a/docs/src/components/CodeSnippet.tsx
+++ b/docs/src/components/CodeSnippet.tsx
@@ -1,4 +1,3 @@
-import * as _ from 'lodash'
import * as React from 'react'
import formatCode from '../utils/formatCode'
diff --git a/docs/src/examples/components/Menu/Types/MenuExampleVerticalWithSubmenu.shorthand.tsx b/docs/src/examples/components/Menu/Types/MenuExampleVerticalWithSubmenu.shorthand.tsx
new file mode 100644
index 0000000000..1fc9b1bfde
--- /dev/null
+++ b/docs/src/examples/components/Menu/Types/MenuExampleVerticalWithSubmenu.shorthand.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react'
+import { Menu } from '@stardust-ui/react'
+
+const items = [
+ {
+ key: 'editorials',
+ content: 'Editorials',
+ menu: {
+ items: [
+ { key: '1', content: 'item1' },
+ {
+ key: '2',
+ content: 'item2',
+ menu: { items: [{ key: '1', content: 'item1' }, { key: '2', content: 'item2' }] },
+ },
+ ],
+ },
+ },
+ { key: 'review', content: 'Reviews' },
+ { key: 'events', content: 'Upcoming Events' },
+]
+
+const MenuExampleVerticalWithSubmenu = () =>
+
+export default MenuExampleVerticalWithSubmenu
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..44929b1aa7
--- /dev/null
+++ b/docs/src/examples/components/Menu/Types/MenuExampleWithSubmenu.shorthand.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react'
+import { Menu } from '@stardust-ui/react'
+
+const items = [
+ {
+ key: 'editorials',
+ content: 'Editorials',
+ menu: {
+ items: [
+ { key: '1', content: 'item1' },
+ {
+ key: '2',
+ content: 'item2',
+ menu: { items: [{ key: '1', content: 'item2.1' }, { key: '2', content: 'item2.2' }] },
+ },
+ {
+ key: '3',
+ content: 'item3',
+ menu: { items: [{ key: '1', content: 'item3.1' }, { key: '2', content: 'item3.2' }] },
+ },
+ ],
+ },
+ },
+ {
+ key: 'review',
+ content: 'Reviews',
+ menu: {
+ items: [
+ { key: '1', content: 'item1' },
+ {
+ key: '2',
+ content: 'item2',
+ menu: { items: [{ key: '1', content: 'item2.1' }, { key: '2', content: 'item2.2' }] },
+ },
+ ],
+ },
+ },
+ { key: 'events', content: 'Upcoming Events' },
+]
+
+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 632620ebd3..93239cded7 100644
--- a/docs/src/examples/components/Menu/Types/index.tsx
+++ b/docs/src/examples/components/Menu/Types/index.tsx
@@ -19,6 +19,12 @@ const Types = () => (
description="A vertical menu displays elements vertically."
examplePath="components/Menu/Types/MenuExampleVertical"
/>
+
+
)
diff --git a/docs/src/examples/components/Menu/Variations/MenuExampleIconOnlyPrimaryInverted.shorthand.tsx b/docs/src/examples/components/Menu/Variations/MenuExampleIconOnlyPrimaryInverted.shorthand.tsx
index 79df9ddee8..19848defe8 100644
--- a/docs/src/examples/components/Menu/Variations/MenuExampleIconOnlyPrimaryInverted.shorthand.tsx
+++ b/docs/src/examples/components/Menu/Variations/MenuExampleIconOnlyPrimaryInverted.shorthand.tsx
@@ -14,9 +14,9 @@ const MenuExampleIconOnlyPrimaryInverted = () => (
items={items}
primary
variables={siteVars => ({
- defaultColor: siteVars.gray06,
- defaultBackgroundColor: siteVars.brand,
- typePrimaryActiveBorderColor: siteVars.white,
+ color: siteVars.gray06,
+ backgroundColor: siteVars.brand,
+ primaryActiveBorderColor: siteVars.white,
})}
/>
)
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx
index 0aa98d68aa..85bebbdce1 100644
--- a/src/components/Menu/Menu.tsx
+++ b/src/components/Menu/Menu.tsx
@@ -5,6 +5,7 @@ import * as React from 'react'
import {
AutoControlledComponent,
childrenExist,
+ createShorthandFactory,
customPropTypes,
UIComponentProps,
ChildrenComponentProps,
@@ -60,12 +61,19 @@ export interface MenuProps extends UIComponentProps, ChildrenComponentProps {
/** A vertical menu displays elements vertically. */
vertical?: boolean
+
+ /** Indicates whether the menu is submenu. */
+ submenu?: boolean
+}
+
+export interface MenuState {
+ activeIndex?: number | string
}
/**
* A menu displays grouped navigation actions.
*/
-class Menu extends AutoControlledComponent, any> {
+class Menu extends AutoControlledComponent, MenuState> {
static displayName = 'Menu'
static className = 'ui-menu'
@@ -88,6 +96,7 @@ class Menu extends AutoControlledComponent, any> {
secondary: customPropTypes.every([customPropTypes.disallow(['primary']), PropTypes.bool]),
underlined: PropTypes.bool,
vertical: PropTypes.bool,
+ submenu: PropTypes.bool,
}
static defaultProps = {
@@ -107,6 +116,15 @@ class Menu extends AutoControlledComponent, any> {
_.invoke(predefinedProps, 'onClick', e, itemProps)
},
+ onActiveChanged: (e, props) => {
+ const { index, active } = props
+ if (active) {
+ this.trySetState({ activeIndex: index })
+ } else if (this.state.activeIndex === index) {
+ this.trySetState({ activeIndex: null })
+ }
+ _.invoke(predefinedProps, 'onActiveChanged', e, props)
+ },
})
renderItems = (variables: ComponentVariablesObject) => {
@@ -119,11 +137,14 @@ class Menu extends AutoControlledComponent, any> {
secondary,
underlined,
vertical,
+ submenu,
} = this.props
const { activeIndex } = this.state
- return _.map(items, (item, index) =>
- MenuItem.create(item, {
+ return _.map(items, (item, index) => {
+ const active =
+ (typeof activeIndex === 'string' ? parseInt(activeIndex, 10) : activeIndex) === index
+ return MenuItem.create(item, {
defaultProps: {
iconOnly,
pills,
@@ -134,11 +155,12 @@ class Menu extends AutoControlledComponent, any> {
variables,
vertical,
index,
- active: parseInt(activeIndex, 10) === index,
+ active,
+ inSubmenu: submenu,
},
overrideProps: this.handleItemOverrides,
- }),
- )
+ })
+ })
}
renderComponent({ ElementType, classes, accessibility, variables, rest }) {
@@ -151,4 +173,6 @@ class Menu extends AutoControlledComponent, any> {
}
}
+Menu.create = createShorthandFactory(Menu, 'items')
+
export default Menu
diff --git a/src/components/Menu/MenuItem.tsx b/src/components/Menu/MenuItem.tsx
index 7b7837c0a9..143734a42c 100644
--- a/src/components/Menu/MenuItem.tsx
+++ b/src/components/Menu/MenuItem.tsx
@@ -4,21 +4,26 @@ import * as PropTypes from 'prop-types'
import * as React from 'react'
import {
+ AutoControlledComponent,
childrenExist,
createShorthandFactory,
customPropTypes,
- UIComponent,
+ doesNodeContainClick,
UIComponentProps,
ChildrenComponentProps,
ContentComponentProps,
commonPropTypes,
isFromKeyboard,
+ EventStack,
} from '../../lib'
import Icon from '../Icon/Icon'
+import Menu from '../Menu/Menu'
import Slot from '../Slot/Slot'
-import { menuItemBehavior } from '../../lib/accessibility'
+import { menuItemBehavior, submenuBehavior } from '../../lib/accessibility'
import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types'
import { ComponentEventHandler, Extendable, ShorthandValue } from '../../../types/utils'
+import { focusAsync } from '../../lib/accessibility/FocusZone'
+import Ref from '../Ref/Ref'
export interface MenuItemProps
extends UIComponentProps,
@@ -55,6 +60,14 @@ export interface MenuItemProps
*/
onClick?: ComponentEventHandler
+ /**
+ * Called on key down pressed.
+ *
+ * @param {SyntheticEvent} event - React's original SyntheticEvent.
+ * @param {object} data - All props.
+ */
+ onKeyDown?: ComponentEventHandler
+
/** A menu can adjust its appearance to de-emphasize its contents. */
pills?: boolean
@@ -78,16 +91,32 @@ export interface MenuItemProps
/** Shorthand for the wrapper component. */
wrapper?: ShorthandValue
+
+ /** Shorthand for the submenu. */
+ menu?: ShorthandValue
+
+ /** Indicates if the menu inside the item is open. */
+ menuOpen?: boolean
+
+ /** Default menu open */
+ defaultMenuOpen?: boolean
+
+ /** Callback for setting the current menu item as active element in the menu. */
+ onActiveChanged?: ComponentEventHandler
+
+ /** Indicates whether the menu item is part of submenu. */
+ inSubmenu?: boolean
}
export interface MenuItemState {
isFromKeyboard: boolean
+ menuOpen: boolean
}
/**
* A menu item is an actionable navigation item within a menu.
*/
-class MenuItem extends UIComponent, MenuItemState> {
+class MenuItem extends AutoControlledComponent, MenuItemState> {
static displayName = 'MenuItem'
static className = 'ui-menu__item'
@@ -110,6 +139,11 @@ class MenuItem extends UIComponent, MenuItemState> {
underlined: PropTypes.bool,
vertical: PropTypes.bool,
wrapper: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
+ menu: customPropTypes.itemShorthand,
+ menuOpen: PropTypes.bool,
+ defaultMenuOpen: PropTypes.bool,
+ onActiveChanged: PropTypes.func,
+ inSubmenu: PropTypes.bool,
}
static defaultProps = {
@@ -118,32 +152,65 @@ class MenuItem extends UIComponent, MenuItemState> {
wrapper: { as: 'li' },
}
- state = {
- isFromKeyboard: false,
+ static autoControlledProps = ['menuOpen']
+
+ private outsideClickSubscription = EventStack.noSubscription
+
+ private menuRef = React.createRef()
+ private itemRef = React.createRef()
+
+ public componentDidMount() {
+ this.updateOutsideClickSubscription()
+ }
+
+ public componentDidUpdate() {
+ this.updateOutsideClickSubscription()
+ }
+
+ public componentWillUnmount() {
+ this.outsideClickSubscription.unsubscribe()
}
- renderComponent({ ElementType, classes, accessibility, rest }) {
- const { children, content, icon, wrapper } = this.props
+ renderComponent({ ElementType, classes, accessibility, rest, styles }) {
+ const { children, content, icon, wrapper, menu, primary, secondary, active } = this.props
+
+ const { menuOpen } = this.state
const menuItemInner = childrenExist(children) ? (
children
) : (
-
- {icon &&
- Icon.create(this.props.icon, {
- defaultProps: { xSpacing: !!content ? 'after' : 'none' },
- })}
- {content}
-
+ [
+
+ {icon &&
+ Icon.create(this.props.icon, {
+ defaultProps: { xSpacing: !!content ? 'after' : 'none' },
+ })}
+ {content}
+
+ ]
)
+ const maybeSubmenu =
+ menu && active && menuOpen ? (
+ [
+ {Menu.create(menu, {
+ defaultProps: {
+ accessibility: submenuBehavior,
+ vertical: true,
+ primary,
+ secondary,
+ styles: styles.menu,
+ submenu: true,
+ },
+ })}
+ ]
+ ) : null
if (wrapper) {
return Slot.create(wrapper, {
@@ -153,18 +220,70 @@ class MenuItem extends UIComponent, MenuItemState> {
...accessibility.keyHandlers.root,
},
overrideProps: () => ({
- children: menuItemInner,
+ children: (
+ <>
+ {menuItemInner}
+ {maybeSubmenu}
+ >
+ ),
+ onClick: this.handleClick,
+ onBlur: this.handleWrapperBlur,
}),
})
}
return menuItemInner
}
+ private handleWrapperBlur = e => {
+ if (!this.props.inSubmenu && !e.currentTarget.contains(e.relatedTarget)) {
+ this.setState({ menuOpen: false })
+ }
+ }
+
protected actionHandlers: AccessibilityActionHandlers = {
performClick: event => this.handleClick(event),
+ openMenu: event => this.openMenu(event),
+ closeAllMenus: event => this.closeAllMenus(event, false),
+ closeAllMenusAndFocusNextParentItem: event => this.closeAllMenus(event, true),
+ closeMenu: event => this.closeMenu(event),
+ }
+
+ private updateOutsideClickSubscription() {
+ this.outsideClickSubscription.unsubscribe()
+
+ if (this.props.menu && this.state.menuOpen) {
+ setTimeout(() => {
+ this.outsideClickSubscription = EventStack.subscribe('click', this.outsideClickHandler)
+ })
+ }
+ }
+
+ private outsideClickHandler = e => {
+ if (!this.state.menuOpen) return
+ if (
+ !doesNodeContainClick(this.itemRef.current, e) &&
+ !doesNodeContainClick(this.menuRef.current, e)
+ ) {
+ this.trySetState({ menuOpen: false })
+ }
+ }
+
+ private performClick = e => {
+ const { active, menu } = this.props
+ if (menu) {
+ if (doesNodeContainClick(this.menuRef.current, e)) {
+ // submenu was clicked => close it and propagate
+ this.setState({ menuOpen: false }, () => focusAsync(this.itemRef.current))
+ } else {
+ // the menuItem element was clicked => toggle the open/close and stop propagation
+ this.trySetState({ menuOpen: active ? !this.state.menuOpen : true })
+ e.stopPropagation()
+ }
+ }
}
private handleClick = e => {
+ this.performClick(e)
_.invoke(this.props, 'onClick', e, this.props)
}
@@ -179,6 +298,45 @@ class MenuItem extends UIComponent, MenuItemState> {
_.invoke(this.props, 'onFocus', e, this.props)
}
+
+ private closeAllMenus = (e, focusNextParent: boolean) => {
+ const { menu, inSubmenu } = this.props
+ const { menuOpen } = this.state
+ if (menu && menuOpen) {
+ this.setState({ menuOpen: false }, () => {
+ if (!inSubmenu && (!focusNextParent || this.props.vertical)) {
+ focusAsync(this.itemRef.current)
+ }
+ })
+ }
+ }
+
+ private closeMenu = e => {
+ const { menu, inSubmenu } = this.props
+ const { menuOpen } = this.state
+ const shouldStopPropagation = inSubmenu || this.props.vertical
+ if (menu && menuOpen) {
+ this.setState({ menuOpen: false }, () => {
+ if (shouldStopPropagation) {
+ focusAsync(this.itemRef.current)
+ }
+ })
+ if (shouldStopPropagation) {
+ e.stopPropagation()
+ }
+ }
+ }
+
+ private openMenu = e => {
+ const { menu } = this.props
+ const { menuOpen } = this.state
+ if (menu && !menuOpen) {
+ this.setState({ menuOpen: true })
+ _.invoke(this.props, 'onActiveChanged', e, { ...this.props, active: true })
+ e.stopPropagation()
+ e.preventDefault()
+ }
+ }
}
MenuItem.create = createShorthandFactory(MenuItem, 'content')
diff --git a/src/index.ts b/src/index.ts
index 2edc275517..b324c0fc66 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -74,7 +74,7 @@ export { default as Layout, LayoutPropsWithDefaults, LayoutProps } from './compo
export { default as List, ListProps } from './components/List/List'
export { default as ListItem, ListItemProps } from './components/List/ListItem'
-export { default as Menu, MenuProps } from './components/Menu/Menu'
+export { default as Menu, MenuProps, MenuState } from './components/Menu/Menu'
export { default as MenuItem, MenuItemState, MenuItemProps } from './components/Menu/MenuItem'
export { default as Popup, PopupState, PopupProps } from './components/Popup/Popup'
diff --git a/src/lib/accessibility/Behaviors/Menu/menuBehavior.ts b/src/lib/accessibility/Behaviors/Menu/menuBehavior.ts
index 377936f75f..bba1918654 100644
--- a/src/lib/accessibility/Behaviors/Menu/menuBehavior.ts
+++ b/src/lib/accessibility/Behaviors/Menu/menuBehavior.ts
@@ -1,4 +1,5 @@
import { Accessibility, FocusZoneMode } from '../../types'
+import { FocusZoneDirection } from '../../FocusZone'
/**
* @description
@@ -21,6 +22,7 @@ const menuBehavior: Accessibility = (props: any) => ({
isCircularNavigation: true,
preventDefaultWhenHandled: true,
shouldFocusFirstElementWhenReceivedFocus: true,
+ direction: props.vertical ? FocusZoneDirection.vertical : FocusZoneDirection.horizontal,
},
},
})
diff --git a/src/lib/accessibility/Behaviors/Menu/menuItemBehavior.ts b/src/lib/accessibility/Behaviors/Menu/menuItemBehavior.ts
index f00c978834..730b783f9c 100644
--- a/src/lib/accessibility/Behaviors/Menu/menuItemBehavior.ts
+++ b/src/lib/accessibility/Behaviors/Menu/menuItemBehavior.ts
@@ -34,10 +34,24 @@ const menuItemBehavior: Accessibility = (props: any) => ({
handledProps: ['aria-label', 'aria-labelledby', 'aria-describedby'],
keyActions: {
- anchor: {
+ root: {
performClick: {
keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }],
},
+ closeAllMenus: {
+ keyCombinations: [{ keyCode: keyboardKey.Escape }],
+ },
+ closeAllMenusAndFocusNextParentItem: {
+ keyCombinations: [{ keyCode: keyboardKey.ArrowRight }],
+ },
+ closeMenu: {
+ keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }],
+ },
+ openMenu: {
+ keyCombinations: [
+ { keyCode: props.vertical ? keyboardKey.ArrowRight : keyboardKey.ArrowDown },
+ ],
+ },
},
},
})
diff --git a/src/lib/accessibility/Behaviors/Menu/submenuBehavior.ts b/src/lib/accessibility/Behaviors/Menu/submenuBehavior.ts
new file mode 100644
index 0000000000..2ef65e9e3e
--- /dev/null
+++ b/src/lib/accessibility/Behaviors/Menu/submenuBehavior.ts
@@ -0,0 +1,31 @@
+import { Accessibility, FocusZoneMode } from '../../types'
+import { FocusZoneDirection } from '../../FocusZone'
+
+/**
+ * @description
+ * The 'menu' role is used to identify an element that creates a list of common actions or functions that a user can invoke.
+ *
+ * @specification
+ * Adds role='menu'.
+ * Embeds FocusZone into component allowing circular arrow key navigation through the children of the component.
+ */
+
+const submenuBehavior: Accessibility = (props: any) => ({
+ attributes: {
+ root: {
+ role: 'menu',
+ },
+ },
+ focusZone: {
+ mode: FocusZoneMode.Embed,
+ props: {
+ isCircularNavigation: true,
+ preventDefaultWhenHandled: true,
+ shouldFocusFirstElementWhenReceivedFocus: true,
+ shouldFocusOnMount: true,
+ direction: FocusZoneDirection.vertical,
+ },
+ },
+})
+
+export default submenuBehavior
diff --git a/src/lib/accessibility/index.ts b/src/lib/accessibility/index.ts
index f795558a7d..82bdf084d2 100644
--- a/src/lib/accessibility/index.ts
+++ b/src/lib/accessibility/index.ts
@@ -4,6 +4,7 @@ export { default as toggleButtonBehavior } from './Behaviors/Button/toggleButton
export { default as imageBehavior } from './Behaviors/Image/imageBehavior'
export { default as menuBehavior } from './Behaviors/Menu/menuBehavior'
export { default as menuItemBehavior } from './Behaviors/Menu/menuItemBehavior'
+export { default as submenuBehavior } from './Behaviors/Menu/submenuBehavior'
export { default as basicListBehavior } from './Behaviors/List/listBehavior'
export { default as basicListItemBehavior } from './Behaviors/List/basicListItemBehavior'
export { default as listBehavior } from './Behaviors/List/listBehavior'
diff --git a/src/themes/teams/components/Menu/menuItemStyles.ts b/src/themes/teams/components/Menu/menuItemStyles.ts
index 4bc41f493c..36ef09fbf5 100644
--- a/src/themes/teams/components/Menu/menuItemStyles.ts
+++ b/src/themes/teams/components/Menu/menuItemStyles.ts
@@ -20,10 +20,10 @@ const getActionStyles = ({
variables: MenuVariables
color: string
}): ICSSInJSStyle =>
- (underlined && !isFromKeyboard) || iconOnly
+ underlined || iconOnly
? {
color,
- background: v.defaultBackgroundColor,
+ background: v.backgroundColor,
}
: primary
? {
@@ -32,9 +32,38 @@ const getActionStyles = ({
}
: {
color,
- background: v.defaultActiveBackgroundColor,
+ background: v.activeBackgroundColor,
}
+const getFocusedStyles = ({
+ props,
+ variables: v,
+ color,
+}: {
+ props: MenuItemPropsAndState
+ variables: MenuVariables
+ color: string
+}): ICSSInJSStyle => {
+ const { primary, underlined, iconOnly, isFromKeyboard, active } = props
+ if (active && !underlined) return {}
+ return {
+ ...((underlined && !isFromKeyboard) || iconOnly
+ ? {
+ color,
+ background: v.backgroundColor,
+ }
+ : primary
+ ? {
+ color: v.primaryFocusedColor,
+ background: v.primaryFocusedBackgroundColor,
+ }
+ : {
+ color,
+ background: v.focusedBackgroundColor,
+ }),
+ }
+}
+
const itemSeparator: ComponentSlotStyleFunction = ({
props,
variables: v,
@@ -52,16 +81,8 @@ const itemSeparator: ComponentSlotStyleFunction ({
+ zIndex: '1000',
+ position: 'absolute',
+ top: vertical ? '0' : '100%',
+ left: vertical ? '100%' : '0',
+ }),
}
export default menuItemStyles
diff --git a/src/themes/teams/components/Menu/menuStyles.ts b/src/themes/teams/components/Menu/menuStyles.ts
index fc6cc2d2c0..d3fc40d934 100644
--- a/src/themes/teams/components/Menu/menuStyles.ts
+++ b/src/themes/teams/components/Menu/menuStyles.ts
@@ -1,6 +1,9 @@
import { pxToRem } from '../../utils'
import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types'
-import { MenuProps } from '../../../../components/Menu/Menu'
+import { MenuProps, MenuState } from '../../../../components/Menu/Menu'
+import { MenuVariables } from './menuVariables'
+
+type MenuPropsAndState = MenuProps & MenuState
const solidBorder = (color: string) => ({
border: `1px solid ${color}`,
@@ -24,12 +27,11 @@ export default {
!iconOnly &&
!(pointing && vertical) &&
!underlined && {
- ...solidBorder(variables.defaultBorderColor),
+ ...solidBorder(variables.borderColor),
...(primary && {
...solidBorder(variables.primaryBorderColor),
}),
borderRadius: pxToRem(4),
- overflow: 'hidden',
}),
...(underlined && {
borderBottom: `2px solid ${variables.primaryUnderlinedBorderColor}`,
@@ -40,4 +42,4 @@ export default {
listStyleType: 'none',
}
},
-} as ComponentSlotStylesInput
+} as ComponentSlotStylesInput
diff --git a/src/themes/teams/components/Menu/menuVariables.ts b/src/themes/teams/components/Menu/menuVariables.ts
index 9282b21e85..3558f204b1 100644
--- a/src/themes/teams/components/Menu/menuVariables.ts
+++ b/src/themes/teams/components/Menu/menuVariables.ts
@@ -1,43 +1,57 @@
import { pxToRem } from '../../utils'
export interface MenuVariables {
- defaultColor: string
- defaultBackgroundColor: string
+ color: string
+ backgroundColor: string
- defaultActiveColor: string
- defaultActiveBackgroundColor: string
- defaultBorderColor: string
+ activeColor: string
+ activeBackgroundColor: string
+ focusedBackgroundColor: string
+ borderColor: string
primaryActiveColor: string
primaryActiveBackgroundColor: string
primaryActiveBorderColor: string
+ primaryFocusedColor: string
+ primaryFocusedBackgroundColor: string
+
primaryBorderColor: string
primaryHoverBorderColor: string
primaryUnderlinedBorderColor: string
circularRadius: string
lineHeightBase: string
+
+ submenuIndicatorContent: string
+ submenuIndicatorRotationAngle: number
}
export default (siteVars: any): MenuVariables => {
return {
- defaultColor: siteVars.gray02,
- defaultBackgroundColor: 'transparent',
+ color: siteVars.gray02,
+ backgroundColor: siteVars.white,
- defaultActiveColor: siteVars.black,
- defaultActiveBackgroundColor: siteVars.gray10,
- defaultBorderColor: siteVars.gray08,
+ activeColor: siteVars.black,
+ activeBackgroundColor: siteVars.gray10,
+ focusedBackgroundColor: siteVars.gray14,
+ borderColor: siteVars.gray08,
primaryActiveColor: siteVars.white,
primaryActiveBackgroundColor: siteVars.brand08,
primaryActiveBorderColor: siteVars.brand,
+ primaryFocusedColor: siteVars.white,
+ primaryFocusedBackgroundColor: siteVars.brand12,
+
primaryBorderColor: siteVars.brand08,
primaryHoverBorderColor: siteVars.gray08,
primaryUnderlinedBorderColor: siteVars.gray08,
circularRadius: pxToRem(999),
lineHeightBase: siteVars.lineHeightBase,
+
+ submenuIndicatorContent: '">"',
+ submenuIndicatorRotationAngle: 90,
}
}
diff --git a/test/specs/behaviors/behavior-test.tsx b/test/specs/behaviors/behavior-test.tsx
index dd8b411b77..c6ea5bf2c8 100644
--- a/test/specs/behaviors/behavior-test.tsx
+++ b/test/specs/behaviors/behavior-test.tsx
@@ -13,6 +13,7 @@ import {
inputBehavior,
menuBehavior,
menuItemBehavior,
+ submenuBehavior,
popupBehavior,
popupFocusTrapBehavior,
dialogBehavior,
@@ -44,6 +45,7 @@ testHelper.addBehavior('inputBehavior', inputBehavior)
testHelper.addBehavior('imageBehavior', imageBehavior)
testHelper.addBehavior('menuBehavior', menuBehavior)
testHelper.addBehavior('menuItemBehavior', menuItemBehavior)
+testHelper.addBehavior('submenuBehavior', submenuBehavior)
testHelper.addBehavior('popupBehavior', popupBehavior)
testHelper.addBehavior('popupFocusTrapBehavior', popupFocusTrapBehavior)
testHelper.addBehavior('radioGroupBehavior', radioGroupBehavior)
diff --git a/test/specs/commonTests/isConformant.tsx b/test/specs/commonTests/isConformant.tsx
index 5836b629b2..a024a4391e 100644
--- a/test/specs/commonTests/isConformant.tsx
+++ b/test/specs/commonTests/isConformant.tsx
@@ -19,6 +19,7 @@ import { FOCUSZONE_WRAP_ATTRIBUTE } from 'src/lib/accessibility/FocusZone/focusU
export interface Conformant {
eventTargets?: object
+ nestingLevel?: number
requiredProps?: object
exportedAtTopLevel?: boolean
rendersPortal?: boolean
@@ -39,6 +40,7 @@ export default (Component, options: Conformant = {}) => {
const {
eventTargets = {},
exportedAtTopLevel = true,
+ nestingLevel = 0,
requiredProps = {},
rendersPortal = false,
usesWrapperSlot = false,
@@ -51,11 +53,18 @@ export default (Component, options: Conformant = {}) => {
const getComponent = (wrapper: ReactWrapper) => {
// FelaTheme wrapper and the component itself:
let component = wrapper
- .childAt(0)
- .childAt(0)
- .childAt(0)
+
+ /**
+ * The wrapper is mounted with Provider, so in total there are three HOC components
+ * that we want to get rid of: ThemeProvider, the actual Component and FelaTheme,
+ * in order to be able to get to the actual rendered result of the component we are testing
+ */
+ _.times(nestingLevel + 3, () => {
+ component = component.childAt(0)
+ })
+
if (component.type() === FocusZone) {
- // `component` is
+ // another HOC component is added: FocuZone
component = component.childAt(0) // skip through
if (component.prop(FOCUSZONE_WRAP_ATTRIBUTE)) {
component = component.childAt(0) // skip the additional wrap of the FocusZone
@@ -63,10 +72,14 @@ export default (Component, options: Conformant = {}) => {
}
if (usesWrapperSlot) {
- component = component
- .childAt(0)
- .childAt(0)
- .childAt(0)
+ /**
+ * If there is a wrapper slot, then again, we need to get rid of all three HOC components:
+ * ThemeProvider, Wrapper (Slot), and FelaTheme in order to be able to get to the actual
+ * rendered result of the component we are testing
+ */
+ _.times(3, () => {
+ component = component.childAt(0)
+ })
}
return component
diff --git a/test/specs/components/Menu/MenuItem-test.tsx b/test/specs/components/Menu/MenuItem-test.tsx
index d85082d9e4..c24ea2547c 100644
--- a/test/specs/components/Menu/MenuItem-test.tsx
+++ b/test/specs/components/Menu/MenuItem-test.tsx
@@ -8,8 +8,10 @@ import { toolbarButtonBehavior, tabBehavior } from '../../../../src/lib/accessib
describe('MenuItem', () => {
isConformant(MenuItem, {
eventTargets: {
- onClick: 'a',
+ onClick: '.ui-menu__item__wrapper',
},
+ // The ElementType is wrapped with Ref, which is adding two HOC in total
+ nestingLevel: 2,
usesWrapperSlot: true,
})
@@ -19,7 +21,14 @@ describe('MenuItem', () => {
.hostNodes()
expect(menuItem.is('li')).toBe(true)
- expect(menuItem.childAt(0).is('a')).toBe(true)
+ // The ElementType is wrapped with Ref, which is adding two HOC in total, that's why we need the three childAt(0) usages
+ expect(
+ menuItem
+ .childAt(0)
+ .childAt(0)
+ .childAt(0)
+ .is('a'),
+ ).toBe(true)
expect(menuItem.text()).toBe('Home')
})