Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "office-ui-fabric-react",
"comment": "ContextualMenu: Allow anchor menu items to have sub menus. Added prop for sub menu hover delay.",
"type": "patch"
}
],
"packageName": "office-ui-fabric-react",
"email": "demarcey@microsoft.com"
}
Original file line number Diff line number Diff line change
Expand Up @@ -470,13 +470,17 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
}

private _renderAnchorMenuItem(item: IContextualMenuItem, classNames: IMenuItemClassNames, index: number, focusableElementIndex: number, totalItemCount: number, hasCheckmarks: boolean, hasIcons: boolean): React.ReactNode {
const { expandedMenuItemKey } = this.state;
const { contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem } = 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);

return (
<div>
<a
Expand All @@ -486,11 +490,17 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
rel={ anchorRel }
className={ classNames.root }
role='menuitem'
aria-owns={ item.key === expandedMenuItemKey ? subMenuId : null }
aria-haspopup={ itemHasSubmenu || null }
aria-expanded={ itemHasSubmenu ? item.key === expandedMenuItemKey : null }
aria-posinset={ focusableElementIndex + 1 }
aria-setsize={ totalItemCount }
aria-disabled={ this._isItemDisabled(item) }
style={ item.style }
onClick={ this._onAnchorClick.bind(this, item) }
onMouseEnter={ this._onItemMouseEnter.bind(this, item) }
onMouseLeave={ this._onMouseItemLeave.bind(this, item) }
onKeyDown={ itemHasSubmenu ? this._onItemKeyDown.bind(this, item) : null }
>
<ChildrenRenderer
item={ item }
Expand All @@ -514,11 +524,7 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
const { expandedMenuItemKey } = this.state;
const { contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem } = this.props;

let { subMenuId } = this.state;
if (item.subMenuProps && item.subMenuProps.id) {
subMenuId = item.subMenuProps.id;
}

const subMenuId = this._getSubMenuId(item);
let ariaLabel = '';

if (item.ariaLabel) {
Expand Down Expand Up @@ -791,6 +797,7 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
*/
private _updateFocusOnMouseEvent(item: any, ev: React.MouseEvent<HTMLElement>) {
const targetElement = ev.currentTarget as HTMLElement;
const { subMenuHoverDelay: timeoutDuration = this._navigationIdleDelay } = this.props;

if (item.key === this.state.expandedMenuItemKey) {
return;
Expand All @@ -812,12 +819,12 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
this._enterTimerId = this._async.setTimeout(() => {
targetElement.focus();
this._onItemSubMenuExpand(item, targetElement);
}, this._navigationIdleDelay);
}, timeoutDuration);
} else {
this._enterTimerId = this._async.setTimeout(() => {
this._onSubMenuDismiss(ev);
targetElement.focus();
}, this._navigationIdleDelay);
}, timeoutDuration);
}
}

Expand Down Expand Up @@ -853,13 +860,14 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
if (item.disabled || item.isDisabled) {
return;
}
let dismiss = false;
if (item.onClick) {
item.onClick(ev, item);
dismiss = !!item.onClick(ev, item);
} else if (this.props.onItemClick) {
this.props.onItemClick(ev, item);
dismiss = !!this.props.onItemClick(ev, item);
}

!ev.defaultPrevented && this.dismiss(ev, true);
(dismiss || !ev.defaultPrevented) && this.dismiss(ev, true);
}

private _onItemKeyDown(item: any, ev: KeyboardEvent) {
Expand Down Expand Up @@ -971,4 +979,14 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
private _isItemDisabled(item: IContextualMenuItem): boolean {
return !!(item.isDisabled || item.disabled);
}

private _getSubMenuId(item: IContextualMenuItem): string | undefined {
let { subMenuId } = this.state;

if (item.subMenuProps && item.subMenuProps.id) {
subMenuId = item.subMenuProps.id;
}

return subMenuId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,10 @@ export interface IContextualMenuProps extends React.Props<ContextualMenu>, IWith

/**
* Click handler which is invoked if onClick is not passed for individual contextual
* menu item
* menu item.
* Returning true will dismiss the menu even if ev.preventDefault() was called.
*/
onItemClick?: (ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem) => void;
onItemClick?: (ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem) => boolean | void;

/**
* CSS class to apply to the context menu.
Expand Down Expand Up @@ -221,6 +222,11 @@ export interface IContextualMenuProps extends React.Props<ContextualMenu>, IWith
/** Method to call when trying to render a submenu. */
onRenderSubMenu?: IRenderFunction<IContextualMenuProps>;

/**
* Delay (in milliseconds) to wait before expanding / dismissing a submenu on mouseEnter or mouseLeave
*/
subMenuHoverDelay?: number;

/**
* Method to override the render of the individual menu items
* @default ContextualMenuItem
Expand Down Expand Up @@ -320,9 +326,10 @@ export interface IContextualMenuItem {
data?: any;

/**
* Callback issued when the menu item is invoked. If ev.preventDefault() is called in onClick, click will not close menu
* Callback issued when the menu item is invoked. If ev.preventDefault() is called in onClick, click will not close menu.
* Returning true will dismiss the menu even if ev.preventDefault() was called.
*/
onClick?: (ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem) => void;
onClick?: (ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem) => boolean | void;

/**
* An optional URL to navigate to upon selection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import * as React from 'react';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import './ContextualMenuExample.scss';

export class ContextualMenuSubmenuExample extends React.Component<any, any> {
export interface IContextualMenuSubmenuExampleState {
hoverDelay: number;
}

export class ContextualMenuSubmenuExample extends React.Component<any, IContextualMenuSubmenuExampleState> {

constructor(props: any) {
super(props);

this.state = {
hoverDelay: 250
};
}

public render() {
return (
<div>
<TextField value={ String(this.state.hoverDelay) } onChanged={ this._onHoverDelayChanged } />
<DefaultButton
id='ContextualMenuButton2'
text='Click for ContextualMenu'
menuProps={ {
shouldFocusOnMount: true,
subMenuHoverDelay: this.state.hoverDelay,
items: [
{
key: 'newItem',
Expand All @@ -29,6 +44,7 @@ export class ContextualMenuSubmenuExample extends React.Component<any, any> {
}
],
},
href: 'https://bing.com',
name: 'New'
},
{
Expand Down Expand Up @@ -72,4 +88,10 @@ export class ContextualMenuSubmenuExample extends React.Component<any, any> {
</div>
);
}

private _onHoverDelayChanged = (newValue: string) => {
this.setState({
hoverDelay: +newValue
});
}
}