-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Add functions to ContextualMenuItem to open and close menus on command #4741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
aafc5d3
90aa6a6
a844d06
da3bef7
4fda842
cad5754
8d73bf7
be9da3d
863b40b
6fa83bb
8699f31
e06449a
782e08f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,6 @@ import { | |
| import { | ||
| BaseComponent, | ||
| IPoint, | ||
| anchorProperties, | ||
| buttonProperties, | ||
| getNativeProps, | ||
| assign, | ||
| getId, | ||
| getRTL, | ||
|
|
@@ -31,8 +28,7 @@ import { withResponsiveMode, ResponsiveMode } from '../../utilities/decorators/w | |
| 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; | ||
|
|
@@ -485,58 +481,34 @@ 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 { contextualMenuItemAs } = this.props; | ||
| const dismissMenu = this.dismiss.bind(this, undefined); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't bind this here, you don't need to.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I have to here. dismiss takes in the event as the first argument but I only pass through the boolean dismissAll in my callback. So I'm binding undefined to only pass through the second argument, basically
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The event can be undefined, it's fine to have it be undefined all the way up |
||
| 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); | ||
| const nativeProps = getNativeProps(item, anchorProperties); | ||
| const disabled = isItemDisabled(item); | ||
|
|
||
| return ( | ||
| <div> | ||
| <KeytipData | ||
| keytipProps={ item.keytipProps } | ||
| ariaDescribedBy={ (nativeProps as any)['aria-describedby'] } | ||
| disabled={ disabled } | ||
| > | ||
| { (keytipAttributes: any): JSX.Element => ( | ||
| <a | ||
| { ...nativeProps } | ||
| { ...keytipAttributes } | ||
| href={ item.href } | ||
| target={ item.target } | ||
| rel={ anchorRel } | ||
| className={ classNames.root } | ||
| role='menuitem' | ||
| aria-owns={ item.key === expandedMenuItemKey ? subMenuId : undefined } | ||
| aria-haspopup={ itemHasSubmenu || undefined } | ||
| aria-expanded={ itemHasSubmenu ? item.key === expandedMenuItemKey : undefined } | ||
| aria-posinset={ focusableElementIndex + 1 } | ||
| aria-setsize={ totalItemCount } | ||
| aria-disabled={ 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 } | ||
| classNames={ classNames } | ||
| index={ index } | ||
| onCheckmarkClick={ hasCheckmarks ? this._onItemClick : undefined } | ||
| hasIcons={ hasIcons } | ||
| /> | ||
| </a> | ||
| ) } | ||
| </KeytipData> | ||
| </div>); | ||
| <ContextualMenuAnchor | ||
| item={ item } | ||
| classNames={ classNames } | ||
| index={ index } | ||
| focusableElementIndex={ focusableElementIndex } | ||
| totalItemCount={ totalItemCount } | ||
| hasCheckmarks={ hasCheckmarks } | ||
| hasIcons={ hasIcons } | ||
| contextualMenuItemAs={ contextualMenuItemAs } | ||
| onItemMouseEnter={ this._onItemMouseEnterBase } | ||
| onItemMouseLeave={ this._onMouseItemLeave } | ||
| onItemMouseMove={ this._onItemMouseMoveBase } | ||
| onItemMouseDown={ this._onItemMouseDown } | ||
| executeItemClick={ this._executeItemClick } | ||
| onItemClick={ this._onAnchorClick } | ||
| onItemKeyDown={ this._onItemKeyDown } | ||
| getSubMenuId={ this._getSubMenuId } | ||
| expandedMenuItemKey={ expandedMenuItemKey } | ||
| openSubMenu={ this._onItemSubMenuExpand } | ||
| dismissSubMenu={ this._onSubMenuDismiss } | ||
| dismissMenu={ dismissMenu } | ||
| componentRef={ item.componentRef } | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| private _renderButtonItem( | ||
|
|
@@ -547,79 +519,35 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext | |
| totalItemCount: number, | ||
| hasCheckmarks?: boolean, | ||
| hasIcons?: boolean) { | ||
| const { contextualMenuItemAs } = this.props; | ||
| const dismissMenu = this.dismiss.bind(this, undefined); | ||
| const { expandedMenuItemKey } = this.state; | ||
| const { contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem } = this.props; | ||
|
|
||
| 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.bind(this, item), | ||
| onKeyDown: itemHasSubmenu ? this._onItemKeyDown.bind(this, item) : null, | ||
| onMouseEnter: this._onItemMouseEnter.bind(this, item), | ||
| onMouseLeave: this._onMouseItemLeave.bind(this, item), | ||
| onMouseDown: (ev: any) => 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) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was not properly copied over, will fix
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you fixed this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
| keytipProps = { | ||
| ...keytipProps, | ||
| hasMenu: true | ||
| }; | ||
| } | ||
|
|
||
| return ( | ||
| <KeytipData | ||
| keytipProps={ keytipProps } | ||
| ariaDescribedBy={ (buttonNativeProperties as any)['aria-describedby'] } | ||
| disabled={ isItemDisabled(item) } | ||
| > | ||
| { (keytipAttributes: any): JSX.Element => ( | ||
| <button | ||
| { ...buttonNativeProperties as React.ButtonHTMLAttributes<HTMLButtonElement> } | ||
| { ...itemButtonProperties as React.ButtonHTMLAttributes<HTMLButtonElement> } | ||
| { ...keytipAttributes } | ||
| > | ||
| <ChildrenRenderer | ||
| item={ item } | ||
| classNames={ classNames } | ||
| index={ index } | ||
| onCheckmarkClick={ hasCheckmarks ? this._onItemClick : undefined } | ||
| hasIcons={ hasIcons } | ||
| /> | ||
| </button> | ||
| ) } | ||
| </KeytipData> | ||
| <ContextualMenuButton | ||
| item={ item } | ||
| classNames={ classNames } | ||
| index={ index } | ||
| focusableElementIndex={ focusableElementIndex } | ||
| totalItemCount={ totalItemCount } | ||
| hasCheckmarks={ hasCheckmarks } | ||
| hasIcons={ hasIcons } | ||
| contextualMenuItemAs={ contextualMenuItemAs } | ||
| onItemMouseEnter={ this._onItemMouseEnterBase } | ||
| onItemMouseLeave={ this._onMouseItemLeave } | ||
| onItemMouseMove={ this._onItemMouseMoveBase } | ||
| onItemMouseDown={ this._onItemMouseDown } | ||
| executeItemClick={ this._executeItemClick } | ||
| onItemClick={ this._onItemClick } | ||
| onItemClickBase={ this._onItemClickBase } | ||
| onItemKeyDown={ this._onItemKeyDown } | ||
| getSubMenuId={ this._getSubMenuId } | ||
| expandedMenuItemKey={ expandedMenuItemKey } | ||
| openSubMenu={ this._onItemSubMenuExpand } | ||
| dismissSubMenu={ this._onSubMenuDismiss } | ||
| dismissMenu={ dismissMenu } | ||
| componentRef={ item.componentRef } | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -632,6 +560,7 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext | |
| hasCheckmarks?: boolean, | ||
| hasIcons?: boolean): JSX.Element { | ||
| const { contextualMenuItemAs } = this.props; | ||
| const dismissMenu = this.dismiss.bind(this, undefined); | ||
|
|
||
| return ( | ||
| <ContextualMenuSplitButton | ||
|
|
@@ -651,6 +580,10 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext | |
| onItemClick={ this._onItemClick } | ||
| onItemClickBase={ this._onItemClickBase } | ||
| onItemKeyDown={ this._onItemKeyDown } | ||
| openSubMenu={ this._onItemSubMenuExpand } | ||
| dismissSubMenu={ this._onSubMenuDismiss } | ||
| dismissMenu={ dismissMenu } | ||
| componentRef={ item.componentRef } | ||
| onTap={ this._onPointerAndTouchEvent } | ||
| /> | ||
| ); | ||
|
|
@@ -731,10 +664,6 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext | |
| this._scrollIdleTimeoutId = this._async.setTimeout(() => { this._isScrollIdle = true; }, NavigationIdleDelay); | ||
| } | ||
|
|
||
| private _onItemMouseEnter = (item: any, ev: React.MouseEvent<HTMLElement>): void => { | ||
| this._onItemMouseEnterBase(item, ev, ev.currentTarget as HTMLElement); | ||
| } | ||
|
|
||
| private _onItemMouseEnterBase = (item: any, ev: React.MouseEvent<HTMLElement>, target?: HTMLElement): void => { | ||
| if (!this._isScrollIdle) { | ||
| return; | ||
|
|
@@ -743,10 +672,6 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext | |
| this._updateFocusOnMouseEvent(item, ev, target); | ||
| } | ||
|
|
||
| private _onItemMouseMove(item: any, ev: React.MouseEvent<HTMLElement>) { | ||
| this._onItemMouseMoveBase(item, ev, ev.currentTarget as HTMLElement); | ||
| } | ||
|
|
||
| private _onItemMouseMoveBase = (item: any, ev: React.MouseEvent<HTMLElement>, target: HTMLElement): void => { | ||
|
|
||
| const targetElement = ev.currentTarget as HTMLElement; | ||
|
|
@@ -857,7 +782,7 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext | |
| ev.preventDefault(); | ||
| } | ||
|
|
||
| private _onAnchorClick(item: IContextualMenuItem, ev: React.MouseEvent<HTMLElement>) { | ||
| private _onAnchorClick = (item: IContextualMenuItem, ev: React.MouseEvent<HTMLElement>) => { | ||
| this._executeItemClick(item, ev); | ||
| ev.stopPropagation(); | ||
| } | ||
|
|
@@ -895,7 +820,7 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext | |
| } | ||
| } | ||
|
|
||
| private _onItemSubMenuExpand(item: IContextualMenuItem, target: HTMLElement) { | ||
| private _onItemSubMenuExpand = (item: IContextualMenuItem, target: HTMLElement): void => { | ||
| if (this.state.expandedMenuItemKey !== item.key) { | ||
|
|
||
| if (this.state.expandedMenuItemKey) { | ||
|
|
@@ -994,7 +919,7 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext | |
| } | ||
| } | ||
|
|
||
| private _getSubMenuId(item: IContextualMenuItem): string | undefined { | ||
| private _getSubMenuId = (item: IContextualMenuItem): string | undefined => { | ||
| let { subMenuId } = this.state; | ||
|
|
||
| if (item.subMenuProps && item.subMenuProps.id) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need to bind these sine the functions are defined a variables the
thisshould be correct. And for dismissMenu you shound't need to pass in undefined since the parameters are optionalThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm passing undefined because this.dismiss takes in the event (optional) as the first param but dismissMenu takes in one param which is dismissAll, which is the second param in this.dismiss. So I'm padding the params basically
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will look into not binding them. I was just following the pattern throughout the file where there are binds