Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
}

private get _isExpanded(): boolean {
if (this.props.persistMenu) {
return !this.state.menuProps!.hidden;
}
return !!this.state.menuProps;
}

Expand Down Expand Up @@ -65,8 +68,13 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
this._labelId = getId();
this._descriptionId = getId();
this._ariaDescriptionId = getId();
let menuProps = null;
if (props.persistMenu && props.menuProps) {
menuProps = props.menuProps;
menuProps.hidden = true;
}
this.state = {
menuProps: null
menuProps: menuProps
};
}

Expand Down Expand Up @@ -401,12 +409,21 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
}

private _dismissMenu = (): void => {
this.setState({ menuProps: null });
let menuProps = null;
if (this.props.persistMenu && this.state.menuProps) {
menuProps = this.state.menuProps;
menuProps.hidden = true;
}
this.setState({ menuProps: menuProps });
}

private _openMenu = (): void => {
if (this.props.menuProps) {
this.setState({ menuProps: this.props.menuProps });
let menuProps = this.props.menuProps;
if (this.props.persistMenu) {
menuProps.hidden = false;
}
this.setState({ menuProps: menuProps });
}
}

Expand All @@ -416,7 +433,11 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
}
const { menuProps } = this.props;
const currentMenuProps = this.state.menuProps;
currentMenuProps ? this._dismissMenu() : this._openMenu();
if (this.props.persistMenu) {
currentMenuProps && currentMenuProps.hidden ? this._openMenu() : this._dismissMenu();
} else {
currentMenuProps ? this._dismissMenu() : this._openMenu();
}
}

private _onRenderSplitButtonContent(tag: any, buttonProps: IButtonProps): JSX.Element {
Expand Down Expand Up @@ -517,7 +538,7 @@ export class BaseButton extends BaseComponent<IBaseButtonProps, IBaseButtonState
'aria-expanded': this._isExpanded,
'data-is-focusable': false
};
return <BaseButton {...splitButtonProps} onMouseDown={ this._onMouseDown } tabIndex={ -1 } />;
return <BaseButton { ...splitButtonProps } onMouseDown={ this._onMouseDown } tabIndex={ -1 } />;
}

private _onMouseDown = (ev: React.MouseEvent<BaseButton>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,15 @@ export interface IButtonProps extends React.AllHTMLAttributes<HTMLAnchorElement
* The default KeyCode is the down arrow. A value of null can be provided to disable the key codes for opening the button menu.
*/
menuTriggerKeyCode?: KeyCodes | null;


/**
* Menu will not be created or destroyed when opened or closed, instead it
* will be hidden. This will improve perf of the menu opening but could potentially
* impact overall perf by having more elemnts in the dom. Should only be used

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be expected to impact the time it takes to mount the button, perhaps that is worth calling out?

* when perf is important.
*/
persistMenu?: boolean;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure what to name this and I am open to suggestions.

}

