Skip to content

Conversation

@chang47
Copy link
Contributor

@chang47 chang47 commented Apr 19, 2018

Problem:
The Contextual Menu is getting bloated and there is a pending change (#4353) that requires better state control for split buttons so the best fix to rectify this problem is to make the split button in the contextual menu its own component.

Solution:
I moved the code that rendered a contextual menu into a separate component and added all the previous event listeners as callbacks that will be called by the new component.

Validation:

  • The split button in the contextual menu still acts as it did before with mouse and keyboard interactions. That is the case with both checkmarks and normal items
  • The other buttons: anchor buttons, normal buttons, also remained unchanged, which is correct as the this change didn't touch those buttons.

@chang47 chang47 requested a review from joschect as a code owner April 19, 2018 17:41
@chang47
Copy link
Contributor Author

chang47 commented Apr 19, 2018

@jspurlin

private _updateFocusOnMouseEvent(item: IContextualMenuItem, ev: React.MouseEvent<HTMLElement>) {
const targetElement = ev.currentTarget as HTMLElement;
private _updateFocusOnMouseEvent(item: IContextualMenuItem, ev: React.MouseEvent<HTMLElement>, target: HTMLElement) {
const targetElement = target;
Copy link
Contributor

Choose a reason for hiding this comment

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

You should make target optional so that it can be leveraged only when it needs to be

private _onItemMouseDown(item: IContextualMenuItem, ev: React.MouseEvent<HTMLElement>) {
if (item.onMouseDown) {
private _onItemMouseDown = (item: IContextualMenuItem, ev: React.MouseEvent<HTMLElement>): void => {
if (item.onMouseDown && this._enterTimerId === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this change here? mousedown should not be gated by the enterTimerId

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, this was back when we were trying to fight the race condition caused by hovering onon the secondary button in split button vs clicking on it where we would open and close the menu.

This should have been removed.

export class ContextualMenuSplitButton extends BaseComponent<IContextualMenuSplitButtonProps, IContextualMenuSplitButtonState> {
private _processingTouch: boolean;
private _lastTouchTimeoutId: number | undefined;
private _splitButton: HTMLDivElement;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see these used anywhere.

aria-checked={ item.isChecked || item.checked }
aria-posinset={ focusableElementIndex + 1 }
aria-setsize={ totalItemCount }
onMouseEnter={ this._onItemMouseEnter.bind(this, { ...item, subMenuProps: null, items: null }) }
Copy link
Contributor

Choose a reason for hiding this comment

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

since item is in props, move this logic to the private function, so for example, onItemMouseEnter would look like:

  private _onItemMouseEnter = (ev: React.MouseEvent<HTMLElement>): void => {
    const { onItemMouseEnter, item } = this.props;
    if (onItemMouseEnter) {
      onItemMouseEnter({...item, submenuProps: null, items: null}, ev, this._splitButton);
    }
  }

Also since the function is defined as a lambda you don't need to bind it.

Copy link
Contributor Author

@chang47 chang47 Apr 20, 2018

Choose a reason for hiding this comment

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

I made the changes, but after playing around with it, I decided that I'm going to keep it as is.

The way the split button before previously was used, we passed in a a item with no submenuProps or list of items when we enter the container, if we get rid of the binding like this, we'll have to make 2 functions, one that does the existing function and the other one that does the same thing, but passes in a modified item, otherwise we face runtime undefined code.

} as IContextualMenuItem;
return React.createElement('button',
getNativeProps(itemProps, buttonProperties),
<ChildrenRenderer data-is-focusable={ false } item={ itemProps } classNames={ classNames } index={ index } onCheckmarkClick={ hasCheckmarks && onItemClick ? onItemClick.bind(this, item) : undefined } hasIcons={ hasIcons } />,
Copy link
Contributor

Choose a reason for hiding this comment

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

Break this up into lines. Also lets go ahead and return instead of using create element. We really should phase createElement out entirely.

@christiango
Copy link
Member

How much of this is just a refactor vs an actual code change?

@chang47
Copy link
Contributor Author

chang47 commented Apr 24, 2018

@christiango Most of this is refactor, for some context if you aren't aware, this was made for my touch/tap code, but I decided that it was better to just make the change separate, so this is only the split button refactoring.

The 2 actual code change I did was:

  1. Remove any mention of splitButtonContainer in Contextual Menu since the ContextualMenuSplitButton has the reference now and does its own state management
  2. When I added onItemMouseEnterBase and _onItemMouseMoveBase. When we select the secondary button in the button, we would only give a reference to that button and not the the whole container, as a result, the submenu will show up only next to the secondary button, which is wrong if the submenu were to appear to the left of the button.

Regarding the second issue, what's interesting is that this was working prior to my change, so I'm going to look a bit at the problem. I think the reason I encountered it previously was because I was passing in the wrong target, but let me validate that again.

import { IContextualMenuItemProps } from './ContextualMenuItem.types';
import { ContextualMenuSplitButton } from './ContextualMenuSplitButton';

export interface IContextualMenuSplitButtonProps extends React.Props<ContextualMenuSplitButton> {
Copy link
Member

Choose a reason for hiding this comment

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

should this extend contextualmenuitem?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, we should.

/**
* CSS class to apply to the context menu.
*/
classNames: IMenuItemClassNames;
Copy link
Member

@dzearing dzearing Apr 25, 2018

Choose a reason for hiding this comment

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

Hmm. This is not the recommended pattern; could you change to getStyles when adding a component? Or is there a reason you're doing this?

The problem with classNames is that it can't be a function of the state, so you can't say have something which is styled according to the expanded state.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only reason I did it this way was because I was passing an existing style, but since we're separating the split button from the menu, I think it makes sense to separate out the styles here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dzearing After thinking about this, we can split the split button part of the code out, however, we still need the className to get the styles for the ContextualMenuItems that we generate.

Specifically, I can split the move the split button to be its own style, but what should we do with the styles that we need to use for the ContextualMenuItem? For example className.icon and classname.checkmarkIcon, both of these are created in Contextual Menu and we currently pass it down to the contextual menu. Do we want to re-create it? Do we just pass it down? What are your thoughts?

Copy link
Member

@dzearing dzearing Apr 27, 2018

Choose a reason for hiding this comment

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

that is totally fine... fwiw: you can still have getStyles, which takes those in.

getStyles={ props => { icon: className.icon, checkmarkIcon: classNames.checkmarkIcon }

One other thought is that it's entirely possible we change IGetStylesFunction to take in an object OR a function. That way if you don't care about theme or props to return some classes, you could just do that.

Copy link
Contributor Author

@chang47 chang47 Apr 27, 2018

Choose a reason for hiding this comment

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

Huh, I see, so add it an as an addition got it. I'll do something like this for my next iteration on this. Thanks!

split: true,
} as IContextualMenuItem;

const buttonProp = assign({}, getNativeProps(itemProps, buttonProperties), {
Copy link
Member

Choose a reason for hiding this comment

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

buttonProps

this._onItemMouseEnterBase(item, ev, ev.currentTarget as HTMLElement);
}

private _onItemMouseEnterBase = (item: any, ev: React.MouseEvent<HTMLElement>, target: HTMLElement): void => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe let target be optional since it's optional when going into focusMouseEvent. Not sure about this though, I'll leave it up to you

aria-checked={ item.isChecked || item.checked }
aria-posinset={ focusableElementIndex + 1 }
aria-setsize={ totalItemCount }
onMouseEnter={ this._onItemMouseEnter.bind({ ...item, subMenuProps: null, items: null }) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't do the bind here, instead in move it to the function call in splitbutton like

    const { onItemMouseEnter, item } = this.props;
    if (onItemMouseEnter) {
      onItemMouseEnter({...item, submenuProps: null}, items: null)}, ev, this._splitButton);
    }

@dzearing dzearing closed this Apr 27, 2018
@dzearing dzearing reopened this Apr 27, 2018
}

private _renderSplitDivider(item: IContextualMenuItem) {
const getDividerClassnames = item.getSplitButtonVerticalDividerClassNames || getSplitButtonVerticalDividerClassNames;
Copy link
Member

Choose a reason for hiding this comment

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

classnames is 2 words (getDividerClassNames)

} as IContextualMenuItem;
return (
<button {...getNativeProps(itemProps, buttonProperties) }>
<ChildrenRenderer data-is-focusable={ false } item={ itemProps } classNames={ classNames } index={ index } onCheckmarkClick={ hasCheckmarks && onItemClick ? onItemClick.bind(this, item) : undefined } hasIcons={ hasIcons } />
Copy link
Member

Choose a reason for hiding this comment

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

you have a bind here, and this is a really long line. Can you clean this up?

Copy link
Member

Choose a reason for hiding this comment

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

(at least line length.) I'm not sure about the bind.

/**
* CSS class to apply to the context menu.
*/
classNames: IMenuItemClassNames;
Copy link
Member

@dzearing dzearing Apr 27, 2018

Choose a reason for hiding this comment

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

that is totally fine... fwiw: you can still have getStyles, which takes those in.

getStyles={ props => { icon: className.icon, checkmarkIcon: classNames.checkmarkIcon }

One other thought is that it's entirely possible we change IGetStylesFunction to take in an object OR a function. That way if you don't care about theme or props to return some classes, you could just do that.

@dzearing dzearing merged commit 175597d into microsoft:master Apr 27, 2018
@microsoft microsoft locked as resolved and limited conversation to collaborators Aug 31, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants