From aafc5d3897f30766015a2dad2573ea9b5fa754b5 Mon Sep 17 00:00:00 2001 From: Kelsey Young Date: Tue, 1 May 2018 18:03:33 -0700 Subject: [PATCH 1/8] Add open and dismiss functions that can be referenced by a contextual menu item --- .../ContextualMenu/ContextualMenu.tsx | 31 +++- .../ContextualMenu/ContextualMenu.types.ts | 9 +- .../ContextualMenu/ContextualMenuItem.tsx | 151 ++++++++++-------- .../ContextualMenuItem.types.ts | 48 +++++- .../ContextualMenuSplitButton.tsx | 20 ++- .../ContextualMenuSplitButton.types.ts | 21 ++- .../ContextualMenuItem.test.tsx.snap | 8 +- 7 files changed, 211 insertions(+), 77 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index 29e3e65a1b4a12..2630493acd45ca 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -24,7 +24,8 @@ import { getFirstFocusable, getLastFocusable, css, - shouldWrapFocus + shouldWrapFocus, + createRef } from '../../Utilities'; import { hasSubmenu, getIsChecked, isItemDisabled } from '../../utilities/contextualMenu/index'; import { withResponsiveMode, ResponsiveMode } from '../../utilities/decorators/withResponsiveMode'; @@ -499,6 +500,10 @@ export class ContextualMenu extends BaseComponent(); + const openSubMenu = this._onItemSubMenuExpand.bind(this); + const dismissSubMenu = this._onSubMenuDismiss.bind(this); + const dismissMenu = this.dismiss.bind(this, undefined); return (
@@ -511,6 +516,7 @@ export class ContextualMenu extends BaseComponent ) } @@ -599,6 +610,10 @@ export class ContextualMenu extends BaseComponent(); + const openSubMenu = this._onItemSubMenuExpand.bind(this); + const dismissSubMenu = this._onSubMenuDismiss.bind(this); + const dismissMenu = this.dismiss.bind(this, undefined); return ( { (keytipAttributes: any): JSX.Element => ( ) } @@ -635,6 +656,10 @@ export class ContextualMenu extends BaseComponent ); } diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts index b9c260cb25661d..55262cbfb032bc 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts @@ -15,7 +15,7 @@ import { IWithResponsiveModeState } from '../../utilities/decorators/withRespons import { IContextualMenuClassNames, IMenuItemClassNames } from './ContextualMenu.classNames'; export { DirectionalHint } from '../../common/DirectionalHint'; import { IVerticalDividerClassNames } from '../Divider/VerticalDivider.types'; -import { IContextualMenuItemProps } from './ContextualMenuItem.types'; +import { IContextualMenuItemProps, IContextualMenuRenderItem } from './ContextualMenuItem.types'; import { IKeytipProps } from '../../Keytip'; export enum ContextualMenuItemType { @@ -462,6 +462,12 @@ export interface IContextualMenuItem { */ keytipProps?: IKeytipProps; + /** + * Optional callback to access the IContextualMenuRenderItem interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + renderItemComponentRef?: (component: IContextualMenuRenderItem | null) => void; + /** * Any additional properties to use when custom rendering menu items. */ @@ -471,7 +477,6 @@ export interface IContextualMenuItem { * Optional prop to make an item readonly which is disabled but visitable by keyboard, will apply aria-readonly and some styling. Not supported by all components */ inactive?: boolean; - } export interface IContextualMenuSection extends React.Props { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx index 49171cbb873839..478f19eedd92f1 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx @@ -1,85 +1,108 @@ import * as React from 'react'; import { hasSubmenu, getIsChecked } from '../../utilities/contextualMenu/index'; -import { getRTL } from '../../Utilities'; +import { BaseComponent, getRTL } from '../../Utilities'; import { Icon } from '../../Icon'; import { IContextualMenuItemProps } from './ContextualMenuItem.types'; -const renderItemIcon = (props: IContextualMenuItemProps) => { - const { - item, - hasIcons, - classNames - } = props; +export class ContextualMenuItem extends BaseComponent { + public render() { + const { item, classNames } = this.props; - // Only present to allow continued use of item.icon which is deprecated. - const { iconProps, icon } = item; + return ( +
+ { this.renderCheckMarkIcon(this.props) } + { this.renderItemIcon(this.props) } + { this.renderItemName(this.props) } + { this.renderSubMenuIcon(this.props) } +
+ ); + } - if (!hasIcons) { - return null; + public openSubMenu = (): void => { + const { item, openSubMenu, subMenuTargetRef } = this.props; + if (hasSubmenu(item) && openSubMenu && subMenuTargetRef && subMenuTargetRef.current) { + openSubMenu(item, subMenuTargetRef.current); + } } - if (item.onRenderIcon) { - return ( - item.onRenderIcon(props) - ); + public dismissSubMenu = (): void => { + const { item, dismissSubMenu } = this.props; + if (hasSubmenu(item) && dismissSubMenu) { + dismissSubMenu(); + } } - if (iconProps) { - return ; + public dismissMenu = (dismissAll?: boolean): void => { + const { dismissMenu } = this.props; + if (dismissMenu) { + dismissMenu(dismissAll); + } } - return ; -}; + private renderItemIcon = (props: IContextualMenuItemProps) => { + const { + item, + hasIcons, + classNames + } = props; -const renderCheckMarkIcon = ({ onCheckmarkClick, item, classNames }: IContextualMenuItemProps) => { - const isItemChecked = getIsChecked(item); - if (onCheckmarkClick) { - const onClick = (e: React.MouseEvent) => onCheckmarkClick(item, e); + // Only present to allow continued use of item.icon which is deprecated. + const { iconProps, icon } = item; - return ( - - ); - } - return null; -}; + if (!hasIcons) { + return null; + } + + if (item.onRenderIcon) { + return ( + item.onRenderIcon(props) + ); + } -const renderItemName = ({ item, classNames }: IContextualMenuItemProps) => { - if (item.name) { - return { item.name }; + if (iconProps) { + return ; + } + + return ; } - return null; -}; -const renderSubMenuIcon = ({ item, classNames }: IContextualMenuItemProps) => { - if (hasSubmenu(item)) { - return ( - - ); + private renderCheckMarkIcon = ({ onCheckmarkClick, item, classNames }: IContextualMenuItemProps) => { + const isItemChecked = getIsChecked(item); + if (onCheckmarkClick) { + const onClick = (e: React.MouseEvent) => onCheckmarkClick(item, e); + + return ( + + ); + } + return null; } - return null; -}; -export const ContextualMenuItem: React.StatelessComponent = (props) => { - const { item, classNames } = props; + private renderItemName = ({ item, classNames }: IContextualMenuItemProps) => { + if (item.name) { + return { item.name }; + } + return null; + } - return ( -
- { renderCheckMarkIcon(props) } - { renderItemIcon(props) } - { renderItemName(props) } - { renderSubMenuIcon(props) } -
- ); -}; + private renderSubMenuIcon = ({ item, classNames }: IContextualMenuItemProps) => { + if (hasSubmenu(item)) { + return ( + + ); + } + return null; + } +} diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts index 189a54047b7d5d..5d41a1e9eedbbe 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts @@ -1,8 +1,31 @@ import { IContextualMenuItem } from './ContextualMenu.types'; import { IMenuItemClassNames } from './ContextualMenu.classNames'; +import { RefObject } from '../../Utilities'; -export interface IContextualMenuItemProps - extends React.HTMLAttributes { +export interface IContextualMenuRenderItem { + /** + * Function to open this item's subMenu, if present. + */ + openSubMenu: () => void; + + /** + * Function to close this item's subMenu, if present. + */ + dismissSubMenu: () => void; + + /** + * Dismiss the menu this item belongs to. + */ + dismissMenu: (dismissAll?: boolean) => void; +} + +export interface IContextualMenuItemProps extends React.HTMLAttributes { + + /** + * Optional callback to access the IContextualMenuRenderItem interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: IContextualMenuRenderItem | null) => void; /** * The item to display @@ -28,4 +51,25 @@ export interface IContextualMenuItemProps * Click handler for the checkmark */ onCheckmarkClick?: ((item: IContextualMenuItem, ev: React.MouseEvent) => void); + + /** + * This prop will get set by ContextualMenu and can be called to open this item's subMenu, if present. + */ + openSubMenu?: (item: IContextualMenuItem, target: HTMLElement) => void; + + /** + * This prop will get set by ContextualMenu and can be called to close this item's subMenu, if present. + */ + dismissSubMenu?: () => void; + + /** + * This prop will get set by ContextualMenu and can be called to close the menu this item belongs to. + * If dismissAll is true, all menus will be closed. + */ + dismissMenu?: (dismissAll?: boolean) => void; + + /** + * This prop will get set by ContextualMenu, will be the target to attach the submenu to when openSubMenu is triggered. + */ + subMenuTargetRef?: RefObject; } diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx index 0a2975b67251d0..8997ddb5c05b14 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx @@ -13,7 +13,7 @@ import { } from './ContextualMenu.classNames'; import { ContextualMenuItem } from './ContextualMenuItem'; import { KeytipData } from '../../KeytipData'; -import { getIsChecked, isItemDisabled } from '../../utilities/contextualMenu/index'; +import { getIsChecked, isItemDisabled, hasSubmenu } from '../../utilities/contextualMenu/index'; import { VerticalDivider } from '../../Divider'; import { IContextualMenuSplitButtonProps } from './ContextualMenuSplitButton.types'; @@ -74,6 +74,24 @@ export class ContextualMenuSplitButton extends BaseComponent { + if (hasSubmenu(this.props.item) && this.props.openSubMenu && this._splitButton) { + this.props.openSubMenu(this.props.item, this._splitButton); + } + } + + public dismissSubMenu = (): void => { + if (hasSubmenu(this.props.item) && this.props.dismissSubMenu) { + this.props.dismissSubMenu(); + } + } + + public dismissMenu = (dismissAll?: boolean): void => { + if (this.props.dismissMenu) { + this.props.dismissMenu(dismissAll); + } + } + private _renderSplitPrimaryButton(item: IContextualMenuItem, classNames: IMenuItemClassNames, index: number, hasCheckmarks: boolean, hasIcons: boolean) { const isChecked: boolean | null | undefined = getIsChecked(item); const canCheck: boolean = isChecked !== null; diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts index 1e7ebdb801f2c5..9b184325f263f3 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts @@ -1,15 +1,14 @@ -import { IContextualMenuItem } from '../../ContextualMenu'; +import { IContextualMenuItem, IContextualMenuRenderItem } from '../../ContextualMenu'; import { IMenuItemClassNames } from './ContextualMenu.classNames'; import { IContextualMenuItemProps } from './ContextualMenuItem.types'; -import { ContextualMenuSplitButton } from './ContextualMenuSplitButton'; export interface IContextualMenuSplitButtonProps extends React.Props { /** * Optional callback to access the ContextualmenuSplitButton interface. Use this instead of ref for accessing * the public methods and properties of the component. */ - componentRef?: (component: ContextualMenuSplitButton | null) => void; + componentRef?: (component: IContextualMenuRenderItem | null) => void; /** * The item that is used to render the split button. @@ -91,4 +90,20 @@ export interface IContextualMenuSplitButtonProps extends React.Props) => void; + + /** + * This prop will get set by ContextualMenu and can be called to open this item's subMenu, if present. + */ + openSubMenu?: (item: any, target: HTMLElement) => void; + + /** + * This prop will get set by ContextualMenu and can be called to close this item's subMenu, if present. + */ + dismissSubMenu?: () => void; + + /** + * This props will get set by ContextualMenu and can be called to close the menu this item belongs to. + * If dismissAll is true, all menus will be closed. + */ + dismissMenu?: (dismissAll?: boolean) => void; } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap index 11c4226f3d2f8f..f641b8f594868d 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap @@ -4,7 +4,7 @@ exports[`ContextMenuItemChildren when a checkmark icon renders the component wit ShallowWrapper { "length": 1, Symbol(enzyme.__root__): [Circular], - Symbol(enzyme.__unrendered__): Date: Wed, 2 May 2018 11:12:23 -0700 Subject: [PATCH 2/8] Add tests for new item functions --- .../ContextualMenu/ContextualMenu.test.tsx | 61 ++++++++++++++++++- .../ContextualMenuSplitButton.types.ts | 2 +- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx index 48505643659b01..aed95deec35e7e 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx @@ -2,12 +2,14 @@ import * as React from 'react'; import { Promise } from 'es6-promise'; import * as ReactTestUtils from 'react-dom/test-utils'; import { - KeyCodes + KeyCodes, + createRef } from '../../Utilities'; import { FocusZoneDirection } from '../../FocusZone'; import { ContextualMenu, canAnyMenuItemsCheck } from './ContextualMenu'; import { IContextualMenuItem, ContextualMenuItemType } from './ContextualMenu.types'; +import { IContextualMenuRenderItem } from './ContextualMenuItem.types'; import { LayerBase as Layer } from '../Layer/Layer.base'; describe('ContextualMenu', () => { @@ -823,4 +825,61 @@ describe('ContextualMenu', () => { expect(canAnyMenuItemsCheck(items)).toEqual(true); }); }); + + describe('IContextualMenuRenderItem function tests', () => { + const contextualItem = createRef(); + let menuDismissed: boolean; + const onDismiss = (ev?: any, dismissAll?: boolean) => { menuDismissed = true; }; + + beforeEach(() => { + menuDismissed = false; + const menu: IContextualMenuItem[] = [ + { + name: 'Test1', + key: 'Test1', + renderItemComponentRef: contextualItem, + subMenuProps: { + items: [ + { + name: 'Test2', + key: 'Test2', + className: 'SubMenuClass' + }, + { + name: 'Test3', + key: 'Test3', + className: 'SubMenuClass' + } + ], + } + } + ]; + + ReactTestUtils.renderIntoDocument( + + ); + }); + + it('openSubMenu will open the item`s submenu if present', () => { + contextualItem.value!.openSubMenu(); + expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + }); + + it('dismissSubMenu will close the item`s submenu if present', () => { + // Open the submenu with a click + const menuItem = document.querySelector('button.ms-ContextualMenu-link') as HTMLButtonElement; + ReactTestUtils.Simulate.click(menuItem); + expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + contextualItem.value!.dismissSubMenu(); + expect(document.querySelector('.SubMenuClass')).toEqual(null); + }); + + it('dismissMenu will close the item`s menu', () => { + contextualItem.value!.dismissMenu(); + expect(menuDismissed).toEqual(true); + }); + }); }); diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts index 9b184325f263f3..99da786fc05b92 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts @@ -102,7 +102,7 @@ export interface IContextualMenuSplitButtonProps extends React.Props void; /** - * This props will get set by ContextualMenu and can be called to close the menu this item belongs to. + * This prop will get set by ContextualMenu and can be called to close the menu this item belongs to. * If dismissAll is true, all menus will be closed. */ dismissMenu?: (dismissAll?: boolean) => void; From da3bef774699a954d4d94f967798c338ce02bde2 Mon Sep 17 00:00:00 2001 From: Kelsey Young Date: Wed, 2 May 2018 11:14:35 -0700 Subject: [PATCH 3/8] Add rush change file --- ...ntextual-menu-item-functions_2018-05-02-18-14.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/office-ui-fabric-react/keyou-contextual-menu-item-functions_2018-05-02-18-14.json diff --git a/common/changes/office-ui-fabric-react/keyou-contextual-menu-item-functions_2018-05-02-18-14.json b/common/changes/office-ui-fabric-react/keyou-contextual-menu-item-functions_2018-05-02-18-14.json new file mode 100644 index 00000000000000..c413bf6f9c0063 --- /dev/null +++ b/common/changes/office-ui-fabric-react/keyou-contextual-menu-item-functions_2018-05-02-18-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Add ContextualMenuItem functions to open and close menus", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "keyou@microsoft.com" +} \ No newline at end of file From 4fda842bef937b44a011e9195cb6d82d2b4fb3bb Mon Sep 17 00:00:00 2001 From: Kelsey Young Date: Wed, 2 May 2018 14:41:28 -0700 Subject: [PATCH 4/8] Remove binds in submenu function - Move private ContextualMenuItem functions back to local functions - Renames and refactoring --- .../ContextualMenu/ContextualMenu.test.tsx | 2 +- .../ContextualMenu/ContextualMenu.tsx | 20 ++- .../ContextualMenu/ContextualMenu.types.ts | 5 +- .../ContextualMenu/ContextualMenuItem.tsx | 134 +++++++++--------- .../ContextualMenuSplitButton.tsx | 15 +- 5 files changed, 87 insertions(+), 89 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx index aed95deec35e7e..c0fea563be96d2 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx @@ -837,7 +837,7 @@ describe('ContextualMenu', () => { { name: 'Test1', key: 'Test1', - renderItemComponentRef: contextualItem, + renderItemRef: contextualItem, subMenuProps: { items: [ { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index 2630493acd45ca..7748e0ec2ca57a 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -501,8 +501,6 @@ export class ContextualMenu extends BaseComponent(); - const openSubMenu = this._onItemSubMenuExpand.bind(this); - const dismissSubMenu = this._onSubMenuDismiss.bind(this); const dismissMenu = this.dismiss.bind(this, undefined); return ( @@ -540,9 +538,9 @@ export class ContextualMenu extends BaseComponent @@ -611,8 +609,6 @@ export class ContextualMenu extends BaseComponent(); - const openSubMenu = this._onItemSubMenuExpand.bind(this); - const dismissSubMenu = this._onSubMenuDismiss.bind(this); const dismissMenu = this.dismiss.bind(this, undefined); return ( @@ -634,9 +630,9 @@ export class ContextualMenu extends BaseComponent @@ -681,7 +677,7 @@ export class ContextualMenu extends BaseComponent ); } @@ -918,7 +914,7 @@ export class ContextualMenu extends BaseComponent { if (this.state.expandedMenuItemKey !== item.key) { if (this.state.expandedMenuItemKey) { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts index 55262cbfb032bc..7e5309912f01b2 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts @@ -463,10 +463,9 @@ export interface IContextualMenuItem { keytipProps?: IKeytipProps; /** - * Optional callback to access the IContextualMenuRenderItem interface. Use this instead of ref for accessing - * the public methods and properties of the component. + * Optional callback to access the IContextualMenuRenderItem interface. This will get passed down to the item that renders the ContextualMenuItem. */ - renderItemComponentRef?: (component: IContextualMenuRenderItem | null) => void; + renderItemRef?: (component: IContextualMenuRenderItem | null) => void; /** * Any additional properties to use when custom rendering menu items. diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx index 478f19eedd92f1..4f4acf8814ed2c 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx @@ -4,6 +4,69 @@ import { BaseComponent, getRTL } from '../../Utilities'; import { Icon } from '../../Icon'; import { IContextualMenuItemProps } from './ContextualMenuItem.types'; +const renderItemIcon = (props: IContextualMenuItemProps) => { + const { + item, + hasIcons, + classNames + } = props; + + // Only present to allow continued use of item.icon which is deprecated. + const { iconProps, icon } = item; + + if (!hasIcons) { + return null; + } + + if (item.onRenderIcon) { + return ( + item.onRenderIcon(props) + ); + } + + if (iconProps) { + return ; + } + + return ; +}; + +const renderCheckMarkIcon = ({ onCheckmarkClick, item, classNames }: IContextualMenuItemProps) => { + const isItemChecked = getIsChecked(item); + if (onCheckmarkClick) { + const onClick = (e: React.MouseEvent) => onCheckmarkClick(item, e); + + return ( + + ); + } + return null; +}; + +const renderItemName = ({ item, classNames }: IContextualMenuItemProps) => { + if (item.name) { + return { item.name }; + } + return null; +}; + +const renderSubMenuIcon = ({ item, classNames }: IContextualMenuItemProps) => { + if (hasSubmenu(item)) { + return ( + + ); + } + return null; +}; + export class ContextualMenuItem extends BaseComponent { public render() { const { item, classNames } = this.props; @@ -14,10 +77,10 @@ export class ContextualMenuItem extends BaseComponent - { this.renderCheckMarkIcon(this.props) } - { this.renderItemIcon(this.props) } - { this.renderItemName(this.props) } - { this.renderSubMenuIcon(this.props) } + { renderCheckMarkIcon(this.props) } + { renderItemIcon(this.props) } + { renderItemName(this.props) } + { renderSubMenuIcon(this.props) }
); } @@ -42,67 +105,4 @@ export class ContextualMenuItem extends BaseComponent { - const { - item, - hasIcons, - classNames - } = props; - - // Only present to allow continued use of item.icon which is deprecated. - const { iconProps, icon } = item; - - if (!hasIcons) { - return null; - } - - if (item.onRenderIcon) { - return ( - item.onRenderIcon(props) - ); - } - - if (iconProps) { - return ; - } - - return ; - } - - private renderCheckMarkIcon = ({ onCheckmarkClick, item, classNames }: IContextualMenuItemProps) => { - const isItemChecked = getIsChecked(item); - if (onCheckmarkClick) { - const onClick = (e: React.MouseEvent) => onCheckmarkClick(item, e); - - return ( - - ); - } - return null; - } - - private renderItemName = ({ item, classNames }: IContextualMenuItemProps) => { - if (item.name) { - return { item.name }; - } - return null; - } - - private renderSubMenuIcon = ({ item, classNames }: IContextualMenuItemProps) => { - if (hasSubmenu(item)) { - return ( - - ); - } - return null; - } } diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx index 8997ddb5c05b14..b7ad185d5ae386 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx @@ -75,20 +75,23 @@ export class ContextualMenuSplitButton extends BaseComponent { - if (hasSubmenu(this.props.item) && this.props.openSubMenu && this._splitButton) { - this.props.openSubMenu(this.props.item, this._splitButton); + const { item, openSubMenu } = this.props; + if (hasSubmenu(item) && openSubMenu && this._splitButton) { + openSubMenu(item, this._splitButton); } } public dismissSubMenu = (): void => { - if (hasSubmenu(this.props.item) && this.props.dismissSubMenu) { - this.props.dismissSubMenu(); + const { item, dismissSubMenu } = this.props; + if (hasSubmenu(item) && dismissSubMenu) { + dismissSubMenu(); } } public dismissMenu = (dismissAll?: boolean): void => { - if (this.props.dismissMenu) { - this.props.dismissMenu(dismissAll); + const { dismissMenu } = this.props; + if (dismissMenu) { + dismissMenu(dismissAll); } } From 8d73bf7cae9e4d2349bfd906d31fa11230592224 Mon Sep 17 00:00:00 2001 From: Kelsey Young Date: Thu, 3 May 2018 17:27:51 -0700 Subject: [PATCH 5/8] Move contextual menu button and anchor into their own component - Move all wrapper items into their own folder under ContextualMenu --- .../ContextualMenu/ContextualMenu.test.tsx | 6 +- .../ContextualMenu/ContextualMenu.tsx | 218 +++++------------- .../ContextualMenu/ContextualMenu.types.ts | 13 +- .../ContextualMenu/ContextualMenuItem.tsx | 55 ++--- .../ContextualMenuItem.types.ts | 46 ---- .../ContextualMenuAnchor.test.tsx | 47 ++++ .../ContextualMenuAnchor.tsx | 136 +++++++++++ .../ContextualMenuAnchor.types.ts | 14 ++ .../ContextualMenuButton.test.tsx | 47 ++++ .../ContextualMenuButton.tsx | 166 +++++++++++++ .../ContextualMenuButton.types.ts | 14 ++ .../ContextualMenuSplitButton.test.tsx | 4 +- .../ContextualMenuSplitButton.tsx | 9 +- .../ContextualMenuSplitButton.types.ts | 8 + .../IContextualMenuItemWrapper.ts} | 34 ++- .../ContextualMenuAnchor.test.tsx.snap | 28 +++ .../ContextualMenuButton.test.tsx.snap | 29 +++ .../ContextualMenuSplitButton.test.tsx.snap | 0 .../ContextualMenuItemWrapper/index.ts | 7 + .../ContextualMenuItem.test.tsx.snap | 8 +- 20 files changed, 613 insertions(+), 276 deletions(-) create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.test.tsx create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.test.tsx create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts rename packages/office-ui-fabric-react/src/components/ContextualMenu/{ => ContextualMenuItemWrapper}/ContextualMenuSplitButton.test.tsx (90%) rename packages/office-ui-fabric-react/src/components/ContextualMenu/{ => ContextualMenuItemWrapper}/ContextualMenuSplitButton.tsx (97%) create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts rename packages/office-ui-fabric-react/src/components/ContextualMenu/{ContextualMenuSplitButton.types.ts => ContextualMenuItemWrapper/IContextualMenuItemWrapper.ts} (81%) create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuAnchor.test.tsx.snap create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuButton.test.tsx.snap rename packages/office-ui-fabric-react/src/components/ContextualMenu/{ => ContextualMenuItemWrapper}/__snapshots__/ContextualMenuSplitButton.test.tsx.snap (100%) create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx index 059da5cc148cbe..ab5642ed213899 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx @@ -9,7 +9,7 @@ import { FocusZoneDirection } from '../../FocusZone'; import { ContextualMenu, canAnyMenuItemsCheck } from './ContextualMenu'; import { IContextualMenuItem, ContextualMenuItemType } from './ContextualMenu.types'; -import { IContextualMenuRenderItem } from './ContextualMenuItem.types'; +import { IContextualMenuItemWrapper } from './ContextualMenuItemWrapper'; import { LayerBase as Layer } from '../Layer/Layer.base'; describe('ContextualMenu', () => { @@ -863,7 +863,7 @@ describe('ContextualMenu', () => { }); describe('IContextualMenuRenderItem function tests', () => { - const contextualItem = createRef(); + const contextualItem = createRef(); let menuDismissed: boolean; const onDismiss = (ev?: any, dismissAll?: boolean) => { menuDismissed = true; }; @@ -873,7 +873,7 @@ describe('ContextualMenu', () => { { name: 'Test1', key: 'Test1', - renderItemRef: contextualItem, + componentRef: contextualItem, subMenuProps: { items: [ { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index 2a49d40664ad53..1f2ad071f3cd75 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -11,9 +11,6 @@ import { import { BaseComponent, IPoint, - anchorProperties, - buttonProperties, - getNativeProps, assign, getId, getRTL, @@ -24,16 +21,14 @@ import { getFirstFocusable, getLastFocusable, css, - shouldWrapFocus, - createRef + shouldWrapFocus } from '../../Utilities'; import { hasSubmenu, getIsChecked, isItemDisabled } from '../../utilities/contextualMenu/index'; import { withResponsiveMode, ResponsiveMode } from '../../utilities/decorators/withResponsiveMode'; import { Callout } from '../../Callout'; import { IIconProps } from '../../Icon'; import { ContextualMenuItem } from './ContextualMenuItem'; -import { KeytipData } from '../../KeytipData'; -import { ContextualMenuSplitButton } from './ContextualMenuSplitButton'; +import { ContextualMenuSplitButton, ContextualMenuButton, ContextualMenuAnchor } from './ContextualMenuItemWrapper'; export interface IContextualMenuState { expandedMenuItemKey?: string; @@ -486,66 +481,34 @@ export class ContextualMenu extends BaseComponent(); + const { contextualMenuItemAs } = this.props; const dismissMenu = this.dismiss.bind(this, undefined); - + const { expandedMenuItemKey } = this.state; return ( -
- - { (keytipAttributes: any): JSX.Element => ( - - - - ) } - -
); + + ); } private _renderButtonItem( @@ -556,87 +519,35 @@ export class ContextualMenu extends BaseComponent this._onItemMouseDown(item, ev), - onMouseMove: this._onItemMouseMove.bind(this, item), - href: item.href, - title: item.title, - 'aria-label': ariaLabel, - 'aria-haspopup': itemHasSubmenu || undefined, - 'aria-owns': item.key === expandedMenuItemKey ? subMenuId : undefined, - 'aria-expanded': itemHasSubmenu ? item.key === expandedMenuItemKey : undefined, - 'aria-checked': !!isChecked, - 'aria-posinset': focusableElementIndex + 1, - 'aria-setsize': totalItemCount, - 'aria-disabled': isItemDisabled(item), - role: item.role || defaultRole, - style: item.style - }; - - let { keytipProps } = item; - if (keytipProps && itemHasSubmenu) { - keytipProps = { - ...keytipProps, - hasMenu: true - }; - } - const btnRef = createRef(); + const { contextualMenuItemAs } = this.props; const dismissMenu = this.dismiss.bind(this, undefined); + const { expandedMenuItemKey } = this.state; return ( - - { (keytipAttributes: any): JSX.Element => ( - - ) } - + ); } @@ -649,9 +560,6 @@ export class ContextualMenu extends BaseComponent ); @@ -756,10 +664,6 @@ export class ContextualMenu extends BaseComponent { this._isScrollIdle = true; }, NavigationIdleDelay); } - private _onItemMouseEnter = (item: any, ev: React.MouseEvent): void => { - this._onItemMouseEnterBase(item, ev, ev.currentTarget as HTMLElement); - } - private _onItemMouseEnterBase = (item: any, ev: React.MouseEvent, target?: HTMLElement): void => { if (!this._isScrollIdle) { return; @@ -768,10 +672,6 @@ export class ContextualMenu extends BaseComponent) { - this._onItemMouseMoveBase(item, ev, ev.currentTarget as HTMLElement); - } - private _onItemMouseMoveBase = (item: any, ev: React.MouseEvent, target: HTMLElement): void => { const targetElement = ev.currentTarget as HTMLElement; @@ -882,7 +782,7 @@ export class ContextualMenu extends BaseComponent) { + private _onAnchorClick = (item: IContextualMenuItem, ev: React.MouseEvent) => { this._executeItemClick(item, ev); ev.stopPropagation(); } @@ -1019,7 +919,7 @@ export class ContextualMenu extends BaseComponent { let { subMenuId } = this.state; if (item.subMenuProps && item.subMenuProps.id) { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts index 7e5309912f01b2..0fcd28fa404d67 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts @@ -15,7 +15,8 @@ import { IWithResponsiveModeState } from '../../utilities/decorators/withRespons import { IContextualMenuClassNames, IMenuItemClassNames } from './ContextualMenu.classNames'; export { DirectionalHint } from '../../common/DirectionalHint'; import { IVerticalDividerClassNames } from '../Divider/VerticalDivider.types'; -import { IContextualMenuItemProps, IContextualMenuRenderItem } from './ContextualMenuItem.types'; +import { IContextualMenuItemProps } from './ContextualMenuItem.types'; +import { IContextualMenuItemWrapper } from './ContextualMenuItemWrapper'; import { IKeytipProps } from '../../Keytip'; export enum ContextualMenuItemType { @@ -255,6 +256,11 @@ export interface IContextualMenuProps extends React.Props, IWith } export interface IContextualMenuItem { + /** + * Optional callback to access the IContextualMenuItemWrapper interface. This will get passed down to the item that renders the ContextualMenuItem. + */ + componentRef?: (component: IContextualMenuItemWrapper | null) => void; + /** * Unique id to identify the item */ @@ -462,11 +468,6 @@ export interface IContextualMenuItem { */ keytipProps?: IKeytipProps; - /** - * Optional callback to access the IContextualMenuRenderItem interface. This will get passed down to the item that renders the ContextualMenuItem. - */ - renderItemRef?: (component: IContextualMenuRenderItem | null) => void; - /** * Any additional properties to use when custom rendering menu items. */ diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx index 4f4acf8814ed2c..95f50568e7dd6a 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { hasSubmenu, getIsChecked } from '../../utilities/contextualMenu/index'; -import { BaseComponent, getRTL } from '../../Utilities'; +import { getRTL } from '../../Utilities'; import { Icon } from '../../Icon'; import { IContextualMenuItemProps } from './ContextualMenuItem.types'; @@ -67,42 +67,19 @@ const renderSubMenuIcon = ({ item, classNames }: IContextualMenuItemProps) => { return null; }; -export class ContextualMenuItem extends BaseComponent { - public render() { - const { item, classNames } = this.props; +export const ContextualMenuItem: React.StatelessComponent = (props) => { + const { item, classNames } = props; - return ( -
- { renderCheckMarkIcon(this.props) } - { renderItemIcon(this.props) } - { renderItemName(this.props) } - { renderSubMenuIcon(this.props) } -
- ); - } - - public openSubMenu = (): void => { - const { item, openSubMenu, subMenuTargetRef } = this.props; - if (hasSubmenu(item) && openSubMenu && subMenuTargetRef && subMenuTargetRef.current) { - openSubMenu(item, subMenuTargetRef.current); - } - } - - public dismissSubMenu = (): void => { - const { item, dismissSubMenu } = this.props; - if (hasSubmenu(item) && dismissSubMenu) { - dismissSubMenu(); - } - } - - public dismissMenu = (dismissAll?: boolean): void => { - const { dismissMenu } = this.props; - if (dismissMenu) { - dismissMenu(dismissAll); - } - } -} + return ( +
+ { renderCheckMarkIcon(props) } + { renderItemIcon(props) } + { renderItemName(props) } + { renderSubMenuIcon(props) } +
+ ); +}; \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts index 5d41a1e9eedbbe..d5b7c5bddb317d 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts @@ -1,32 +1,7 @@ import { IContextualMenuItem } from './ContextualMenu.types'; import { IMenuItemClassNames } from './ContextualMenu.classNames'; -import { RefObject } from '../../Utilities'; - -export interface IContextualMenuRenderItem { - /** - * Function to open this item's subMenu, if present. - */ - openSubMenu: () => void; - - /** - * Function to close this item's subMenu, if present. - */ - dismissSubMenu: () => void; - - /** - * Dismiss the menu this item belongs to. - */ - dismissMenu: (dismissAll?: boolean) => void; -} export interface IContextualMenuItemProps extends React.HTMLAttributes { - - /** - * Optional callback to access the IContextualMenuRenderItem interface. Use this instead of ref for accessing - * the public methods and properties of the component. - */ - componentRef?: (component: IContextualMenuRenderItem | null) => void; - /** * The item to display */ @@ -51,25 +26,4 @@ export interface IContextualMenuItemProps extends React.HTMLAttributes) => void); - - /** - * This prop will get set by ContextualMenu and can be called to open this item's subMenu, if present. - */ - openSubMenu?: (item: IContextualMenuItem, target: HTMLElement) => void; - - /** - * This prop will get set by ContextualMenu and can be called to close this item's subMenu, if present. - */ - dismissSubMenu?: () => void; - - /** - * This prop will get set by ContextualMenu and can be called to close the menu this item belongs to. - * If dismissAll is true, all menus will be closed. - */ - dismissMenu?: (dismissAll?: boolean) => void; - - /** - * This prop will get set by ContextualMenu, will be the target to attach the submenu to when openSubMenu is triggered. - */ - subMenuTargetRef?: RefObject; } diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.test.tsx new file mode 100644 index 00000000000000..b225c5d81d03d6 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.test.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import * as renderer from 'react-test-renderer'; +import { IContextualMenuItem } from '../ContextualMenu.types'; +import { IMenuItemClassNames } from '../ContextualMenu.classNames'; +import { ContextualMenuAnchor } from './ContextualMenuAnchor'; + +describe('ContextualMenuButton', () => { + describe('creates a normal button', () => { + let menuItem: IContextualMenuItem; + let menuClassNames: IMenuItemClassNames; + + beforeEach(() => { + menuItem = { key: '123' }; + menuClassNames = getMenuItemClassNames(); + }); + + it('renders the contextual menu split button correctly', () => { + const component = renderer.create( + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + }); +}); + +function getMenuItemClassNames(): IMenuItemClassNames { + return { + item: 'item', + divider: '---', + root: 'root', + linkContent: 'linkContent', + icon: 'icon', + checkmarkIcon: 'checkmarkIcon', + subMenuIcon: 'subMenuIcon', + label: 'label', + splitContainer: 'splitContainer', + splitPrimary: 'splitPrimary', + splitMenu: 'splitMenu', + linkContentMenu: 'linkContentMenu', + }; +} diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx new file mode 100644 index 00000000000000..cf494b2518c408 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx @@ -0,0 +1,136 @@ +import * as React from 'react'; +import { + BaseComponent, + anchorProperties, + getNativeProps, +} from '../../../Utilities'; +import { IContextualMenuAnchorProps } from './ContextualMenuAnchor.types'; +import { KeytipData } from '../../KeytipData'; +import { isItemDisabled, hasSubmenu } from '../../../utilities/contextualMenu/index'; +import { IContextualMenuItem, ContextualMenuItem } from '../../ContextualMenu'; + +export class ContextualMenuAnchor extends BaseComponent { + private _anchor: HTMLAnchorElement; + + public render() { + const { + item, + classNames, + index, + focusableElementIndex, + totalItemCount, + hasCheckmarks, + hasIcons, + contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem, + expandedMenuItemKey, + onItemClick + } = this.props; + + let anchorRel = item.rel; + if (item.target && item.target.toLowerCase() === '_blank') { + anchorRel = anchorRel ? anchorRel : 'nofollow noopener noreferrer'; // Safe default to prevent tabjacking + } + + const subMenuId = this._getSubMenuId(item); + const itemHasSubmenu = hasSubmenu(item); + const nativeProps = getNativeProps(item, anchorProperties); + const disabled = isItemDisabled(item); + + return ( + ); + } + + public openSubMenu = (): void => { + const { item, openSubMenu } = this.props; + if (hasSubmenu(item) && openSubMenu && this._anchor) { + openSubMenu(item, this._anchor); + } + } + + public dismissSubMenu = (): void => { + const { item, dismissSubMenu } = this.props; + if (hasSubmenu(item) && dismissSubMenu) { + dismissSubMenu(); + } + } + + public dismissMenu = (dismissAll?: boolean): void => { + const { dismissMenu } = this.props; + if (dismissMenu) { + dismissMenu(dismissAll); + } + } + + private _onItemMouseEnter = (ev: React.MouseEvent): void => { + const { item, onItemMouseEnter } = this.props; + if (onItemMouseEnter) { + onItemMouseEnter(item, ev, ev.currentTarget as HTMLElement); + } + } + + private _onItemClick = (ev: React.MouseEvent): void => { + const { item, onItemClick } = this.props; + if (onItemClick) { + onItemClick(item, ev); + } + } + + private _onItemMouseLeave = (ev: React.MouseEvent): void => { + const { item, onItemMouseLeave } = this.props; + if (onItemMouseLeave) { + onItemMouseLeave(item, ev); + } + } + + private _onItemKeyDown = (ev: React.KeyboardEvent): void => { + const { item, onItemKeyDown } = this.props; + if (onItemKeyDown) { + onItemKeyDown(item, ev); + } + } + + private _getSubMenuId = (item: IContextualMenuItem): string | undefined => { + const { getSubMenuId } = this.props; + if (getSubMenuId) { + return getSubMenuId(item); + } + } +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts new file mode 100644 index 00000000000000..ead336dad99ffa --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts @@ -0,0 +1,14 @@ +import { IContextualMenuItem } from '../../ContextualMenu'; +import { IContextualMenuItemWrapperProps } from './IContextualMenuItemWrapper'; + +export interface IContextualMenuAnchorProps extends IContextualMenuItemWrapperProps { + /** + * Callback to get the subMenu ID for an IContextualMenuItem + */ + getSubMenuId?: (item: IContextualMenuItem) => string | undefined; + + /** + * Key of the currently expanded subMenu + */ + expandedMenuItemKey?: string; +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.test.tsx new file mode 100644 index 00000000000000..d78a6eabf2e80b --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.test.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import * as renderer from 'react-test-renderer'; +import { IContextualMenuItem } from '../ContextualMenu.types'; +import { IMenuItemClassNames } from '../ContextualMenu.classNames'; +import { ContextualMenuButton } from './ContextualMenuButton'; + +describe('ContextualMenuButton', () => { + describe('creates a normal button', () => { + let menuItem: IContextualMenuItem; + let menuClassNames: IMenuItemClassNames; + + beforeEach(() => { + menuItem = { key: '123' }; + menuClassNames = getMenuItemClassNames(); + }); + + it('renders the contextual menu split button correctly', () => { + const component = renderer.create( + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + }); +}); + +function getMenuItemClassNames(): IMenuItemClassNames { + return { + item: 'item', + divider: '---', + root: 'root', + linkContent: 'linkContent', + icon: 'icon', + checkmarkIcon: 'checkmarkIcon', + subMenuIcon: 'subMenuIcon', + label: 'label', + splitContainer: 'splitContainer', + splitPrimary: 'splitPrimary', + splitMenu: 'splitMenu', + linkContentMenu: 'linkContentMenu', + }; +} diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx new file mode 100644 index 00000000000000..4b86d083691fb9 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx @@ -0,0 +1,166 @@ +import * as React from 'react'; +import { + BaseComponent, + buttonProperties, + getNativeProps, +} from '../../../Utilities'; +import { IContextualMenuButtonProps } from './ContextualMenuButton.types'; +import { KeytipData } from '../../KeytipData'; +import { getIsChecked, isItemDisabled, hasSubmenu } from '../../../utilities/contextualMenu/index'; +import { IContextualMenuItem, ContextualMenuItem } from '../../ContextualMenu'; + +export class ContextualMenuButton extends BaseComponent { + private _btn: HTMLButtonElement; + + public render() { + const { + item, + classNames, + index, + focusableElementIndex, + totalItemCount, + hasCheckmarks, + hasIcons, + contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem, + expandedMenuItemKey, + onItemMouseDown, + onItemClick + } = this.props; + + let { keytipProps } = item; + if (keytipProps) { + keytipProps = { + ...keytipProps, + hasMenu: true + }; + } + + const subMenuId = this._getSubMenuId(item); + let ariaLabel = ''; + + if (item.ariaLabel) { + ariaLabel = item.ariaLabel; + } else if (item.name) { + ariaLabel = item.name; + } + + const isChecked: boolean | null | undefined = getIsChecked(item); + const canCheck: boolean = isChecked !== null; + const defaultRole = canCheck ? 'menuitemcheckbox' : 'menuitem'; + const itemHasSubmenu = hasSubmenu(item); + + const buttonNativeProperties = getNativeProps(item, buttonProperties); + // Do not add the disabled attribute to the button so that it is focusable + delete (buttonNativeProperties as any).disabled; + + const itemButtonProperties = { + className: classNames.root, + onClick: this._onItemClick, + onKeyDown: itemHasSubmenu ? this._onItemKeyDown : null, + onMouseEnter: this._onItemMouseEnter, + onMouseLeave: this._onItemMouseLeave, + onMouseDown: (ev: any) => onItemMouseDown ? onItemMouseDown(item, ev) : undefined, + onMouseMove: this._onItemMouseMove, + href: item.href, + title: item.title, + 'aria-label': ariaLabel, + 'aria-haspopup': itemHasSubmenu || undefined, + 'aria-owns': item.key === expandedMenuItemKey ? subMenuId : undefined, + 'aria-expanded': itemHasSubmenu ? item.key === expandedMenuItemKey : undefined, + 'aria-checked': !!isChecked, + 'aria-posinset': focusableElementIndex + 1, + 'aria-setsize': totalItemCount, + 'aria-disabled': isItemDisabled(item), + role: item.role || defaultRole, + style: item.style + }; + + return ( + + { (keytipAttributes: any): JSX.Element => ( + + ) } + + ); + } + + public openSubMenu = (): void => { + const { item, openSubMenu } = this.props; + if (hasSubmenu(item) && openSubMenu && this._btn) { + openSubMenu(item, this._btn); + } + } + + public dismissSubMenu = (): void => { + const { item, dismissSubMenu } = this.props; + if (hasSubmenu(item) && dismissSubMenu) { + dismissSubMenu(); + } + } + + public dismissMenu = (dismissAll?: boolean): void => { + const { dismissMenu } = this.props; + if (dismissMenu) { + dismissMenu(dismissAll); + } + } + + private _onItemMouseEnter = (ev: React.MouseEvent): void => { + const { item, onItemMouseEnter } = this.props; + if (onItemMouseEnter) { + onItemMouseEnter(item, ev, ev.currentTarget as HTMLElement); + } + } + + private _onItemMouseMove = (ev: React.MouseEvent): void => { + const { item, onItemMouseMove } = this.props; + if (onItemMouseMove) { + onItemMouseMove(item, ev, ev.currentTarget as HTMLElement); + } + } + + private _onItemClick = (ev: React.MouseEvent): void => { + const { item, onItemClickBase } = this.props; + if (onItemClickBase) { + onItemClickBase(item, ev, ev.currentTarget as HTMLElement); + } + } + + private _onItemMouseLeave = (ev: React.MouseEvent): void => { + const { item, onItemMouseLeave } = this.props; + if (onItemMouseLeave) { + onItemMouseLeave(item, ev); + } + } + + private _onItemKeyDown = (ev: React.KeyboardEvent): void => { + const { item, onItemKeyDown } = this.props; + if (onItemKeyDown) { + onItemKeyDown(item, ev); + } + } + + private _getSubMenuId = (item: IContextualMenuItem): string | undefined => { + const { getSubMenuId } = this.props; + if (getSubMenuId) { + return getSubMenuId(item); + } + } +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts new file mode 100644 index 00000000000000..499bc0fd2abb19 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts @@ -0,0 +1,14 @@ +import { IContextualMenuItem } from '../../ContextualMenu'; +import { IContextualMenuItemWrapperProps } from './IContextualMenuItemWrapper'; + +export interface IContextualMenuButtonProps extends IContextualMenuItemWrapperProps { + /** + * Callback to get the subMenu ID for an IContextualMenuItem + */ + getSubMenuId?: (item: IContextualMenuItem) => string | undefined; + + /** + * Key of the currently expanded subMenu + */ + expandedMenuItemKey?: string; +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.test.tsx similarity index 90% rename from packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.test.tsx rename to packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.test.tsx index 9e8ae71f134aa4..93d4bbe181307d 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.test.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as renderer from 'react-test-renderer'; -import { IContextualMenuItem } from './ContextualMenu.types'; -import { IMenuItemClassNames } from './ContextualMenu.classNames'; +import { IContextualMenuItem } from '../ContextualMenu.types'; +import { IMenuItemClassNames } from '../ContextualMenu.classNames'; import { ContextualMenuSplitButton } from './ContextualMenuSplitButton'; describe('ContextualMenuSplitButton', () => { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx similarity index 97% rename from packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx rename to packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx index 043d8162f78931..7d02387239dd46 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx @@ -5,15 +5,14 @@ import { buttonProperties, getNativeProps, KeyCodes -} from '../../Utilities'; -import { IContextualMenuItem } from '../../ContextualMenu'; +} from '../../../Utilities'; +import { IContextualMenuItem, ContextualMenuItem } from '../../ContextualMenu'; import { IMenuItemClassNames, getSplitButtonVerticalDividerClassNames -} from './ContextualMenu.classNames'; -import { ContextualMenuItem } from './ContextualMenuItem'; +} from '../ContextualMenu.classNames'; import { KeytipData } from '../../KeytipData'; -import { getIsChecked, isItemDisabled, hasSubmenu } from '../../utilities/contextualMenu/index'; +import { getIsChecked, isItemDisabled, hasSubmenu } from '../../../utilities/contextualMenu/index'; import { VerticalDivider } from '../../Divider'; import { IContextualMenuSplitButtonProps } from './ContextualMenuSplitButton.types'; diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts new file mode 100644 index 00000000000000..7749a4ecd48c1b --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts @@ -0,0 +1,8 @@ +import { IContextualMenuItemWrapperProps } from './IContextualMenuItemWrapper'; + +export interface IContextualMenuSplitButtonProps extends IContextualMenuItemWrapperProps { + /** + * Callback for touch/pointer events on the split button. + */ + onTap?: (ev: React.TouchEvent | PointerEvent) => void; +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/IContextualMenuItemWrapper.ts similarity index 81% rename from packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts rename to packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/IContextualMenuItemWrapper.ts index dfa863cde924bc..04f6664b84a8f1 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuSplitButton.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/IContextualMenuItemWrapper.ts @@ -1,17 +1,32 @@ +import { IContextualMenuItem, IContextualMenuItemProps } from '../../ContextualMenu'; +import { IMenuItemClassNames } from '../ContextualMenu.classNames'; -import { IContextualMenuItem, IContextualMenuRenderItem } from '../../ContextualMenu'; -import { IMenuItemClassNames } from './ContextualMenu.classNames'; -import { IContextualMenuItemProps } from './ContextualMenuItem.types'; +export interface IContextualMenuItemWrapper { + /** + * Function to open this item's subMenu, if present. + */ + openSubMenu: () => void; + + /** + * Function to close this item's subMenu, if present. + */ + dismissSubMenu: () => void; -export interface IContextualMenuSplitButtonProps extends React.Props { /** - * Optional callback to access the ContextualmenuSplitButton interface. Use this instead of ref for accessing + * Dismiss the menu this item belongs to. + */ + dismissMenu: (dismissAll?: boolean) => void; +} + +export interface IContextualMenuItemWrapperProps extends React.Props { + /** + * Optional callback to access the IContextualMenuRenderItem interface. Use this instead of ref for accessing * the public methods and properties of the component. */ - componentRef?: (component: IContextualMenuRenderItem | null) => void; + componentRef?: (component: IContextualMenuItemWrapper | null) => void; /** - * The item that is used to render the split button. + * The IContextualMenuItem that is used to render the item in the menu. */ item: IContextualMenuItem; @@ -106,9 +121,4 @@ export interface IContextualMenuSplitButtonProps extends React.Props void; - - /** - * Callback for touch/pointer events on the split button. - */ - onTap?: (ev: React.TouchEvent | PointerEvent) => void; } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuAnchor.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuAnchor.test.tsx.snap new file mode 100644 index 00000000000000..79afd17750ee41 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuAnchor.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ContextualMenuButton creates a normal button renders the contextual menu split button correctly 1`] = ` +
+ + +`; diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuButton.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuButton.test.tsx.snap new file mode 100644 index 00000000000000..6cc1f76a5a875e --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuButton.test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ContextualMenuButton creates a normal button renders the contextual menu split button correctly 1`] = ` + +`; diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuSplitButton.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuSplitButton.test.tsx.snap similarity index 100% rename from packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuSplitButton.test.tsx.snap rename to packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/__snapshots__/ContextualMenuSplitButton.test.tsx.snap diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts new file mode 100644 index 00000000000000..e36d163e24167c --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts @@ -0,0 +1,7 @@ +export * from './ContextualMenuAnchor'; +export * from './ContextualMenuAnchor.types'; +export * from './ContextualMenuButton'; +export * from './ContextualMenuButton.types'; +export * from './ContextualMenuSplitButton'; +export * from './ContextualMenuSplitButton.types'; +export * from './IContextualMenuItemWrapper'; \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap index f641b8f594868d..11c4226f3d2f8f 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap @@ -4,7 +4,7 @@ exports[`ContextMenuItemChildren when a checkmark icon renders the component wit ShallowWrapper { "length": 1, Symbol(enzyme.__root__): [Circular], - Symbol(enzyme.__unrendered__): Date: Fri, 4 May 2018 12:54:28 -0700 Subject: [PATCH 6/8] Move the open/dismiss functions back to ContextualMenuItem - Rename the prop to componentRef on IContextualMenuItem - Add tests for split button and anchor - Pass a prop function which returns the element to use in openSubMenu to ContextualMenuItem --- .../ContextualMenu/ContextualMenu.test.tsx | 189 ++++++++++++++---- .../ContextualMenu/ContextualMenu.tsx | 3 - .../ContextualMenu/ContextualMenu.types.ts | 7 +- .../ContextualMenu/ContextualMenuItem.tsx | 57 ++++-- .../ContextualMenuItem.types.ts | 45 +++++ .../ContextualMenuAnchor.tsx | 48 ++--- .../ContextualMenuAnchor.types.ts | 13 +- .../ContextualMenuButton.tsx | 56 +++--- .../ContextualMenuButton.types.ts | 13 +- ....ts => ContextualMenuItemWrapper.types.ts} | 43 +--- .../ContextualMenuSplitButton.tsx | 45 ++--- .../ContextualMenuSplitButton.types.ts | 9 +- .../ContextualMenuItemWrapper/index.ts | 2 +- .../ContextualMenuItem.test.tsx.snap | 8 +- 14 files changed, 347 insertions(+), 191 deletions(-) rename packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/{IContextualMenuItemWrapper.ts => ContextualMenuItemWrapper.types.ts} (69%) diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx index ab5642ed213899..3fa6126bc0ed12 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx @@ -9,7 +9,7 @@ import { FocusZoneDirection } from '../../FocusZone'; import { ContextualMenu, canAnyMenuItemsCheck } from './ContextualMenu'; import { IContextualMenuItem, ContextualMenuItemType } from './ContextualMenu.types'; -import { IContextualMenuItemWrapper } from './ContextualMenuItemWrapper'; +import { IContextualMenuRenderItem } from './ContextualMenuItem.types'; import { LayerBase as Layer } from '../Layer/Layer.base'; describe('ContextualMenu', () => { @@ -863,59 +863,160 @@ describe('ContextualMenu', () => { }); describe('IContextualMenuRenderItem function tests', () => { - const contextualItem = createRef(); + const contextualItem = createRef(); let menuDismissed: boolean; const onDismiss = (ev?: any, dismissAll?: boolean) => { menuDismissed = true; }; - beforeEach(() => { - menuDismissed = false; - const menu: IContextualMenuItem[] = [ - { - name: 'Test1', - key: 'Test1', - componentRef: contextualItem, - subMenuProps: { - items: [ - { - name: 'Test2', - key: 'Test2', - className: 'SubMenuClass' - }, - { - name: 'Test3', - key: 'Test3', - className: 'SubMenuClass' - } - ], + describe('for a button element', () => { + beforeEach(() => { + menuDismissed = false; + const menu: IContextualMenuItem[] = [ + { + name: 'Test1', + key: 'Test1', + componentRef: contextualItem, + subMenuProps: { + items: [ + { + name: 'Test2', + key: 'Test2', + className: 'SubMenuClass' + }, + { + name: 'Test3', + key: 'Test3', + className: 'SubMenuClass' + } + ], + } } - } - ]; + ]; + ReactTestUtils.renderIntoDocument( + + ); + }); - ReactTestUtils.renderIntoDocument( - - ); - }); + it('openSubMenu will open the item`s submenu if present', () => { + contextualItem.value!.openSubMenu(); + expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + }); - it('openSubMenu will open the item`s submenu if present', () => { - contextualItem.value!.openSubMenu(); - expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + it('dismissSubMenu will close the item`s submenu if present', () => { + contextualItem.value!.openSubMenu(); + expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + contextualItem.value!.dismissSubMenu(); + expect(document.querySelector('.SubMenuClass')).toEqual(null); + }); + + it('dismissMenu will close the item`s menu', () => { + contextualItem.value!.dismissMenu(); + expect(menuDismissed).toEqual(true); + }); }); - it('dismissSubMenu will close the item`s submenu if present', () => { - // Open the submenu with a click - const menuItem = document.querySelector('button.ms-ContextualMenu-link') as HTMLButtonElement; - ReactTestUtils.Simulate.click(menuItem); - expect(document.querySelector('.SubMenuClass')).not.toEqual(null); - contextualItem.value!.dismissSubMenu(); - expect(document.querySelector('.SubMenuClass')).toEqual(null); + describe('for a split button element', () => { + beforeEach(() => { + menuDismissed = false; + const menu: IContextualMenuItem[] = [ + { + name: 'Test1', + key: 'Test1', + componentRef: contextualItem, + split: true, + subMenuProps: { + items: [ + { + name: 'Test2', + key: 'Test2', + className: 'SubMenuClass' + }, + { + name: 'Test3', + key: 'Test3', + className: 'SubMenuClass' + } + ], + } + } + ]; + ReactTestUtils.renderIntoDocument( + + ); + }); + + it('openSubMenu will open the item`s submenu if present', () => { + contextualItem.value!.openSubMenu(); + expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + }); + + it('dismissSubMenu will close the item`s submenu if present', () => { + contextualItem.value!.openSubMenu(); + expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + contextualItem.value!.dismissSubMenu(); + expect(document.querySelector('.SubMenuClass')).toEqual(null); + }); + + it('dismissMenu will close the item`s menu', () => { + contextualItem.value!.dismissMenu(); + expect(menuDismissed).toEqual(true); + }); }); - it('dismissMenu will close the item`s menu', () => { - contextualItem.value!.dismissMenu(); - expect(menuDismissed).toEqual(true); + describe('for an anchor element', () => { + beforeEach(() => { + menuDismissed = false; + const menu: IContextualMenuItem[] = [ + { + name: 'Test1', + key: 'Test1', + componentRef: contextualItem, + href: '#test', + subMenuProps: { + items: [ + { + name: 'Test2', + key: 'Test2', + className: 'SubMenuClass' + }, + { + name: 'Test3', + key: 'Test3', + className: 'SubMenuClass' + } + ], + } + } + ]; + ReactTestUtils.renderIntoDocument( + + ); + }); + + it('openSubMenu will open the item`s submenu if present', () => { + contextualItem.value!.openSubMenu(); + expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + }); + + it('dismissSubMenu will close the item`s submenu if present', () => { + contextualItem.value!.openSubMenu(); + expect(document.querySelector('.SubMenuClass')).not.toEqual(null); + contextualItem.value!.dismissSubMenu(); + expect(document.querySelector('.SubMenuClass')).toEqual(null); + }); + + it('dismissMenu will close the item`s menu', () => { + contextualItem.value!.dismissMenu(); + expect(menuDismissed).toEqual(true); + }); }); }); }); diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index 1f2ad071f3cd75..47736657d61ccd 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -506,7 +506,6 @@ export class ContextualMenu extends BaseComponent ); } @@ -546,7 +545,6 @@ export class ContextualMenu extends BaseComponent ); } @@ -583,7 +581,6 @@ export class ContextualMenu extends BaseComponent ); diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts index 0fcd28fa404d67..0041d785c5c811 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.types.ts @@ -15,8 +15,7 @@ import { IWithResponsiveModeState } from '../../utilities/decorators/withRespons import { IContextualMenuClassNames, IMenuItemClassNames } from './ContextualMenu.classNames'; export { DirectionalHint } from '../../common/DirectionalHint'; import { IVerticalDividerClassNames } from '../Divider/VerticalDivider.types'; -import { IContextualMenuItemProps } from './ContextualMenuItem.types'; -import { IContextualMenuItemWrapper } from './ContextualMenuItemWrapper'; +import { IContextualMenuItemProps, IContextualMenuRenderItem } from './ContextualMenuItem.types'; import { IKeytipProps } from '../../Keytip'; export enum ContextualMenuItemType { @@ -257,9 +256,9 @@ export interface IContextualMenuProps extends React.Props, IWith export interface IContextualMenuItem { /** - * Optional callback to access the IContextualMenuItemWrapper interface. This will get passed down to the item that renders the ContextualMenuItem. + * Optional callback to access the IContextualMenuRenderItem interface. This will get passed down to ContextualMenuItem. */ - componentRef?: (component: IContextualMenuItemWrapper | null) => void; + componentRef?: (component: IContextualMenuRenderItem | null) => void; /** * Unique id to identify the item diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx index 95f50568e7dd6a..5ab51166bb88e1 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { hasSubmenu, getIsChecked } from '../../utilities/contextualMenu/index'; -import { getRTL } from '../../Utilities'; +import { BaseComponent, getRTL } from '../../Utilities'; import { Icon } from '../../Icon'; import { IContextualMenuItemProps } from './ContextualMenuItem.types'; @@ -67,19 +67,46 @@ const renderSubMenuIcon = ({ item, classNames }: IContextualMenuItemProps) => { return null; }; -export const ContextualMenuItem: React.StatelessComponent = (props) => { - const { item, classNames } = props; +export class ContextualMenuItem extends BaseComponent { - return ( -
+ { renderCheckMarkIcon(this.props) } + { renderItemIcon(this.props) } + { renderItemName(this.props) } + { renderSubMenuIcon(this.props) } +
+ ); + } + + public openSubMenu = (): void => { + const { item, openSubMenu, getContainerElement } = this.props; + if (getContainerElement) { + const containerElement = getContainerElement(); + if (hasSubmenu(item) && openSubMenu && containerElement) { + openSubMenu(item, containerElement); } - > - { renderCheckMarkIcon(props) } - { renderItemIcon(props) } - { renderItemName(props) } - { renderSubMenuIcon(props) } -
- ); -}; \ No newline at end of file + } + } + + public dismissSubMenu = (): void => { + const { item, dismissSubMenu } = this.props; + if (hasSubmenu(item) && dismissSubMenu) { + dismissSubMenu(); + } + } + + public dismissMenu = (dismissAll?: boolean): void => { + const { dismissMenu } = this.props; + if (dismissMenu) { + dismissMenu(dismissAll); + } + } +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts index d5b7c5bddb317d..a7a535e4bc51a2 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts @@ -1,7 +1,30 @@ import { IContextualMenuItem } from './ContextualMenu.types'; import { IMenuItemClassNames } from './ContextualMenu.classNames'; +export interface IContextualMenuRenderItem { + /** + * Function to open this item's subMenu, if present. + */ + openSubMenu: () => void; + + /** + * Function to close this item's subMenu, if present. + */ + dismissSubMenu: () => void; + + /** + * Dismiss the menu this item belongs to. + */ + dismissMenu: (dismissAll?: boolean) => void; +} + export interface IContextualMenuItemProps extends React.HTMLAttributes { + /** + * Optional callback to access the IContextualMenuRenderItem interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: IContextualMenuRenderItem | null) => void; + /** * The item to display */ @@ -26,4 +49,26 @@ export interface IContextualMenuItemProps extends React.HTMLAttributes) => void); + + /** + * This prop will get set by ContextualMenu and can be called to open this item's subMenu, if present. + */ + openSubMenu?: (item: any, target: HTMLElement) => void; + + /** + * This prop will get set by ContextualMenu and can be called to close this item's subMenu, if present. + */ + dismissSubMenu?: () => void; + + /** + * This prop will get set by ContextualMenu and can be called to close the menu this item belongs to. + * If dismissAll is true, all menus will be closed. + */ + dismissMenu?: (dismissAll?: boolean) => void; + + /** + * This prop will get set by the wrapping component and will return the element that wraps this ContextualMenuItem. + * Used for openSubMenu. + */ + getContainerElement?: () => HTMLElement | undefined; } diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx index cf494b2518c408..13b4c14892175e 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx @@ -3,6 +3,7 @@ import { BaseComponent, anchorProperties, getNativeProps, + createRef } from '../../../Utilities'; import { IContextualMenuAnchorProps } from './ContextualMenuAnchor.types'; import { KeytipData } from '../../KeytipData'; @@ -10,7 +11,7 @@ import { isItemDisabled, hasSubmenu } from '../../../utilities/contextualMenu/in import { IContextualMenuItem, ContextualMenuItem } from '../../ContextualMenu'; export class ContextualMenuAnchor extends BaseComponent { - private _anchor: HTMLAnchorElement; + private _anchor = createRef(); public render() { const { @@ -23,7 +24,10 @@ export class ContextualMenuAnchor extends BaseComponent this._anchor = anchor } + ref={ this._anchor } href={ item.href } target={ item.target } rel={ anchorRel } @@ -66,11 +78,16 @@ export class ContextualMenuAnchor extends BaseComponent ) } @@ -78,27 +95,6 @@ export class ContextualMenuAnchor extends BaseComponent); } - public openSubMenu = (): void => { - const { item, openSubMenu } = this.props; - if (hasSubmenu(item) && openSubMenu && this._anchor) { - openSubMenu(item, this._anchor); - } - } - - public dismissSubMenu = (): void => { - const { item, dismissSubMenu } = this.props; - if (hasSubmenu(item) && dismissSubMenu) { - dismissSubMenu(); - } - } - - public dismissMenu = (dismissAll?: boolean): void => { - const { dismissMenu } = this.props; - if (dismissMenu) { - dismissMenu(dismissAll); - } - } - private _onItemMouseEnter = (ev: React.MouseEvent): void => { const { item, onItemMouseEnter } = this.props; if (onItemMouseEnter) { @@ -133,4 +129,8 @@ export class ContextualMenuAnchor extends BaseComponent { + return this._anchor.current ? this._anchor.current : undefined; + } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts index ead336dad99ffa..9109c027b5172c 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts @@ -1,14 +1,21 @@ import { IContextualMenuItem } from '../../ContextualMenu'; -import { IContextualMenuItemWrapperProps } from './IContextualMenuItemWrapper'; +import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types'; +import { ContextualMenuAnchor } from './ContextualMenuAnchor'; export interface IContextualMenuAnchorProps extends IContextualMenuItemWrapperProps { /** - * Callback to get the subMenu ID for an IContextualMenuItem + * Optional callback to access the ContextualMenuAnchor interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: ContextualMenuAnchor | null) => void; + + /** + * Callback to get the subMenu ID for an IContextualMenuItem. */ getSubMenuId?: (item: IContextualMenuItem) => string | undefined; /** - * Key of the currently expanded subMenu + * Key of the currently expanded subMenu. */ expandedMenuItemKey?: string; } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx index 4b86d083691fb9..542f4eb7b23c62 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx @@ -3,6 +3,7 @@ import { BaseComponent, buttonProperties, getNativeProps, + createRef } from '../../../Utilities'; import { IContextualMenuButtonProps } from './ContextualMenuButton.types'; import { KeytipData } from '../../KeytipData'; @@ -10,7 +11,7 @@ import { getIsChecked, isItemDisabled, hasSubmenu } from '../../../utilities/con import { IContextualMenuItem, ContextualMenuItem } from '../../ContextualMenu'; export class ContextualMenuButton extends BaseComponent { - private _btn: HTMLButtonElement; + private _btn = createRef(); public render() { const { @@ -24,17 +25,12 @@ export class ContextualMenuButton extends BaseComponent { (keytipAttributes: any): JSX.Element => ( ) } @@ -101,27 +110,6 @@ export class ContextualMenuButton extends BaseComponent { - const { item, openSubMenu } = this.props; - if (hasSubmenu(item) && openSubMenu && this._btn) { - openSubMenu(item, this._btn); - } - } - - public dismissSubMenu = (): void => { - const { item, dismissSubMenu } = this.props; - if (hasSubmenu(item) && dismissSubMenu) { - dismissSubMenu(); - } - } - - public dismissMenu = (dismissAll?: boolean): void => { - const { dismissMenu } = this.props; - if (dismissMenu) { - dismissMenu(dismissAll); - } - } - private _onItemMouseEnter = (ev: React.MouseEvent): void => { const { item, onItemMouseEnter } = this.props; if (onItemMouseEnter) { @@ -163,4 +151,8 @@ export class ContextualMenuButton extends BaseComponent { + return this._btn.current ? this._btn.current : undefined; + } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts index 499bc0fd2abb19..b06ae35ffb792a 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts @@ -1,14 +1,21 @@ import { IContextualMenuItem } from '../../ContextualMenu'; -import { IContextualMenuItemWrapperProps } from './IContextualMenuItemWrapper'; +import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types'; +import { ContextualMenuButton } from './ContextualMenuButton'; export interface IContextualMenuButtonProps extends IContextualMenuItemWrapperProps { /** - * Callback to get the subMenu ID for an IContextualMenuItem + * Optional callback to access the ContextualMenuSplitButton interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: ContextualMenuButton | null) => void; + + /** + * Callback to get the subMenu ID for an IContextualMenuItem. */ getSubMenuId?: (item: IContextualMenuItem) => string | undefined; /** - * Key of the currently expanded subMenu + * Key of the currently expanded subMenu. */ expandedMenuItemKey?: string; } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/IContextualMenuItemWrapper.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts similarity index 69% rename from packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/IContextualMenuItemWrapper.ts rename to packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts index 04f6664b84a8f1..95f1058b937c3b 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/IContextualMenuItemWrapper.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts @@ -1,30 +1,7 @@ import { IContextualMenuItem, IContextualMenuItemProps } from '../../ContextualMenu'; import { IMenuItemClassNames } from '../ContextualMenu.classNames'; -export interface IContextualMenuItemWrapper { - /** - * Function to open this item's subMenu, if present. - */ - openSubMenu: () => void; - - /** - * Function to close this item's subMenu, if present. - */ - dismissSubMenu: () => void; - - /** - * Dismiss the menu this item belongs to. - */ - dismissMenu: (dismissAll?: boolean) => void; -} - export interface IContextualMenuItemWrapperProps extends React.Props { - /** - * Optional callback to access the IContextualMenuRenderItem interface. Use this instead of ref for accessing - * the public methods and properties of the component. - */ - componentRef?: (component: IContextualMenuItemWrapper | null) => void; - /** * The IContextualMenuItem that is used to render the item in the menu. */ @@ -36,12 +13,12 @@ export interface IContextualMenuItemWrapperProps extends React.Props | React.StatelessComponent; /** - * Callback for when the user's mouse enters the split button. + * Callback for when the user's mouse enters the wrapper. */ onItemMouseEnter?: (item: IContextualMenuItem, ev: React.MouseEvent, target: HTMLElement) => boolean | void; /** - * Callback for when the user's mouse leaves the split button. + * Callback for when the user's mouse leaves the wrapper. */ onItemMouseLeave?: (item: IContextualMenuItem, ev: React.MouseEvent) => void; /** - * Callback for when the user's mouse moves in the split button. + * Callback for when the user's mouse moves in the wrapper. */ onItemMouseMove?: (item: IContextualMenuItem, ev: React.MouseEvent, target: HTMLElement) => void; /** - * Callback for the mousedown event on the icon button in the split menu. + * Callback for the mousedown event on the icon button in the wrapper. */ onItemMouseDown?: (item: IContextualMenuItem, ev: React.MouseEvent) => void; @@ -92,7 +69,7 @@ export interface IContextualMenuItemWrapperProps extends React.Props | React.KeyboardEvent) => void; /** - * Callback for when the click event on the icon button from the split button. + * Callback for when the click event on the icon button from the wrapper. */ onItemClick?: (item: IContextualMenuItem, ev: React.MouseEvent | React.KeyboardEvent) => void; @@ -102,7 +79,7 @@ export interface IContextualMenuItemWrapperProps extends React.Props | React.KeyboardEvent, target: HTMLElement) => void; /** - * Callback for keyboard events on the split button. + * Callback for keyboard events on the wrapper. */ onItemKeyDown?: (item: IContextualMenuItem, ev: React.KeyboardEvent) => void; diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx index 7d02387239dd46..a81a0be9b32165 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx @@ -12,7 +12,7 @@ import { getSplitButtonVerticalDividerClassNames } from '../ContextualMenu.classNames'; import { KeytipData } from '../../KeytipData'; -import { getIsChecked, isItemDisabled, hasSubmenu } from '../../../utilities/contextualMenu/index'; +import { getIsChecked, isItemDisabled } from '../../../utilities/contextualMenu/index'; import { VerticalDivider } from '../../Divider'; import { IContextualMenuSplitButtonProps } from './ContextualMenuSplitButton.types'; @@ -84,27 +84,6 @@ export class ContextualMenuSplitButton extends BaseComponent { - const { item, openSubMenu } = this.props; - if (hasSubmenu(item) && openSubMenu && this._splitButton) { - openSubMenu(item, this._splitButton); - } - } - - public dismissSubMenu = (): void => { - const { item, dismissSubMenu } = this.props; - if (hasSubmenu(item) && dismissSubMenu) { - dismissSubMenu(); - } - } - - public dismissMenu = (dismissAll?: boolean): void => { - const { dismissMenu } = this.props; - if (dismissMenu) { - dismissMenu(dismissAll); - } - } - private _renderSplitPrimaryButton(item: IContextualMenuItem, classNames: IMenuItemClassNames, index: number, hasCheckmarks: boolean, hasIcons: boolean) { const isChecked: boolean | null | undefined = getIsChecked(item); const canCheck: boolean = isChecked !== null; @@ -151,7 +130,10 @@ export class ContextualMenuSplitButton extends BaseComponent - + ); } @@ -292,4 +285,8 @@ export class ContextualMenuSplitButton extends BaseComponent { + return this._splitButton; + } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts index 7749a4ecd48c1b..bdf4cb9262bf2a 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts @@ -1,6 +1,13 @@ -import { IContextualMenuItemWrapperProps } from './IContextualMenuItemWrapper'; +import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types'; +import { ContextualMenuSplitButton } from './ContextualMenuSplitButton'; export interface IContextualMenuSplitButtonProps extends IContextualMenuItemWrapperProps { + /** + * Optional callback to access the ContextualMenuSplitButton interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: ContextualMenuSplitButton | null) => void; + /** * Callback for touch/pointer events on the split button. */ diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts index e36d163e24167c..f4fc0e031142d1 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts @@ -4,4 +4,4 @@ export * from './ContextualMenuButton'; export * from './ContextualMenuButton.types'; export * from './ContextualMenuSplitButton'; export * from './ContextualMenuSplitButton.types'; -export * from './IContextualMenuItemWrapper'; \ No newline at end of file +export * from './ContextualMenuItemWrapper.types'; \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap b/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap index 11c4226f3d2f8f..f641b8f594868d 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/__snapshots__/ContextualMenuItem.test.tsx.snap @@ -4,7 +4,7 @@ exports[`ContextMenuItemChildren when a checkmark icon renders the component wit ShallowWrapper { "length": 1, Symbol(enzyme.__root__): [Circular], - Symbol(enzyme.__unrendered__): Date: Mon, 7 May 2018 11:04:49 -0700 Subject: [PATCH 7/8] Make base class ContextualMenuItemWrapper - Move shared functions into protected members - Rename getContainerElement to getSubmenuTarget --- .../ContextualMenu/ContextualMenuItem.tsx | 6 +- .../ContextualMenuItem.types.ts | 2 +- .../ContextualMenuAnchor.tsx | 46 ++-------------- .../ContextualMenuAnchor.types.ts | 21 ------- .../ContextualMenuButton.tsx | 53 ++---------------- .../ContextualMenuButton.types.ts | 21 ------- .../ContextualMenuItemWrapper.tsx | 55 +++++++++++++++++++ .../ContextualMenuItemWrapper.types.ts | 22 ++++++++ .../ContextualMenuSplitButton.tsx | 37 ++++++------- .../ContextualMenuSplitButton.types.ts | 15 ----- .../ContextualMenuItemWrapper/index.ts | 4 +- 11 files changed, 110 insertions(+), 172 deletions(-) delete mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts delete mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.tsx delete mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx index 5ab51166bb88e1..13d90cc6e6a1ce 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx @@ -87,9 +87,9 @@ export class ContextualMenuItem extends BaseComponent { - const { item, openSubMenu, getContainerElement } = this.props; - if (getContainerElement) { - const containerElement = getContainerElement(); + const { item, openSubMenu, getSubmenuTarget } = this.props; + if (getSubmenuTarget) { + const containerElement = getSubmenuTarget(); if (hasSubmenu(item) && openSubMenu && containerElement) { openSubMenu(item, containerElement); } diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts index a7a535e4bc51a2..f9489052a43c3a 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts @@ -70,5 +70,5 @@ export interface IContextualMenuItemProps extends React.HTMLAttributes HTMLElement | undefined; + getSubmenuTarget?: () => HTMLElement | undefined; } diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx index 13b4c14892175e..f1ab2089c88156 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; import { - BaseComponent, anchorProperties, getNativeProps, createRef } from '../../../Utilities'; -import { IContextualMenuAnchorProps } from './ContextualMenuAnchor.types'; +import { ContextualMenuItemWrapper } from './ContextualMenuItemWrapper'; import { KeytipData } from '../../KeytipData'; import { isItemDisabled, hasSubmenu } from '../../../utilities/contextualMenu/index'; -import { IContextualMenuItem, ContextualMenuItem } from '../../ContextualMenu'; +import { ContextualMenuItem } from '../../ContextualMenu'; -export class ContextualMenuAnchor extends BaseComponent { +export class ContextualMenuAnchor extends ContextualMenuItemWrapper { private _anchor = createRef(); public render() { @@ -87,7 +86,7 @@ export class ContextualMenuAnchor extends BaseComponent ) } @@ -95,42 +94,7 @@ export class ContextualMenuAnchor extends BaseComponent); } - private _onItemMouseEnter = (ev: React.MouseEvent): void => { - const { item, onItemMouseEnter } = this.props; - if (onItemMouseEnter) { - onItemMouseEnter(item, ev, ev.currentTarget as HTMLElement); - } - } - - private _onItemClick = (ev: React.MouseEvent): void => { - const { item, onItemClick } = this.props; - if (onItemClick) { - onItemClick(item, ev); - } - } - - private _onItemMouseLeave = (ev: React.MouseEvent): void => { - const { item, onItemMouseLeave } = this.props; - if (onItemMouseLeave) { - onItemMouseLeave(item, ev); - } - } - - private _onItemKeyDown = (ev: React.KeyboardEvent): void => { - const { item, onItemKeyDown } = this.props; - if (onItemKeyDown) { - onItemKeyDown(item, ev); - } - } - - private _getSubMenuId = (item: IContextualMenuItem): string | undefined => { - const { getSubMenuId } = this.props; - if (getSubMenuId) { - return getSubMenuId(item); - } - } - - private _getContainerElement = (): HTMLElement | undefined => { + protected _getSubmenuTarget = (): HTMLElement | undefined => { return this._anchor.current ? this._anchor.current : undefined; } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts deleted file mode 100644 index 9109c027b5172c..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuAnchor.types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IContextualMenuItem } from '../../ContextualMenu'; -import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types'; -import { ContextualMenuAnchor } from './ContextualMenuAnchor'; - -export interface IContextualMenuAnchorProps extends IContextualMenuItemWrapperProps { - /** - * Optional callback to access the ContextualMenuAnchor interface. Use this instead of ref for accessing - * the public methods and properties of the component. - */ - componentRef?: (component: ContextualMenuAnchor | null) => void; - - /** - * Callback to get the subMenu ID for an IContextualMenuItem. - */ - getSubMenuId?: (item: IContextualMenuItem) => string | undefined; - - /** - * Key of the currently expanded subMenu. - */ - expandedMenuItemKey?: string; -} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx index 542f4eb7b23c62..dd01a524cdffe9 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; import { - BaseComponent, buttonProperties, getNativeProps, createRef } from '../../../Utilities'; -import { IContextualMenuButtonProps } from './ContextualMenuButton.types'; +import { ContextualMenuItemWrapper } from './ContextualMenuItemWrapper'; import { KeytipData } from '../../KeytipData'; import { getIsChecked, isItemDisabled, hasSubmenu } from '../../../utilities/contextualMenu/index'; -import { IContextualMenuItem, ContextualMenuItem } from '../../ContextualMenu'; +import { ContextualMenuItem } from '../../ContextualMenu'; -export class ContextualMenuButton extends BaseComponent { +export class ContextualMenuButton extends ContextualMenuItemWrapper { private _btn = createRef(); public render() { @@ -102,7 +101,7 @@ export class ContextualMenuButton extends BaseComponent ) } @@ -110,49 +109,7 @@ export class ContextualMenuButton extends BaseComponent): void => { - const { item, onItemMouseEnter } = this.props; - if (onItemMouseEnter) { - onItemMouseEnter(item, ev, ev.currentTarget as HTMLElement); - } - } - - private _onItemMouseMove = (ev: React.MouseEvent): void => { - const { item, onItemMouseMove } = this.props; - if (onItemMouseMove) { - onItemMouseMove(item, ev, ev.currentTarget as HTMLElement); - } - } - - private _onItemClick = (ev: React.MouseEvent): void => { - const { item, onItemClickBase } = this.props; - if (onItemClickBase) { - onItemClickBase(item, ev, ev.currentTarget as HTMLElement); - } - } - - private _onItemMouseLeave = (ev: React.MouseEvent): void => { - const { item, onItemMouseLeave } = this.props; - if (onItemMouseLeave) { - onItemMouseLeave(item, ev); - } - } - - private _onItemKeyDown = (ev: React.KeyboardEvent): void => { - const { item, onItemKeyDown } = this.props; - if (onItemKeyDown) { - onItemKeyDown(item, ev); - } - } - - private _getSubMenuId = (item: IContextualMenuItem): string | undefined => { - const { getSubMenuId } = this.props; - if (getSubMenuId) { - return getSubMenuId(item); - } - } - - private _getContainerElement = (): HTMLElement | undefined => { + protected _getSubmenuTarget = (): HTMLElement | undefined => { return this._btn.current ? this._btn.current : undefined; } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts deleted file mode 100644 index b06ae35ffb792a..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuButton.types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IContextualMenuItem } from '../../ContextualMenu'; -import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types'; -import { ContextualMenuButton } from './ContextualMenuButton'; - -export interface IContextualMenuButtonProps extends IContextualMenuItemWrapperProps { - /** - * Optional callback to access the ContextualMenuSplitButton interface. Use this instead of ref for accessing - * the public methods and properties of the component. - */ - componentRef?: (component: ContextualMenuButton | null) => void; - - /** - * Callback to get the subMenu ID for an IContextualMenuItem. - */ - getSubMenuId?: (item: IContextualMenuItem) => string | undefined; - - /** - * Key of the currently expanded subMenu. - */ - expandedMenuItemKey?: string; -} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.tsx new file mode 100644 index 00000000000000..687a9fc0b4b8e8 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { + BaseComponent +} from '../../../Utilities'; +import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types'; +import { IContextualMenuItem } from '../../ContextualMenu'; + +export class ContextualMenuItemWrapper extends BaseComponent { + + protected _onItemMouseEnter = (ev: React.MouseEvent): void => { + const { item, onItemMouseEnter } = this.props; + if (onItemMouseEnter) { + onItemMouseEnter(item, ev, ev.currentTarget as HTMLElement); + } + } + + protected _onItemClick = (ev: React.MouseEvent): void => { + const { item, onItemClickBase } = this.props; + if (onItemClickBase) { + onItemClickBase(item, ev, ev.currentTarget as HTMLElement); + } + } + + protected _onItemMouseLeave = (ev: React.MouseEvent): void => { + const { item, onItemMouseLeave } = this.props; + if (onItemMouseLeave) { + onItemMouseLeave(item, ev); + } + } + + protected _onItemKeyDown = (ev: React.KeyboardEvent): void => { + const { item, onItemKeyDown } = this.props; + if (onItemKeyDown) { + onItemKeyDown(item, ev); + } + } + + protected _onItemMouseMove = (ev: React.MouseEvent): void => { + const { item, onItemMouseMove } = this.props; + if (onItemMouseMove) { + onItemMouseMove(item, ev, ev.currentTarget as HTMLElement); + } + } + + protected _getSubMenuId = (item: IContextualMenuItem): string | undefined => { + const { getSubMenuId } = this.props; + if (getSubMenuId) { + return getSubMenuId(item); + } + } + + protected _getSubmenuTarget = (): HTMLElement | undefined => { + return undefined; + } +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts index 95f1058b937c3b..d3b1949f0d07ba 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts @@ -1,7 +1,14 @@ import { IContextualMenuItem, IContextualMenuItemProps } from '../../ContextualMenu'; import { IMenuItemClassNames } from '../ContextualMenu.classNames'; +import { ContextualMenuItemWrapper } from './ContextualMenuItemWrapper'; export interface IContextualMenuItemWrapperProps extends React.Props { + /** + * Optional callback to access the ContextualMenuSplitButton interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: ContextualMenuItemWrapper | null) => void; + /** * The IContextualMenuItem that is used to render the item in the menu. */ @@ -83,6 +90,21 @@ export interface IContextualMenuItemWrapperProps extends React.Props) => void; + /** + * Callback to get the subMenu ID for an IContextualMenuItem. + */ + getSubMenuId?: (item: IContextualMenuItem) => string | undefined; + + /** + * Key of the currently expanded subMenu. + */ + expandedMenuItemKey?: string; + + /** + * Callback for touch/pointer events on the split button. + */ + onTap?: (ev: React.TouchEvent | PointerEvent) => void; + /** * This prop will get set by ContextualMenu and can be called to open this item's subMenu, if present. */ diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx index a81a0be9b32165..f6c3c9decff342 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { - BaseComponent, assign, buttonProperties, getNativeProps, @@ -14,13 +13,13 @@ import { import { KeytipData } from '../../KeytipData'; import { getIsChecked, isItemDisabled } from '../../../utilities/contextualMenu/index'; import { VerticalDivider } from '../../Divider'; -import { IContextualMenuSplitButtonProps } from './ContextualMenuSplitButton.types'; +import { ContextualMenuItemWrapper } from './ContextualMenuItemWrapper'; export interface IContextualMenuSplitButtonState { } const TouchIdleDelay = 500; /* ms */ -export class ContextualMenuSplitButton extends BaseComponent { +export class ContextualMenuSplitButton extends ContextualMenuItemWrapper { private _splitButton: HTMLDivElement; private _lastTouchTimeoutId: number | undefined; private _processingTouch: boolean; @@ -84,6 +83,21 @@ export class ContextualMenuSplitButton extends BaseComponent): void => { + const { item, onItemKeyDown } = this.props; + if (ev.which === KeyCodes.enter) { + this._executeItemClick(ev); + ev.preventDefault(); + ev.stopPropagation(); + } else if (onItemKeyDown) { + onItemKeyDown(item, ev); + } + } + + protected _getSubmenuTarget = (): HTMLElement | undefined => { + return this._splitButton; + } + private _renderSplitPrimaryButton(item: IContextualMenuItem, classNames: IMenuItemClassNames, index: number, hasCheckmarks: boolean, hasIcons: boolean) { const isChecked: boolean | null | undefined = getIsChecked(item); const canCheck: boolean = isChecked !== null; @@ -167,7 +181,7 @@ export class ContextualMenuSplitButton extends BaseComponent ); @@ -240,17 +254,6 @@ export class ContextualMenuSplitButton extends BaseComponent): void => { - const { item, onItemKeyDown } = this.props; - if (ev.which === KeyCodes.enter) { - this._executeItemClick(ev); - ev.preventDefault(); - ev.stopPropagation(); - } else if (onItemKeyDown) { - onItemKeyDown(item, ev); - } - } - private _onTouchStart = (ev: React.TouchEvent): void => { if (this._splitButton && !('onpointerdown' in this._splitButton)) { this._handleTouchAndPointerEvent(ev); @@ -285,8 +288,4 @@ export class ContextualMenuSplitButton extends BaseComponent { - return this._splitButton; - } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts deleted file mode 100644 index bdf4cb9262bf2a..00000000000000 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuSplitButton.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types'; -import { ContextualMenuSplitButton } from './ContextualMenuSplitButton'; - -export interface IContextualMenuSplitButtonProps extends IContextualMenuItemWrapperProps { - /** - * Optional callback to access the ContextualMenuSplitButton interface. Use this instead of ref for accessing - * the public methods and properties of the component. - */ - componentRef?: (component: ContextualMenuSplitButton | null) => void; - - /** - * Callback for touch/pointer events on the split button. - */ - onTap?: (ev: React.TouchEvent | PointerEvent) => void; -} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts index f4fc0e031142d1..e110536ca48139 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/index.ts @@ -1,7 +1,5 @@ export * from './ContextualMenuAnchor'; -export * from './ContextualMenuAnchor.types'; export * from './ContextualMenuButton'; -export * from './ContextualMenuButton.types'; export * from './ContextualMenuSplitButton'; -export * from './ContextualMenuSplitButton.types'; +export * from './ContextualMenuItemWrapper'; export * from './ContextualMenuItemWrapper.types'; \ No newline at end of file From 782e08f4546b4d88a7bcbf45c5c58eb7fa33d223 Mon Sep 17 00:00:00 2001 From: Kelsey Young Date: Mon, 7 May 2018 12:18:00 -0700 Subject: [PATCH 8/8] Remove dismiss bind and pass through undefined at ContextualMenuItem --- .../src/components/ContextualMenu/ContextualMenu.tsx | 9 +++------ .../src/components/ContextualMenu/ContextualMenuItem.tsx | 8 ++++---- .../ContextualMenu/ContextualMenuItem.types.ts | 2 +- .../ContextualMenuItemWrapper.types.ts | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index 47736657d61ccd..010ffcf65223a8 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -482,7 +482,6 @@ export class ContextualMenu extends BaseComponent ); } @@ -519,7 +518,6 @@ export class ContextualMenu extends BaseComponent ); } @@ -558,7 +556,6 @@ export class ContextualMenu extends BaseComponent ); diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx index 13d90cc6e6a1ce..5de1b09a30b8f1 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.tsx @@ -89,9 +89,9 @@ export class ContextualMenuItem extends BaseComponent { const { item, openSubMenu, getSubmenuTarget } = this.props; if (getSubmenuTarget) { - const containerElement = getSubmenuTarget(); - if (hasSubmenu(item) && openSubMenu && containerElement) { - openSubMenu(item, containerElement); + const submenuTarget = getSubmenuTarget(); + if (hasSubmenu(item) && openSubMenu && submenuTarget) { + openSubMenu(item, submenuTarget); } } } @@ -106,7 +106,7 @@ export class ContextualMenuItem extends BaseComponent { const { dismissMenu } = this.props; if (dismissMenu) { - dismissMenu(dismissAll); + dismissMenu(undefined /* ev */, dismissAll); } } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts index f9489052a43c3a..7bbedea0c442e3 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItem.types.ts @@ -64,7 +64,7 @@ export interface IContextualMenuItemProps extends React.HTMLAttributes void; + dismissMenu?: (ev?: any, dismissAll?: boolean) => void; /** * This prop will get set by the wrapping component and will return the element that wraps this ContextualMenuItem. diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts index d3b1949f0d07ba..75d95c01acdad5 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuItemWrapper/ContextualMenuItemWrapper.types.ts @@ -119,5 +119,5 @@ export interface IContextualMenuItemWrapperProps extends React.Props void; + dismissMenu?: (ev?: any, dismissAll?: boolean) => void; } \ No newline at end of file