export enum ElementType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export class ButtonContextualMenuExample extends React.Component<IButtonProps, {
key: 'calendarEvent',
name: 'Calendar event',
icon: 'Calendar'
}
]
},
],
directionalHintFixed: true
}
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,10 @@ export interface ICalloutContentStyleProps {
overflowYHidden?: boolean;

/**
* @deprecated will be removed in v6. Do not use.
* Max height applied to the content of a callout.
*/
contentMaxHeight?: number;
contentMaxHeight?: number

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the semicolon still be here?


/**
* Background color for the beak and callout.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta
directionalHint: DirectionalHint.bottomAutoEdge
};

private _classNames: {[key in keyof ICalloutContentStyles]: string };
private _classNames: { [key in keyof ICalloutContentStyles]: string };
private _didSetInitialFocus: boolean;
private _hostElement = createRef<HTMLDivElement>();
private _calloutElement = createRef<HTMLDivElement>();
Expand Down Expand Up @@ -172,8 +172,8 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta
target = this._getTarget();
const { positions } = this.state;

const getContentMaxHeight: number = this._getMaxHeight() + this.state.heightOffset!;
const contentMaxHeight: number = calloutMaxHeight! && (calloutMaxHeight! < getContentMaxHeight) ? calloutMaxHeight! : getContentMaxHeight!;
const getContentMaxHeight: number | undefined = this._getMaxHeight() ? this._getMaxHeight() + this.state.heightOffset! : undefined;
const contentMaxHeight: number | undefined = calloutMaxHeight! && getContentMaxHeight && (calloutMaxHeight! < getContentMaxHeight) ? calloutMaxHeight! : getContentMaxHeight!;
const overflowYHidden = !!finalHeight;

const beakVisible = isBeakVisible && (!!target);
Expand All @@ -184,15 +184,14 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta
className,
overflowYHidden: overflowYHidden,
calloutWidth,
contentMaxHeight,
positions,
beakWidth,
backgroundColor,
beakStyle
}
);

const overflowStyle: React.CSSProperties = overflowYHidden ? { overflowY: 'hidden' } : {};
const overflowStyle: React.CSSProperties = overflowYHidden ? { overflowY: 'hidden', maxHeight: contentMaxHeight } : { maxHeight: contentMaxHeight };
const visibilityStyle: React.CSSProperties | undefined = this.props.hidden ? { visibility: 'hidden' } : undefined;
// React.CSSProperties does not understand IRawStyle, so the inline animations will need to be cast as any for now.
const content = (
Expand Down Expand Up @@ -384,7 +383,11 @@ export class CalloutContentBase extends BaseComponent<ICalloutProps, ICalloutSta
// Since the callout cannot measure it's border size it must be taken into account here. Otherwise it will
// overlap with the target.
const totalGap = gapSpace + beakWidth! + BORDER_WIDTH * 2;
this._maxHeight = getMaxHeight(this._target, this.props.directionalHint!, totalGap, this._getBounds());
this._async.requestAnimationFrame(() => {
if (this._target) {
this._maxHeight = getMaxHeight(this._target, this.props.directionalHint!, totalGap, this._getBounds());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to trigger a new render after this RAF call to actually set the max height?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I really should have had this been setstate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I ended up reverting this, to not use setstate. Ideally the maxHeight should be set as soon as possible, which can happen during the first available animation frame.

}
});
} else {
this._maxHeight = this._getBounds().height! - BORDER_WIDTH * 2;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,29 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
const newTarget = newProps.target;
this._setTargetWindowAndElement(newTarget!);
}
if (newProps.hidden !== this.props.hidden) {
if (newProps.hidden) {
this._onMenuClosed()
} else {
this._onMenuOpened()
this._previousActiveElement = this._targetWindow ? this._targetWindow.document.activeElement as HTMLElement : null;
}
}
}

// Invoked once, both on the client and server, immediately before the initial rendering occurs.
public componentWillMount() {
const target = this.props.target;
this._setTargetWindowAndElement(target!);
this._previousActiveElement = this._targetWindow ? this._targetWindow.document.activeElement as HTMLElement : null;
if (!this.props.hidden) {
this._previousActiveElement = this._targetWindow ? this._targetWindow.document.activeElement as HTMLElement : null;
}
}

// Invoked once, only on the client (not on the server), immediately after the initial rendering occurs.
public componentDidMount() {
this._events.on(this._targetWindow, 'resize', this.dismiss);
if (this.props.onMenuOpened) {
this.props.onMenuOpened(this.props);
if (!this.props.hidden) {
this._onMenuOpened();
}
}

Expand Down Expand Up @@ -270,6 +279,7 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
onScroll={ this._onScroll }
bounds={ bounds }
directionalHintFixed={ directionalHintFixed }
hidden={ this.props.hidden }
>
<div
role='menu'
Expand Down Expand Up @@ -316,6 +326,16 @@ export class ContextualMenu extends BaseComponent<IContextualMenuProps, IContext
}
}

private _onMenuOpened() {
this._events.on(this._targetWindow, 'resize', this.dismiss);
this.props.onMenuOpened && this.props.onMenuOpened(this.props);
}

private _onMenuClosed() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a similar call to this.props.onMenuDismissed here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not, onDismiss is a callback that would have triggered the hidden prop change farther up the tree.

this._events.off(this._targetWindow, 'resize', this.dismiss);
setTimeout(() => this._previousActiveElement!.focus(), 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is _previousActiveElement guaranteed to not be null/undefined here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not. I'll add a check

}

/**
* Gets the focusZoneDirection by using the arrowDirection if specified,
* the direction specificed in the focusZoneProps, or defaults to FocusZoneDirection.vertical
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ export interface IContextualMenuProps extends React.Props<ContextualMenu>, IWith
* @default {direction: FocusZoneDirection.vertical}
*/
focusZoneProps?: IFocusZoneProps;

/**
* If specified, renders the ContextualMenu in a hidden state.
* Use this flag, rather than rendering a ContextualMenu conditionally based on visibility,
* to improve rendering performance when it becomes visible.
* Note: When ContextualMenu is hidden its content will not be rendered. It will only render
* once the ContextualMenu is visible.
*/
hidden?: boolean;
}

export interface IContextualMenuItem {
Expand Down