Repository: uber-web/baseui
Pull request #1016 (2019-03-13 22:20:17)

feat(menu): enables keyboard navigation between nested menus, provides proper focus feedback
#### Description

<img width="633" alt="Screen Shot 2019-03-13 at 3 05 44 PM" src="https://user-images.githubusercontent.com/5317799/54317821-82526580-45a1-11e9-94c2-dae448d4a511.png">

- **Ensures targeted focus coloring** - Only one menu may be 'active' at any time. If the menu is active, highlighted menu item is blue. If inactive, highlighted menu item is black.
- **Fixes doubled item highlight** - Bug is described in [this issue](https://github.com/uber-web/baseui/issues/993). Mouse hover now triggers a state update, and this change removes highlight from css `:hover` pseudo-selector. Keyboard navigation between items will overwrite a mouse highlight and vice-versa. Always prefers the most recent action.
- **Fixes keyboard navigation with multiple menus** - Only initializes keyboard navigation on menu focus. Previously if multiple menus were on the page, they would all change the highlighted item.
- **Enables keyboard navigation between nested menus** - Adds a new `NestedMenu` component to wrap nested menus to track refs for each. Left arrow navigates to parent menu, right arrow navigates to child menu. This works through context.
- **Adds more contextual child menu example** - it's now a 'file' menu, rather than a series of numbers and also shows how only a couple items create a child menu.

#### Scope

- [x] Minor: New Feature
- [x] Manually tested `Select` and `Datepicker` components for stability.
- [x] unit/e2e tests
- [x] update the disabled item colors

nadiia: Two minor issues from the example in docs:
- The tab order seems to be odd when tabbing from the submenu.
- The submenu stays open when tabbed outside of the menu.
chasestarr: updated so that tabbing is trapped when focused on child menus. restricts keyboard navigation to arrows
tajo: Looks and works great!

Patch:
diff --git a/documentation-site/static/examples/menu/child.js b/documentation-site/static/examples/menu/child.js
index 8c6189bd5..15923c876 100644
--- a/documentation-site/static/examples/menu/child.js
+++ b/documentation-site/static/examples/menu/child.js
@@ -1,50 +1,77 @@
 import React from 'react';
-import {Menu, StatefulMenu} from 'baseui/menu';
+import {StatefulMenu, NestedMenus} from 'baseui/menu';
 
-const ITEMS = [
-  {label: 'Item One'},
-  {label: 'Item Two'},
-  {label: 'Item Three'},
-  {label: 'Item Four'},
-  {label: 'Item Five'},
-  {label: 'Item Six'},
-  {label: 'Item Seven'},
-  {label: 'Item Eight'},
-  {label: 'Item Nine'},
-  {label: 'Item Ten'},
-  {label: 'Item Eleven'},
-  {label: 'Item Twelve'},
+const OPEN_RECENT = 'Open Recent ->';
+const NEW_BREAKPOINT = 'New Breakpoint ->';
+const FILE = [
+  {label: 'New File'},
+  {label: 'New Window'},
+  {label: 'Open...'},
+  {label: 'Open Workspace...'},
+  {label: OPEN_RECENT},
+  {label: 'Add Folder to Workspace...'},
+  {label: 'Save'},
+  {label: 'Save As...'},
+  {label: 'Toggle Breakpoint'},
+  {label: NEW_BREAKPOINT},
+  {label: 'Close Folder'},
+  {label: 'Close Window'},
 ];
 
-const ThirdMenu = () => (
-  <Menu
-    items={ITEMS}
-    overrides={{List: {style: {width: '200px'}}}}
-    rootRef={React.createRef()}
-  />
-);
+const RECENT_FILES = [
+  {label: 'Reopen Closed Editor'},
+  {label: '~/workspace/baseui'},
+  {label: '~/workspace/styletron'},
+  {label: '~/workspace/my-project'},
+  {label: 'More...'},
+  {label: 'Clear Recently Opened'},
+];
 
-const SecondMenu = () => (
-  <Menu
-    items={ITEMS}
-    overrides={{
-      List: {style: {width: '200px'}},
-      Option: {props: {getChildMenu: ThirdMenu}},
-    }}
-    rootRef={React.createRef()}
-  />
-);
+const BREAKPOINTS = [
+  {label: 'Conditional Breakpoint...'},
+  {label: 'Inline Breakpoint'},
+  {label: 'Function Breakpoint...'},
+  {label: 'Logpoint...'},
+];
 
 export default () => (
-  <StatefulMenu
-    items={ITEMS}
-    overrides={{
-      List: {style: {width: '350px', overflow: 'auto'}},
-      Option: {
-        props: {
-          getChildMenu: SecondMenu,
+  <NestedMenus>
+    <StatefulMenu
+      items={FILE}
+      overrides={{
+        List: {style: {width: '350px', overflow: 'auto'}},
+        Option: {
+          props: {
+            size: 'compact',
+            getChildMenu: item => {
+              if (item.label === OPEN_RECENT) {
+                return (
+                  <StatefulMenu
+                    size="compact"
+                    items={RECENT_FILES}
+                    overrides={{
+                      List: {style: {width: '200px'}},
+                      Option: {props: {size: 'compact'}},
+                    }}
+                  />
+                );
+              }
+
+              if (item.label === NEW_BREAKPOINT) {
+                return (
+                  <StatefulMenu
+                    items={BREAKPOINTS}
+                    overrides={{
+                      List: {style: {width: '220px'}},
+                      Option: {props: {size: 'compact'}},
+                    }}
+                  />
+                );
+              }
+            },
+          },
         },
-      },
-    }}
-  />
+      }}
+    />
+  </NestedMenus>
 );
diff --git a/src/menu/constants.js b/src/menu/constants.js
index 6fbc61d4d..37f458559 100644
--- a/src/menu/constants.js
+++ b/src/menu/constants.js
@@ -10,18 +10,16 @@ export const STATE_CHANGE_TYPES = {
   click: 'click',
   moveUp: 'moveUp',
   moveDown: 'moveDown',
-};
-
-// Dict of props to prepend $ to pass thru to styletron
-export const SHARED_PROPS_MAPPER = {
-  disabled: true,
-  ref: true,
-  isHighlighted: true,
+  mouseEnter: 'mouseEnter',
+  focus: 'focus',
+  reset: 'reset',
 };
 
 export const KEY_STRINGS = {
   ArrowUp: 'ArrowUp',
   ArrowDown: 'ArrowDown',
+  ArrowLeft: 'ArrowLeft',
+  ArrowRight: 'ArrowRight',
   Enter: 'Enter',
   Space: ' ',
   Escape: 'Escape',
diff --git a/src/menu/index.js b/src/menu/index.js
index 7dfe4ae82..a0b1c3b2a 100644
--- a/src/menu/index.js
+++ b/src/menu/index.js
@@ -10,6 +10,7 @@ export {default as StatefulContainer} from './stateful-container.js';
 export {default as OptionList} from './option-list.js';
 export {default as OptionProfile} from './option-profile.js';
 export {default as Menu} from './menu.js';
+export {default as NestedMenus} from './nested-menus.js';
 // Constants
 export {KEY_STRINGS, STATE_CHANGE_TYPES} from './constants.js';
 // Styled elements
diff --git a/src/menu/maybe-child-menu.js b/src/menu/maybe-child-menu.js
index b0770f50e..110272775 100644
--- a/src/menu/maybe-child-menu.js
+++ b/src/menu/maybe-child-menu.js
@@ -9,12 +9,14 @@ LICENSE file in the root directory of this source tree.
 
 import * as React from 'react';
 
-import {StatefulPopover} from '../popover/index.js';
+import {Popover} from '../popover/index.js';
 
 type PropsT = {
   children: React.Node,
   getChildMenu: ?(item: *) => React.Node,
+  isOpen: boolean,
   item: *,
+  resetParentMenu: () => void,
 };
 
 export default function MaybeChildMenu(props: PropsT) {
@@ -22,16 +24,37 @@ export default function MaybeChildMenu(props: PropsT) {
     return props.children;
   }
 
+  const ChildMenu = props.getChildMenu(props.item);
+  if (!ChildMenu) {
+    return props.children;
+  }
+
   return (
-    <StatefulPopover
-      content={props.getChildMenu(props.item)}
+    <Popover
+      isOpen={props.isOpen}
+      content={ChildMenu}
       ignoreBoundary
-      onMouseEnterDelay={100}
-      onMouseLeaveDelay={100}
+      onMouseEnterDelay={30}
+      onMouseLeaveDelay={30}
       placement="rightTop"
-      triggerType="hover"
+      overrides={{
+        Body: {
+          props: {
+            // Adds mouseleave to popover body so that child menu closes when user mouses out.
+            onMouseLeave: props.resetParentMenu,
+            // Trap tabbing when focused on a child menu. Popover mounts the element at the end of
+            // the html body by default. If a user was to tab to the next element it would navigate
+            // to elements not within the immediate area surrounding the menu.
+            onKeyDown: (e: KeyboardEvent) => {
+              if (e.keyCode === 9) {
+                e.preventDefault();
+              }
+            },
+          },
+        },
+      }}
     >
       {props.children}
-    </StatefulPopover>
+    </Popover>
   );
 }
diff --git a/src/menu/menu.js b/src/menu/menu.js
index c27e622cf..e7b2c492f 100644
--- a/src/menu/menu.js
+++ b/src/menu/menu.js
@@ -9,31 +9,58 @@ import * as React from 'react';
 // Components
 import {List as StyledList} from './styled-components.js';
 import OptionList from './option-list.js';
-import {getSharedProps} from './utils.js';
 import {getOverrides} from '../helpers/overrides.js';
 // Types
 import type {StatelessMenuPropsT} from './types.js';
 
-export default function Menu({
-  getRequiredItemProps = (item, index) => ({}),
-  items,
-  overrides = {},
-  rootRef = React.createRef(),
-}: StatelessMenuPropsT) {
+export default function Menu(props: StatelessMenuPropsT) {
+  const {
+    getRequiredItemProps = (item, index) => ({}),
+    items,
+    overrides = {},
+    rootRef = React.createRef(),
+    focusMenu = () => {},
+    unfocusMenu = () => {},
+  } = props;
+
   const [List, listProps] = getOverrides(overrides.List, StyledList);
   const [Option, optionProps] = getOverrides(overrides.Option, OptionList);
+
   return (
-    <List role="listbox" $ref={rootRef} {...listProps}>
+    <List
+      role="listbox"
+      $ref={rootRef}
+      onMouseEnter={focusMenu}
+      onMouseOver={focusMenu}
+      onMouseLeave={unfocusMenu}
+      onFocus={focusMenu}
+      onBlur={unfocusMenu}
+      tabIndex={0}
+      {...listProps}
+    >
       {items.map((item, index) => {
-        const requiredProps = getRequiredItemProps(item, index);
+        const {
+          disabled,
+          isFocused,
+          isHighlighted,
+          ref,
+          resetMenu = () => {},
+          ...rest
+        } = getRequiredItemProps(item, index);
+
         return (
           <Option
-            item={item}
             key={index}
-            role="option"
+            item={item}
             overrides={overrides}
-            tabIndex={index === 0 ? 0 : -1} // Allows tab focus into first element
-            {...getSharedProps(requiredProps)}
+            resetMenu={resetMenu}
+            role="option"
+            $disabled={disabled}
+            $ref={ref}
+            $isFocused={isFocused}
+            $isHighlighted={isHighlighted}
+            aria-selected={isHighlighted && isFocused}
+            {...rest}
             {...optionProps}
           />
         );
diff --git a/src/menu/nested-menus.js b/src/menu/nested-menus.js
new file mode 100644
index 000000000..e1dd54711
--- /dev/null
+++ b/src/menu/nested-menus.js
@@ -0,0 +1,83 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+// @flow
+
+import * as React from 'react';
+
+type Ref = {current: ?HTMLElement};
+type ContextT = {
+  addMenuToNesting: (ref: Ref) => void,
+  removeMenuFromNesting: (ref: Ref) => void,
+  getParentMenu: (ref: Ref) => ?Ref,
+  getChildMenu: (ref: Ref) => ?Ref,
+};
+type StateT = {
+  menus: Ref[],
+};
+type PropsT = {
+  children: React.Node,
+};
+
+export const NestedMenuContext: React.Context<ContextT> = React.createContext({
+  addMenuToNesting: () => {},
+  removeMenuFromNesting: () => {},
+  getParentMenu: () => {},
+  getChildMenu: () => {},
+});
+
+function isSame(a: ?HTMLElement, b: ?HTMLElement) {
+  if (!a || !b) {
+    return false;
+  }
+
+  return a.isSameNode(b);
+}
+
+export default class NestedMenus extends React.Component<PropsT, StateT> {
+  state = {menus: []};
+
+  addMenuToNesting = (ref: Ref) => {
+    this.setState({menus: [...this.state.menus, ref]});
+  };
+
+  removeMenuFromNesting = (ref: Ref) => {
+    const nextMenus = this.state.menus.filter(
+      r => !isSame(r.current, ref.current),
+    );
+    this.setState({menus: nextMenus});
+  };
+
+  findMenuIndexByRef = (ref: Ref) => {
+    return this.state.menus.findIndex(r => isSame(r.current, ref.current));
+  };
+
+  getParentMenu = (ref: Ref): ?Ref => {
+    const index = this.findMenuIndexByRef(ref) - 1;
+    return this.state.menus[index];
+  };
+
+  getChildMenu = (ref: Ref): ?Ref => {
+    const index = this.findMenuIndexByRef(ref) + 1;
+    return this.state.menus[index];
+  };
+
+  render() {
+    return (
+      <NestedMenuContext.Provider
+        value={{
+          addMenuToNesting: this.addMenuToNesting,
+          removeMenuFromNesting: this.removeMenuFromNesting,
+          getParentMenu: this.getParentMenu,
+          getChildMenu: this.getChildMenu,
+        }}
+      >
+        {this.props.children}
+      </NestedMenuContext.Provider>
+    );
+  }
+}
diff --git a/src/menu/option-list.js b/src/menu/option-list.js
index 7d6fe0920..637bad5c3 100644
--- a/src/menu/option-list.js
+++ b/src/menu/option-list.js
@@ -15,27 +15,36 @@ import MaybeChildMenu from './maybe-child-menu.js';
 import {ListItem as StyledListItem} from './styled-components.js';
 import type {OptionListPropsT} from './types.js';
 
-export default function OptionList({
-  item,
-  getChildMenu,
-  getItemLabel,
-  size,
-  overrides,
-  $isHighlighted,
-  ...restProps
-}: OptionListPropsT) {
+export default function OptionList(props: OptionListPropsT) {
+  const {
+    getChildMenu,
+    getItemLabel,
+    item,
+    resetMenu = () => {},
+    size,
+    overrides = {},
+    $isHighlighted,
+    ...restProps
+  } = props;
+
   const [ListItem, listItemProps] = getOverrides(
     overrides.ListItem,
     StyledListItem,
   );
-  const sharedProps = {
-    $size: size,
-    $isHighlighted,
-  };
 
   return (
-    <MaybeChildMenu getChildMenu={getChildMenu} item={item}>
-      <ListItem {...sharedProps} {...restProps} {...listItemProps}>
+    <MaybeChildMenu
+      getChildMenu={getChildMenu}
+      isOpen={!!$isHighlighted}
+      item={item}
+      resetParentMenu={resetMenu}
+    >
+      <ListItem
+        $size={size}
+        $isHighlighted={$isHighlighted}
+        {...restProps}
+        {...listItemProps}
+      >
         {getItemLabel({isHighlighted: $isHighlighted, ...item})}
       </ListItem>
     </MaybeChildMenu>
@@ -45,5 +54,7 @@ export default function OptionList({
 OptionList.defaultProps = {
   getItemLabel: (item: *) => (item ? item.label : ''),
   size: OPTION_LIST_SIZE.default,
+  onMouseEnter: () => {},
   overrides: {},
+  resetMenu: () => {},
 };
diff --git a/src/menu/option-profile.js b/src/menu/option-profile.js
index 44ee2654c..ff51afe1b 100644
--- a/src/menu/option-profile.js
+++ b/src/menu/option-profile.js
@@ -22,15 +22,19 @@ import {getOverrides} from '../helpers/overrides.js';
 // Types
 import type {OptionProfilePropsT} from './types.js';
 
-export default function OptionProfile({
-  item,
-  getChildMenu,
-  getProfileItemLabels,
-  getProfileItemImg,
-  getProfileItemImgText,
-  overrides = {},
-  ...restProps
-}: OptionProfilePropsT) {
+export default function OptionProfile(props: OptionProfilePropsT) {
+  const {
+    item,
+    getChildMenu,
+    getProfileItemLabels,
+    getProfileItemImg,
+    getProfileItemImgText,
+    overrides = {},
+    resetMenu = () => {},
+    $isHighlighted,
+    ...restProps
+  } = props;
+
   const [ListItemProfile, listItemProfileProps] = getOverrides(
     overrides.ListItemProfile,
     StyledListItemProfile,
@@ -64,7 +68,12 @@ export default function OptionProfile({
   const {title, subtitle, body} = getProfileItemLabels(item);
 
   return (
-    <MaybeChildMenu getChildMenu={getChildMenu} item={item}>
+    <MaybeChildMenu
+      getChildMenu={getChildMenu}
+      isOpen={!!$isHighlighted}
+      item={item}
+      resetParentMenu={resetMenu}
+    >
       <ListItemProfile {...restProps} {...listItemProfileProps}>
         <ProfileImgContainer {...profileImgContainerProps}>
           {ItemImg &&
diff --git a/src/menu/stateful-container.js b/src/menu/stateful-container.js
index 1a4846758..e1573d5b0 100644
--- a/src/menu/stateful-container.js
+++ b/src/menu/stateful-container.js
@@ -25,8 +25,9 @@ export default class MenuStatefulContainer extends React.Component<
 > {
   static defaultProps = {
     initialState: {
-      // We start the index at -1 to indicate that no highlighting exists initially
+      // Defaults to -1 so no item is highlighted
       highlightedIndex: -1,
+      isFocused: false,
     },
     stateReducer: (
       changeType: ?$PropertyType<StateReducerFnT, 'changeType'>,
@@ -35,14 +36,18 @@ export default class MenuStatefulContainer extends React.Component<
     onItemSelect: () => {},
     getRequiredItemProps: () => ({}),
     children: () => null,
+
+    // from nested-menus context
+    addMenuToNesting: () => {},
+    removeMenuFromNesting: () => {},
+    getParentMenu: () => {},
+    getChildMenu: () => {},
   };
 
   state: StatefulContainerStateT = {...this.props.initialState};
 
   componentDidMount() {
     if (__BROWSER__) {
-      // TODO(#185): perhaps only bind event listener on focus
-      document.addEventListener('keydown', this.onKeyDown);
       if (
         this.state.highlightedIndex > -1 &&
         this.refList[this.state.highlightedIndex]
@@ -54,13 +59,30 @@ export default class MenuStatefulContainer extends React.Component<
           this.state.highlightedIndex === this.props.items.length - 1,
         );
       }
+
+      if (this.state.isFocused) {
+        document.addEventListener('keydown', this.onKeyDown);
+      }
     }
+    this.props.addMenuToNesting && this.props.addMenuToNesting(this.rootRef);
   }
 
   componentWillUnmount() {
     if (__BROWSER__) {
       document.removeEventListener('keydown', this.onKeyDown);
     }
+    this.props.removeMenuFromNesting &&
+      this.props.removeMenuFromNesting(this.rootRef);
+  }
+
+  componentDidUpdate(_: mixed, prevState: StatefulContainerStateT) {
+    if (__BROWSER__) {
+      if (!prevState.isFocused && this.state.isFocused) {
+        document.addEventListener('keydown', this.onKeyDown);
+      } else if (prevState.isFocused && !this.state.isFocused) {
+        document.removeEventListener('keydown', this.onKeyDown);
+      }
+    }
   }
 
   // One array to hold all of list item refs
@@ -72,56 +94,71 @@ export default class MenuStatefulContainer extends React.Component<
 
   // Internal set state function that will also invoke stateReducer
   internalSetState(
-    changeType: ?$Keys<typeof STATE_CHANGE_TYPES>,
-    changes: StatefulContainerStateT,
+    changeType: $Keys<typeof STATE_CHANGE_TYPES>,
+    changes: $Shape<StatefulContainerStateT>,
   ) {
     const {stateReducer} = this.props;
     this.setState(stateReducer(changeType, changes, this.state));
   }
 
-  // Regular type here because this is not done through React
   onKeyDown = (event: KeyboardEvent) => {
-    // Up arrow
     switch (event.key) {
       case KEY_STRINGS.ArrowUp:
       case KEY_STRINGS.ArrowDown:
-        this.handleArrowKey(event.key);
-        event.preventDefault();
-        event.stopPropagation();
+      case KEY_STRINGS.ArrowLeft:
+      case KEY_STRINGS.ArrowRight:
+        this.handleArrowKey(event);
         break;
       case KEY_STRINGS.Enter:
         this.handleEnterKey(event);
-        event.preventDefault();
         break;
     }
   };
 
   // Handler for arrow keys
-  handleArrowKey(key: string) {
-    const {items} = this.props;
-    const {highlightedIndex: oldIndex} = this.state;
-    let highlightedIndex = oldIndex;
-    let stateChangeType = null;
-    if (key === KEY_STRINGS.ArrowUp) {
-      highlightedIndex = Math.max(0, oldIndex - 1);
-      stateChangeType = STATE_CHANGE_TYPES.moveUp;
-    } else if (key === KEY_STRINGS.ArrowDown) {
-      highlightedIndex = Math.min(oldIndex + 1, items.length - 1);
-      stateChangeType = STATE_CHANGE_TYPES.moveDown;
+  handleArrowKey = (event: KeyboardEvent) => {
+    event.preventDefault();
+    const prevIndex = this.state.highlightedIndex;
+    let nextIndex = prevIndex;
+
+    if (event.key === KEY_STRINGS.ArrowUp) {
+      nextIndex = Math.max(0, prevIndex - 1);
+      this.internalSetState(STATE_CHANGE_TYPES.moveUp, {
+        highlightedIndex: nextIndex,
+      });
+    } else if (event.key === KEY_STRINGS.ArrowDown) {
+      nextIndex = Math.min(prevIndex + 1, this.props.items.length - 1);
+      this.internalSetState(STATE_CHANGE_TYPES.moveDown, {
+        highlightedIndex: nextIndex,
+      });
+    } else if (event.key === KEY_STRINGS.ArrowLeft) {
+      if (this.props.getParentMenu) {
+        const parent = this.props.getParentMenu(this.rootRef);
+        if (parent && parent.current) {
+          parent.current.focus();
+        }
+      }
+    } else if (event.key === KEY_STRINGS.ArrowRight) {
+      if (this.props.getChildMenu) {
+        const child = this.props.getChildMenu(this.rootRef);
+        if (child && child.current) {
+          child.current.focus();
+        }
+      }
     }
-    if (this.refList[highlightedIndex]) {
+
+    if (this.refList[nextIndex]) {
       scrollItemIntoView(
-        this.refList[highlightedIndex].current,
+        this.refList[nextIndex].current,
         this.rootRef.current,
-        highlightedIndex === 0,
-        highlightedIndex === items.length - 1,
+        nextIndex === 0,
+        nextIndex === this.props.items.length - 1,
       );
     }
-    this.internalSetState(stateChangeType, {highlightedIndex});
-  }
+  };
 
   // Handler for enter key
-  handleEnterKey(event: KeyboardEvent) {
+  handleEnterKey = (event: KeyboardEvent) => {
     const {items, onItemSelect} = this.props;
     const {highlightedIndex} = this.state;
     if (
@@ -131,43 +168,68 @@ export default class MenuStatefulContainer extends React.Component<
     ) {
       onItemSelect({item: items[highlightedIndex], event});
     }
-  }
+  };
 
   getRequiredItemProps: GetRequiredItemPropsFnT = (item, index) => {
-    const {highlightedIndex} = this.state;
-    const {onItemSelect, getRequiredItemProps} = this.props;
-    let onClickHandler;
-    if (onItemSelect && !item.disabled) {
-      onClickHandler = () => {
-        onItemSelect({item});
-        this.internalSetState(STATE_CHANGE_TYPES.click, {
-          highlightedIndex: index,
-        });
-      };
-    }
-    // Create and store ref or re-use
     let itemRef = this.refList[index];
     if (!itemRef) {
       itemRef = React.createRef();
       this.refList[index] = itemRef;
     }
     return {
-      disabled: item.disabled,
+      disabled: !!item.disabled,
       ref: itemRef,
-      isHighlighted: highlightedIndex === index,
-      onClick: onClickHandler,
-      ...getRequiredItemProps(item, index),
+      isFocused: this.state.isFocused,
+      isHighlighted: this.state.highlightedIndex === index,
+      onClick: () => {
+        if (this.props.onItemSelect && !item.disabled) {
+          this.props.onItemSelect({item});
+          this.internalSetState(STATE_CHANGE_TYPES.click, {
+            highlightedIndex: index,
+          });
+        }
+      },
+      onMouseEnter: () => {
+        this.internalSetState(STATE_CHANGE_TYPES.mouseEnter, {
+          highlightedIndex: index,
+        });
+      },
+      resetMenu: this.resetMenu,
+      ...this.props.getRequiredItemProps(item, index),
     };
   };
 
+  focusMenu = (event: FocusEvent | MouseEvent | KeyboardEvent) => {
+    if (this.state.isFocused) {
+      return;
+    }
+
+    if (this.rootRef.current.contains(event.target)) {
+      this.internalSetState(STATE_CHANGE_TYPES.focus, {isFocused: true});
+      this.rootRef.current.focus();
+    }
+  };
+
+  unfocusMenu = () => {
+    this.internalSetState(STATE_CHANGE_TYPES.focus, {isFocused: false});
+  };
+
+  resetMenu = () => {
+    this.internalSetState(STATE_CHANGE_TYPES.reset, {
+      isFocused: false,
+      highlightedIndex: -1,
+    });
+  };
+
   render() {
-    const {highlightedIndex} = this.state;
-    const {children, items} = this.props;
-    return children(
+    return this.props.children(
       ({
         getRequiredItemProps: this.getRequiredItemProps,
-        highlightedIndex,
-        items,
+        highlightedIndex: this.state.highlightedIndex,
+        isFocused: this.state.isFocused,
+        items: this.props.items,
+        focusMenu: this.focusMenu,
+        unfocusMenu: this.unfocusMenu,
         rootRef: this.rootRef,
       }: RenderPropsT),
     );
diff --git a/src/menu/stateful-menu.js b/src/menu/stateful-menu.js
index 5c587a7ce..026328a5f 100644
--- a/src/menu/stateful-menu.js
+++ b/src/menu/stateful-menu.js
@@ -9,6 +9,7 @@ LICENSE file in the root directory of this source tree.
 import * as React from 'react';
 
 import Menu from './menu.js';
+import {NestedMenuContext} from './nested-menus.js';
 import StatefulContainer from './stateful-container.js';
 
 import type {StatefulMenuPropsT, StateReducerFnT} from './types.js';
@@ -19,6 +20,7 @@ export default class StatefulMenu extends React.PureComponent<
   static defaultProps = {
     // Mostly to satisfy flow
     initialState: {
+      isFocused: false,
       // We start the index at -1 to indicate that no highlighting exists initially
       highlightedIndex: -1,
     },
@@ -35,9 +37,13 @@ export default class StatefulMenu extends React.PureComponent<
   render() {
     const {overrides, ...props} = this.props;
     return (
-      <StatefulContainer {...props}>
-        {renderProps => <Menu {...renderProps} overrides={overrides} />}
-      </StatefulContainer>
+      <NestedMenuContext.Consumer>
+        {ctx => (
+          <StatefulContainer {...ctx} {...props}>
+            {renderProps => <Menu {...renderProps} overrides={overrides} />}
+          </StatefulContainer>
+        )}
+      </NestedMenuContext.Consumer>
     );
   }
 }
diff --git a/src/menu/styled-components.js b/src/menu/styled-components.js
index 99abf01a1..580711aa8 100644
--- a/src/menu/styled-components.js
+++ b/src/menu/styled-components.js
@@ -13,6 +13,7 @@ import type {ThemeT} from '../styles/index.js';
 type StyledPropsT = {
   $disabled?: boolean,
   $theme: ThemeT,
+  $isFocused?: boolean,
   $isHighlighted?: boolean,
   $size?: $Keys<typeof OPTION_LIST_SIZE>,
 };
@@ -33,29 +34,48 @@ export const List = styled('ul', ({$theme}: StyledPropsT) => ({
   overflow: 'auto',
 }));
 
-export const ListItem = styled(
-  'li',
-  ({$disabled, $theme, $isHighlighted, $size}: StyledPropsT) => ({
+function getFontColor(props: StyledPropsT) {
+  if (props.$disabled) {
+    return props.$theme.colors.menuFontDisabled;
+  }
+
+  if (props.$isHighlighted && props.$isFocused) {
+    return props.$theme.colors.menuFontHighlighted;
+  }
+
+  if (props.$isHighlighted && !props.$isFocused) {
+    return props.$theme.colors.menuFontSelected;
+  }
+
+  return props.$theme.colors.menuFontDefault;
+}
+
+function getBackgroundColor(props: StyledPropsT) {
+  if (props.$disabled) {
+    return 'transparent';
+  }
+
+  if (props.$isHighlighted) {
+    return props.$theme.colors.menuFillHover;
+  }
+
+  return 'transparent';
+}
+
+export const ListItem = styled('li', (props: StyledPropsT) => {
+  const {$disabled, $theme, $size} = props;
+  return {
     ...($size === OPTION_LIST_SIZE.compact
       ? $theme.typography.font200
       : $theme.typography.font300),
     position: 'relative',
     display: 'block',
-    color: $disabled
-      ? $theme.colors.foregroundAlt
-      : $isHighlighted
-        ? $theme.colors.primary
-        : $theme.colors.foreground,
+    color: getFontColor(props),
     cursor: $disabled ? 'not-allowed' : 'pointer',
-    backgroundColor: $isHighlighted
-      ? $theme.colors.menuFillHover
-      : 'transparent',
+    backgroundColor: getBackgroundColor(props),
     transitionProperty: 'color, background-color',
     transitionDuration: $theme.animation.timing100,
     transitionTimingFunction: $theme.animation.easeOutCurve,
-    ':hover': {
-      backgroundColor: $theme.colors.menuFillHover,
-    },
     marginBottom: '0',
     paddingTop:
       $size === OPTION_LIST_SIZE.compact
@@ -76,8 +96,8 @@ export const ListItem = styled(
     ':focus': {
       outline: 'none',
     },
-  }),
-);
+  };
+});
 
 export const ListItemProfile = styled('li', ({$theme}: StyledPropsT) => ({
   position: 'relative',
diff --git a/src/menu/types.js b/src/menu/types.js
index 75d2c586a..4a165a098 100644
--- a/src/menu/types.js
+++ b/src/menu/types.js
@@ -36,7 +36,6 @@ export type RootRefT = React$ElementRef<*>;
 export type OnItemSelectFnT = ({
   item: ItemT,
   event?: SyntheticEvent<HTMLElement> | KeyboardEvent,
-  /* eslint-disable-next-line flowtype/no-weak-types */
 }) => mixed;
 
 export type ProfileOverridesT = {
@@ -51,10 +50,12 @@ export type ProfileOverridesT = {
 };
 
 export type RenderItemPropsT = {
+  disabled?: boolean,
   ref?: React$ElementRef<*>,
+  isFocused?: boolean,
   // indicates when the item is visually focused
   isHighlighted?: boolean,
-  'aria-activedescendant'?: boolean,
+  resetMenu?: () => mixed,
 };
 
 export type GetRequiredItemPropsFnT = (
@@ -71,6 +72,9 @@ export type StateReducerFnT = (
 export type StatefulContainerStateT = {
   // index of currently highlighted item (from keyboard control)
   highlightedIndex: number,
+  // indicates when the menu can be navigated by keyboard and affects menu item option rendering
+  // see https://github.com/uber-web/baseui/issues/993 for a description.
+  isFocused: boolean,
 };
 
 export type RenderPropsT = StatefulContainerStateT & {
@@ -101,6 +105,10 @@ export type StatefulContainerPropsT = {
   onItemSelect: OnItemSelectFnT,
   /** Child as function pattern. */
   children: RenderPropsT => React.Node,
+  addMenuToNesting?: (ref: {current: ?HTMLElement}) => void,
+  removeMenuFromNesting?: (ref: {current: ?HTMLElement}) => void,
+  getParentMenu?: (ref: {current: ?HTMLElement}) => ?{current: ?HTMLElement},
+  getChildMenu?: (ref: {current: ?HTMLElement}) => ?{current: ?HTMLElement},
 };
 
 export type MenuPropsT = {
@@ -133,6 +141,8 @@ export type SharedStatelessPropsT = {
   onFocus?: (event: SyntheticFocusEvent<HTMLElement>) => mixed,
   /** Ref for the menu container element. Used to capture key events for navigation */
   rootRef: RootRefT,
+  focusMenu?: (event: FocusEvent | MouseEvent | KeyboardEvent) => mixed,
+  unfocusMenu?: () => mixed,
 };
 
 export type StatefulMenuPropsT = StatefulContainerPropsT & MenuPropsT;
@@ -152,11 +162,15 @@ export type OptionListPropsT = {
   getItemLabel: GetItemLabelFnT,
   /** Used to render a sub menu at this menu item. You'll often render another menu from this function. */
   getChildMenu?: (item: ItemT) => React.Node,
+  /** Callback used to change highlighted index in stateful menu. */
+  onMouseEnter?: (event: MouseEvent) => mixed,
   /** Renders UI in defined scale. */
-  size: $Keys<typeof OPTION_LIST_SIZE>,
-  overrides: {
+  size?: $Keys<typeof OPTION_LIST_SIZE>,
+  overrides?: {
     ListItem?: OverrideT<*>,
   },
+  /** Utility to reset menu to defualt state. Useful for rendering child menus. */
+  resetMenu?: () => void,
   /** Renders UI in 'highlighted' state. */
   $isHighlighted?: boolean,
 };
@@ -181,4 +195,8 @@ export type OptionProfilePropsT = {
     ProfileSubtitle?: OverrideT<*>,
     ProfileBody?: OverrideT<*>,
   },
+  /** Utility to reset menu to defualt state. Useful for rendering child menus. */
+  resetMenu?: () => void,
+  /** Renders UI in 'highlighted' state. */
+  $isHighlighted?: boolean,
 };
diff --git a/src/menu/utils.js b/src/menu/utils.js
index 98a2561fc..5ce679d61 100644
--- a/src/menu/utils.js
+++ b/src/menu/utils.js
@@ -7,28 +7,11 @@ LICENSE file in the root directory of this source tree.
 // @flow
 /* eslint-disable import/prefer-default-export */
 import smoothscroll from 'smoothscroll-polyfill';
-import {SHARED_PROPS_MAPPER} from './constants.js';
 
 if (__BROWSER__) {
   smoothscroll.polyfill();
 }
 
-/**
- * Given a props object and a mapper dictionary of prop keys, we will prepend
- * all of the existing prop keys inside mapper with $ to present styletron
- * from passing through those props to the underlying React component.
- */
-export function getSharedProps(
-  props: {},
-  mapper: {} = SHARED_PROPS_MAPPER,
-): {} {
-  return Object.keys(props).reduce((newProps, propName) => {
-    const newName = mapper[propName] ? `$${propName}` : propName;
-    newProps[newName] = props[propName];
-    return newProps;
-  }, {});
-}
-
 // Helps scroll a list item into view when cycling through list via
 // keybindings and highlighted item is not in view.
 
diff --git a/src/select/dropdown.js b/src/select/dropdown.js
index 98ead95f0..014d733bc 100644
--- a/src/select/dropdown.js
+++ b/src/select/dropdown.js
@@ -111,6 +111,7 @@ export default class SelectDropdown extends React.Component<DropdownPropsT> {
           items={options}
           size={size}
           initialState={{
+            isFocused: true,
             highlightedIndex:
               Array.isArray(value) && value.length === 1
                 ? options.findIndex(opt => opt.id === value[0].id)
diff --git a/src/select/styled-components.js b/src/select/styled-components.js
index 60cf3d189..3329a6481 100644
--- a/src/select/styled-components.js
+++ b/src/select/styled-components.js
@@ -79,16 +79,10 @@ export const StyledDropdownListItem = StyledListItem;
 
 export const StyledOptionContent = styled('div', props => {
   const {$isHighlighted, $selected, $disabled, $theme} = props;
-  const {
-    colors: {foregroundAlt, primary400, foreground},
-  } = $theme;
+
   return {
     cursor: $disabled ? 'not-allowed' : 'pointer',
-    color: $disabled
-      ? foregroundAlt
-      : $selected || $isHighlighted
-        ? primary400
-        : foreground,
+    color: $selected && !$isHighlighted ? $theme.colors.menuFontSelected : null,
     fontWeight: $selected ? 'bold' : 'normal',
   };
 });
diff --git a/src/styles/types.js b/src/styles/types.js
index 550484d90..2cf93a167 100644
--- a/src/styles/types.js
+++ b/src/styles/types.js
@@ -185,6 +185,10 @@ export type ColorsT = {
 
   // Menu
   menuFillHover: string,
+  menuFontDefault: string,
+  menuFontDisabled: string,
+  menuFontHighlighted: string,
+  menuFontSelected: string,
 
   // Shadow
   shadowFocus: string,
diff --git a/src/themes/creator.js b/src/themes/creator.js
index d9c3a986f..efbec825f 100644
--- a/src/themes/creator.js
+++ b/src/themes/creator.js
@@ -199,7 +199,11 @@ export default function createTheme(
       inputTextDisabled: primitives.mono600,
 
       // Menu
-      menuFillHover: primitives.mono300,
+      menuFillHover: primitives.mono200,
+      menuFontDefault: primitives.mono800,
+      menuFontDisabled: primitives.mono500,
+      menuFontHighlighted: primitives.primary400,
+      menuFontSelected: primitives.mono1000,
 
       // Notification
       notificationPrimaryBackground: primitives.primary50,
diff --git a/src/themes/dark-theme-colors.js b/src/themes/dark-theme-colors.js
index ed1b7f833..a2018c9db 100644
--- a/src/themes/dark-theme-colors.js
+++ b/src/themes/dark-theme-colors.js
@@ -124,7 +124,11 @@ export default {
     inputTextDisabled: primitives.mono500,
 
     // Menu
-    menuFillHover: primitives.mono600,
+    menuFillHover: primitives.mono800,
+    menuFontDefault: primitives.mono400,
+    menuFontDisabled: primitives.mono500,
+    menuFontHighlighted: primitives.mono200,
+    menuFontSelected: primitives.mono200,
   },
   tooltip: {
     backgroundColor: primitives.mono200,


Test patch:
diff --git a/src/menu/__tests__/maybe-child-menu.test.js b/src/menu/__tests__/maybe-child-menu.test.js
index 1c63b9b54..ff537f8cd 100644
--- a/src/menu/__tests__/maybe-child-menu.test.js
+++ b/src/menu/__tests__/maybe-child-menu.test.js
@@ -14,7 +14,12 @@ import MaybeChildMenu from '../maybe-child-menu.js';
 describe('MaybeChildMenu', () => {
   it('does not render popover if getChildMenu is undefined', () => {
     const wrapper = mount(
-      <MaybeChildMenu getChildMenu={null} item={{label: 'item'}}>
+      <MaybeChildMenu
+        isOpen={true}
+        getChildMenu={null}
+        item={{label: 'item'}}
+        resetParentMenu={() => {}}
+      >
         <div>child</div>
       </MaybeChildMenu>,
     );
@@ -29,8 +34,10 @@ describe('MaybeChildMenu', () => {
   it('renders popover if getChildMenu is provided', () => {
     const wrapper = mount(
       <MaybeChildMenu
+        isOpen={true}
         getChildMenu={() => <button>child menu</button>}
         item={{label: 'item'}}
+        resetParentMenu={() => {}}
       >
         <div>child</div>
       </MaybeChildMenu>,
@@ -41,6 +48,6 @@ describe('MaybeChildMenu', () => {
         .children()
         .first()
         .name(),
-    ).toBe('StatefulPopover');
+    ).toBe('Popover');
   });
 });
diff --git a/src/menu/__tests__/menu-child.scenario.js b/src/menu/__tests__/menu-child.scenario.js
new file mode 100644
index 000000000..87f619600
--- /dev/null
+++ b/src/menu/__tests__/menu-child.scenario.js
@@ -0,0 +1,91 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+
+import {StatefulMenu, NestedMenus} from '../index.js';
+
+export const name = 'menu-child';
+
+const OPEN_RECENT = 'Open Recent ->';
+const NEW_BREAKPOINT = 'New Breakpoint ->';
+const FILE = [
+  {label: 'New File'},
+  {label: 'New Window'},
+  {label: 'Open...'},
+  {label: 'Open Workspace...'},
+  {label: OPEN_RECENT},
+  {label: 'Add Folder to Workspace...'},
+  {label: 'Save'},
+  {label: 'Save As...'},
+  {label: 'Toggle Breakpoint'},
+  {label: NEW_BREAKPOINT},
+  {label: 'Close Folder'},
+  {label: 'Close Window'},
+];
+
+const RECENT_FILES = [
+  {label: 'Reopen Closed Editor'},
+  {label: '~/workspace/baseui'},
+  {label: '~/workspace/styletron'},
+  {label: '~/workspace/my-project'},
+  {label: 'More...'},
+  {label: 'Clear Recently Opened'},
+];
+
+const BREAKPOINTS = [
+  {label: 'Conditional Breakpoint...'},
+  {label: 'Inline Breakpoint'},
+  {label: 'Function Breakpoint...'},
+  {label: 'Logpoint...'},
+];
+
+const childMenu = items => (
+  <StatefulMenu
+    items={items}
+    overrides={{
+      List: {
+        style: {width: '300px'},
+        props: {'data-e2e': 'child-menu'},
+      },
+      Option: {
+        props: {
+          size: 'compact',
+        },
+      },
+    }}
+  />
+);
+
+export const component = () => (
+  <NestedMenus>
+    <StatefulMenu
+      items={FILE}
+      overrides={{
+        List: {
+          style: {width: '300px', overflow: 'auto'},
+          props: {'data-e2e': 'parent-menu'},
+        },
+        Option: {
+          props: {
+            size: 'compact',
+            getChildMenu: item => {
+              if (item.label === OPEN_RECENT) {
+                return childMenu(RECENT_FILES);
+              }
+
+              if (item.label === NEW_BREAKPOINT) {
+                return childMenu(BREAKPOINTS);
+              }
+            },
+          },
+        },
+      }}
+    />
+  </NestedMenus>
+);
diff --git a/src/menu/__tests__/menu.e2e.js b/src/menu/__tests__/menu.e2e.js
index bce1ce141..b25b0d639 100644
--- a/src/menu/__tests__/menu.e2e.js
+++ b/src/menu/__tests__/menu.e2e.js
@@ -5,6 +5,7 @@ This source code is licensed under the MIT license found in the
 LICENSE file in the root directory of this source tree.
 */
 
+/* global document */
 /* eslint-env node */
 /* eslint-disable flowtype/require-valid-file-annotation */
 
@@ -17,3 +18,110 @@ describe('menu', () => {
     expect(accessibilityReport).toHaveNoAccessibilityIssues();
   });
 });
+
+const parentSelector = '[data-e2e="parent-menu"]';
+const childSelector = '[data-e2e="child-menu"]';
+const highlightedselector = '[aria-selected="true"]';
+
+function hoverItem(page, x, y) {
+  const MENU_MARGIN_TOP = 16;
+  const MENU_ITEM_HEIGHT = 26;
+
+  const MENU_WIDTH = 300;
+  const MENU_WIDTH_OFFSET = MENU_WIDTH / 2;
+
+  const xPos = MENU_WIDTH_OFFSET + MENU_WIDTH * x;
+  const yPos = MENU_MARGIN_TOP + MENU_ITEM_HEIGHT * y;
+
+  return page.mouse.move(xPos, yPos);
+}
+
+function findActiveElement(page) {
+  return page.evaluateHandle(() => document.activeElement);
+}
+
+async function findHighlightedLabel(page) {
+  const highlightedItem = await page.$(highlightedselector);
+  return await page.evaluate(
+    highlightedItem => highlightedItem.textContent,
+    highlightedItem,
+  );
+}
+
+function compareElements(page, a, b) {
+  return page.evaluate((a, b) => a === b, a, b);
+}
+
+describe('menu-child', () => {
+  it('focuses menu on mouse enter and blurs on mouse leave', async () => {
+    await mount(page, 'menu-child');
+    await hoverItem(page, 0, 0);
+
+    const parent = await page.$(parentSelector);
+    const activeElement = await findActiveElement(page);
+    const isEqual = await compareElements(page, parent, activeElement);
+    expect(isEqual).toBe(true);
+  });
+
+  it('up/down arrows change highlighted item', async () => {
+    await mount(page, 'menu-child');
+    await hoverItem(page, 0, 0);
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowUp');
+
+    const text = await findHighlightedLabel(page);
+    expect(text).toBe('New Window');
+  });
+
+  it('left/right arrows change focused menu', async () => {
+    await mount(page, 'menu-child');
+    await hoverItem(page, 0, 0);
+
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowRight');
+
+    const child = await page.$(childSelector);
+    const activeElement = await findActiveElement(page);
+    const isEqual = await compareElements(page, child, activeElement);
+    expect(isEqual).toBe(true);
+  });
+
+  it('can select item in child menu by keyboard navigation', async () => {
+    await mount(page, 'menu-child');
+    await hoverItem(page, 0, 0);
+
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowRight');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+    await page.keyboard.press('ArrowDown');
+
+    const text = await findHighlightedLabel(page);
+    expect(text).toBe('More...');
+  });
+
+  it('opens child menu on hover', async () => {
+    await mount(page, 'menu-child');
+    await hoverItem(page, 0, 5);
+    await page.waitFor(childSelector);
+  });
+
+  it('highlights child menu item on hover', async () => {
+    await mount(page, 'menu-child');
+    await hoverItem(page, 0, 5);
+    await page.waitFor(childSelector);
+
+    await hoverItem(page, 1, 5);
+    const text = await findHighlightedLabel(page);
+    expect(text).toBe('Reopen Closed Editor');
+  });
+});
diff --git a/src/menu/__tests__/stateful-container.test.js b/src/menu/__tests__/stateful-container.test.js
index f82852939..ce2b355b4 100644
--- a/src/menu/__tests__/stateful-container.test.js
+++ b/src/menu/__tests__/stateful-container.test.js
@@ -57,21 +57,23 @@ describe('Menu StatefulContainer', () => {
       onItemSelect: mockItemSelect,
       children: mockChildrenFn,
     };
-    const component = mount(<StatefulContainer {...props} />);
-    expect(component.instance().refList).toEqual([]);
-    expect(component.instance().rootRef).toEqual(React.createRef());
-    expect(mockChildrenFn.mock.calls[0][0]).toEqual({
-      highlightedIndex: -1,
-      items: mockItems,
-      rootRef: React.createRef(),
-      getRequiredItemProps: component.instance().getRequiredItemProps,
-    });
+    mount(<StatefulContainer {...props} />);
+
+    const result = mockChildrenFn.mock.calls[0][0];
+    expect(result).toHaveProperty('highlightedIndex', -1);
+    expect(result).toHaveProperty('items', mockItems);
+    expect(result).toHaveProperty('rootRef');
+    expect(result).toHaveProperty('getRequiredItemProps');
+    expect(result).toHaveProperty('focusMenu');
+    expect(result).toHaveProperty('unfocusMenu');
+    expect(result).toHaveProperty('isFocused', false);
   });
 
   test('with initialState', () => {
     const props = {
       ...getSharedProps(),
       initialState: {
+        isFocused: false,
         highlightedIndex: 5,
       },
     };
@@ -79,22 +81,19 @@ describe('Menu StatefulContainer', () => {
     expect(component.state('highlightedIndex')).toBe(5);
   });
 
-  test('add and remove event listeners', () => {
-    const component = mount(<StatefulContainer {...getSharedProps()} />);
-    expect(document.addEventListener.mock.calls.length).toBe(1);
-    component.unmount();
-    expect(document.removeEventListener.mock.calls.length).toBe(1);
-  });
-
   test('getRequiredItemProps returns correct props', () => {
     const component = mount(<StatefulContainer {...getSharedProps()} />);
     const item = mockItems[0];
     const props = component.instance().getRequiredItemProps(item, 0);
-    expect(props).toEqual({
-      ref: React.createRef(),
-      isHighlighted: false,
-      onClick: props.onClick,
-    });
+
+    expect(props).toHaveProperty('disabled', false);
+    expect(props).toHaveProperty('isFocused', false);
+    expect(props).toHaveProperty('isHighlighted', false);
+    expect(props).toHaveProperty('onClick');
+    expect(props).toHaveProperty('onMouseEnter');
+    expect(props).toHaveProperty('ref');
+    expect(props).toHaveProperty('resetMenu');
+
     props.onClick();
     expect(mockItemSelect.mock.calls[0][0]).toEqual({
       item,
@@ -105,11 +104,14 @@ describe('Menu StatefulContainer', () => {
     const component = mount(<StatefulContainer {...getSharedProps()} />);
     const item = mockItems[1];
     const props = component.instance().getRequiredItemProps(item, 1);
-    expect(props).toEqual({
-      disabled: true,
-      ref: React.createRef(),
-      isHighlighted: false,
-    });
+
+    expect(props).toHaveProperty('disabled', true);
+    expect(props).toHaveProperty('isFocused', false);
+    expect(props).toHaveProperty('isHighlighted', false);
+    expect(props).toHaveProperty('onClick');
+    expect(props).toHaveProperty('onMouseEnter');
+    expect(props).toHaveProperty('ref');
+    expect(props).toHaveProperty('resetMenu');
   });
 
   test('getRequiredItemProps returns correct props for active child', () => {
@@ -119,11 +121,14 @@ describe('Menu StatefulContainer', () => {
     });
     const item = mockItems[0];
     const props = component.instance().getRequiredItemProps(item, 0);
-    expect(props).toEqual({
-      ref: React.createRef(),
-      isHighlighted: true,
-      onClick: props.onClick,
-    });
+
+    expect(props).toHaveProperty('disabled', false);
+    expect(props).toHaveProperty('isFocused', false);
+    expect(props).toHaveProperty('isHighlighted', true);
+    expect(props).toHaveProperty('onClick');
+    expect(props).toHaveProperty('onMouseEnter');
+    expect(props).toHaveProperty('ref');
+    expect(props).toHaveProperty('resetMenu');
   });
 
   test('onKeyDown - handleArrowKey', () => {
@@ -135,14 +140,15 @@ describe('Menu StatefulContainer', () => {
     component.instance().onKeyDown({
       key: KEY_STRINGS.ArrowUp,
       preventDefault: jest.fn(),
-      stopPropagation: jest.fn(),
     });
     expect(component.state('highlightedIndex')).toEqual(0);
+
     expect(props.stateReducer.mock.calls[0]).toEqual([
       STATE_CHANGE_TYPES.moveUp,
       {highlightedIndex: 0},
-      {highlightedIndex: -1},
+      {highlightedIndex: -1, isFocused: false},
     ]);
+
     const parent = React.createRef();
     const child = React.createRef();
     // $FlowFixMe
@@ -162,7 +168,7 @@ describe('Menu StatefulContainer', () => {
     expect(props.stateReducer.mock.calls[1]).toEqual([
       STATE_CHANGE_TYPES.moveDown,
       {highlightedIndex: 1},
-      {highlightedIndex: 0},
+      {highlightedIndex: 0, isFocused: false},
     ]);
   });
 
diff --git a/src/menu/__tests__/utils.test.js b/src/menu/__tests__/utils.test.js
index ff7cd9e9b..96b0bbb6d 100644
--- a/src/menu/__tests__/utils.test.js
+++ b/src/menu/__tests__/utils.test.js
@@ -7,22 +7,6 @@ LICENSE file in the root directory of this source tree.
 // @flow
 import * as Utils from '../utils.js';
 
-describe('Menu Utils - getSharedProps', () => {
-  test('successfully maps', () => {
-    const props = {
-      someStyletronProps: 'styletron',
-      normalProps: 'normal',
-    };
-    const mapper = {
-      someStyletronProps: true,
-    };
-    expect(Utils.getSharedProps(props, mapper)).toEqual({
-      $someStyletronProps: 'styletron',
-      normalProps: 'normal',
-    });
-  });
-});
-
 describe.only('Menu Utils - scrollItemIntoView', () => {
   // |=======================| <-- top of parent window (at 0)
   // | |-------------------| |