-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Contextual Menu: Moving Split Button to be its own component #4618
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
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.
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) { |
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.
why is this change here? mousedown should not be gated by the enterTimerId
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.
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.
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 }) } |
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.
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.
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 } />, |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this extend contextualmenuitem?
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.
You're right, we should.
| /** | ||
| * CSS class to apply to the context menu. | ||
| */ | ||
| classNames: IMenuItemClassNames; |
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.
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.
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.
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.
@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.
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.
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.
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), { |
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.
buttonProps
| 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.
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 }) } |
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.
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.
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 } /> |
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.
you have a bind here, and this is a really long line. Can you clean this up?
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.
(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.
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.
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: