Contextual Menu: Moving Split Button to be its own component#4618
Conversation
| 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; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
why is this change here? mousedown should not be gated by the enterTimerId
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 }) } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 } />, |
There was a problem hiding this comment.
Break this up into lines. Also lets go ahead and return instead of using create element. We really should phase createElement out entirely.
|
How much of this is just a refactor vs an actual code change? |
|
@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:
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> { |
There was a problem hiding this comment.
should this extend contextualmenuitem?
There was a problem hiding this comment.
You're right, we should.
| /** | ||
| * CSS class to apply to the context menu. | ||
| */ | ||
| classNames: IMenuItemClassNames; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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), { |
| this._onItemMouseEnterBase(item, ev, ev.currentTarget as HTMLElement); | ||
| } | ||
|
|
||
| private _onItemMouseEnterBase = (item: any, ev: React.MouseEvent<HTMLElement>, target: HTMLElement): void => { |
There was a problem hiding this comment.
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 }) } |
There was a problem hiding this comment.
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);
}
| } | ||
|
|
||
| private _renderSplitDivider(item: IContextualMenuItem) { | ||
| const getDividerClassnames = item.getSplitButtonVerticalDividerClassNames || getSplitButtonVerticalDividerClassNames; |
There was a problem hiding this comment.
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 } /> |
There was a problem hiding this comment.
you have a bind here, and this is a really long line. Can you clean this up?
There was a problem hiding this comment.
(at least line length.) I'm not sure about the bind.
| /** | ||
| * CSS class to apply to the context menu. | ||
| */ | ||
| classNames: IMenuItemClassNames; |
There was a problem hiding this comment.
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.
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: