Skip to content

Conversation

@kelseyyoung
Copy link
Contributor

@kelseyyoung kelseyyoung commented May 2, 2018

Pull request checklist

  • Addresses an existing issue: Fixes #0000
  • Include a change request file using $ npm run change

Description of changes

This work is related to keytips. It gives a way for a developer, given a ref to a contextual menu item, to open and close its menus on command instead of mocking a click or keypress. They are similar to the functions added to BaseButton (openMenu and dismissMenu). The functions added are:

  • openSubMenu() - opens an item's subMenu, if it has one
  • dismissSubMenu() - closes an item's subMenu, if it has one
  • dismissMenu(dismissAll?: boolean) - closes the menu this ContextualMenuItem lives in, or all menus if dismissAll is true. The dismissAll parameter is dependent on the developer's implementation of onDismiss (which is how it's always been)

For ContextualMenuItem, these functions will call optional props that are assigned by ContextualMenu. The openSubMenu prop function requires a target to attach the subMenu to; this is also assigned in ContextualMenu by passing down a ref to the container element (the anchor in _renderAnchorMenuItem and the button in _renderButtonItem)

ContextualMenuSplitButton also uses this interface and has access to these functions. It does not need to take in a ref for openSubMenu because the containing div (this._splitButton) already has a ref and is what the subMenu should attach to

Part of the change requires ContextualMenuItem to become a BaseComponent to take advantage of componentRef

Focus areas to test

Wrote unit tests and tested manually

@kelseyyoung kelseyyoung requested a review from joschect as a code owner May 2, 2018 18:29
const anchorRef = createRef<HTMLAnchorElement>();
const openSubMenu = this._onItemSubMenuExpand.bind(this);
const dismissSubMenu = this._onSubMenuDismiss.bind(this);
const dismissMenu = this.dismiss.bind(this, undefined);
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 think you need to bind these sine the functions are defined a variables the this should be correct. And for dismissMenu you shound't need to pass in undefined since the parameters are optional

Copy link
Contributor Author

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

Copy link
Contributor Author

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

@kelseyyoung
Copy link
Contributor Author

Hmmm I will see why I'm adding 350B...I wouldn't think it would be that much based on the changes

- Move private ContextualMenuItem functions back to local functions
- Renames and refactoring
Copy link
Contributor

@joschect joschect left a comment

Choose a reason for hiding this comment

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

I'm tempted to say that we should approach this from another angle. Ref's should be avoided in general, and in this case props could be a way of expressing which item/menu should be open. I'm not sure how much more work is required but I do believe that that's the correct way to move in. @dzearing or @cliffkoh might disagree with me though.

hasMenu: true
};
}
const btnRef = createRef<HTMLButtonElement>();
Copy link
Contributor

Choose a reason for hiding this comment

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

We really should avoid using refs whenever possible. Especially not in the way that this one is used. Instead of passing a ref down, pass a queryselector through or something like that. If anything the item itself should decide what the target is.

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 ref is required so that the submenu positions itself correctly on the containing and elements (which is what they normally position on) which aren't available in ContextualMenuItem. An alternative approach is to move out the button and anchor items into their own component, like ContextualMenuSplitButton was moved. This would remove the need to pass down a ref (in the code you can see that I don't pass down that ref to ContextualMenuSplitButton). Again curious to hear @dzearing and @cliffkoh opinions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also up for discussion: passing the componentRef to ContextualMenuItem through IContextualMenuItem and 'renderItemRef' in order to set the componentRef on ContextualMenuItem. Again pinging @dzearing and @cliffkoh

Copy link
Contributor

@jspurlin jspurlin May 3, 2018

Choose a reason for hiding this comment

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

@kelseyyoung the best sounding approach you've described would be to split button and anchor into their own components ( ContextualMenuButton and ContextualMenuAnchor) to remove the need to pass down a ref

const { contextualMenuItemAs } = this.props;

const openSubMenu = this._onItemSubMenuExpand.bind(this);
const dismissSubMenu = this._onSubMenuDismiss.bind(this);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do these need to use bind?

- Move all wrapper items into their own folder under ContextualMenu
@kelseyyoung
Copy link
Contributor Author

@jspurlin and @joschect, I've updated the PR where Button and Anchor are moved to their own components, I've called these along with Split Button "ContextualMenuItemWrapper" components. This change means no real changes to ContextualMenuItem. The componentRef in IContextualMenuItem is now passed to one of these wrapper components

};

let { keytipProps } = item;
if (keytipProps && itemHasSubmenu) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was not properly copied over, will fix

Copy link
Contributor

Choose a reason for hiding this comment

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

Have you fixed this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes

@kelseyyoung
Copy link
Contributor Author

After testing in WAC I'm having some problems where the internal ref that determines where to open the submenu isn't being set fast enough. I'll try to figure out why tomorrow

- Rename the prop to componentRef on IContextualMenuItem
- Add tests for split button and anchor
- Pass a prop function which returns the element to use in openSubMenu to ContextualMenuItem
@kelseyyoung
Copy link
Contributor Author

Another update: the previous iteration had issues where our internal refs weren't being correctly set. I've moved the open/dismiss functions back to ContextualMenuItem. For openSubMenu to get the element to position on, I've added a 'getContainerElement' prop in ContextualMenuItem which is set by the wrapper components (SplitButton, Button, Anchor). This involves refs internally in the wrapper components but doesn't involve passing them down to ContextualMenuItem. Would love to hear feedback from @joschect and @dzearing


export const ContextualMenuItem: React.StatelessComponent<IContextualMenuItemProps> = (props) => {
const { item, classNames } = props;
export class ContextualMenuItem extends BaseComponent<IContextualMenuItemProps, {}> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did the extends change from React.StatelessComponent to BaseComponent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To take advantage of componentRef. You can't use a ref on a functional component

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another option would be to just extend PureComponent and assign ref instead of componentRef. But then naming the prop 'componentRef' is a little misleading

Copy link
Contributor

Choose a reason for hiding this comment

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

Any decision here?

disabled={ disabled }
>
{ (keytipAttributes: any): JSX.Element => (
<a
Copy link
Contributor

Choose a reason for hiding this comment

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

ContextualMenuAnchor and ContextualMenuButton are very similar (a different tag is used and a few different props are used). Did you consider creating a generic component that takes a tag and itemprops and would return the correct type of item?

Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't a bad idea, you could even have the tag be a prop that get's passed down.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Jeremy and I talked about it; in my opinion there are still a few differences between the two and may require more work. I don't know if this PR should be that far reaching. I understand there's now some duplication across these two files, but I guess I would rather log a bug to fix this later

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you reply with the bug info for the future work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

openSubMenu={ openSubMenu }
dismissSubMenu={ dismissSubMenu }
dismissMenu={ dismissMenu }
getContainerElement={ this._getContainerElement }
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this solution much better overall. I might call it "getMenuTarget" or somehting like that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will rename to 'getSubmenuTarget'


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);
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 bind this here, you don't need to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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

</div>);
}

private _onItemMouseEnter = (ev: React.MouseEvent<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.

Since these are the same as the ButtonItem methods, they should either get passed in as props, or inherited from some base component to reduce code duplication.

- Move shared functions into protected members
- Rename getContainerElement to getSubmenuTarget
@kelseyyoung
Copy link
Contributor Author

Updated the PR, these are the changes:
Made a ContextualMenuItemWrapper base class. All three wrapper classes inherit from it. The shared functions are now protected in the base class. SplitButton still has a lot of private functions because of the large difference in it's functionality

@kelseyyoung kelseyyoung merged commit daa7b69 into microsoft:master May 7, 2018
Markionium added a commit to Markionium/office-ui-fabric-react that referenced this pull request May 13, 2018
* master: (52 commits)
  Marqueeselection style update (microsoft#4803)
  Applying package updates.
  FocusZone: Add the ability to stop focus from propagating outside the FocusZone (microsoft#4823)
  Unknown persona coin (microsoft#4809)
  Applying package updates.
  BaseButton: Allow Alt + Down on menu buttons to open the menu (microsoft#4807)
  Applying package updates.
  Website: Scroll to top of page on navigation (microsoft#4810)
  Applying package updates.
  ActivityItem: fix getstyles (microsoft#4802)
  Mark Slider#ValuePosition enum as deprecated as it is unused. (microsoft#4799)
  Icon: ensure `styles` still works. (microsoft#4805)
  Popup: Added check for onBlur to prevent focus setting focus to be incorrectly disabled when closing a menu in Chrome (microsoft#4804)
  Update package.json
  Move <Layer> to use React portals when available (microsoft#4724)
  Fix breadcrumb rendering issue when overflow item is at last index  (microsoft#4787)
  Fixes focus issue for contextual menus (microsoft#4744)
  Remove redundant selected items prop on BaseExtendedPicker (microsoft#4769)
  SearchBox: New prop for turning off icon animation (microsoft#4794)
  Add functions to ContextualMenuItem to open and close menus on command (microsoft#4741)
  ...
@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.

3 participants