Skip to content
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

Button: Decoupling ContextualMenu to improve bundlesize #8220

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@uifabric/experiments",
"comment": "Button: Decoupling ContextualMenu to improve bundlesize",
"type": "minor"
}
],
"packageName": "@uifabric/experiments",
"email": "[email protected]"
}
28 changes: 21 additions & 7 deletions packages/experiments/src/components/Button/Button.types.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { IComponent, IComponentStyles, IHTMLSlot, IHTMLElementSlot, ISlotProp, IStyleableComponentProps } from '../../Foundation';
import { IButtonMenuProps, IButtonMenuSlot } from './ButtonMenu/ButtonMenu.types';
import {
IComponent,
IComponentStyles,
IHTMLSlot,
IHTMLElementSlot,
ISlotProp,
ISlottableReactType,
IStyleableComponentProps
} from '../../Foundation';
import { IFontWeight, IStackSlot, ITextSlot } from 'office-ui-fabric-react';
import { IContextualMenuSlot, IIconSlot } from '../../utilities/factoryComponents.types';
import { IIconSlot } from '../../utilities/factoryComponents.types';
import { IBaseProps } from '../../Utilities';
import { IRawStyleBase } from '@uifabric/merge-styles/lib/IRawStyleBase';

Expand Down Expand Up @@ -36,9 +45,9 @@ export interface IButtonSlots {
icon?: IIconSlot;

/**
* Defines the contextual menu that appears when you click on the Button.
* Defines the menu that appears when you click on the Button.
*/
menu?: IContextualMenuSlot;
menu?: IButtonMenuSlot;

/**
* Defines the menu chevron icon that is displayed insisde the Button.
Expand Down Expand Up @@ -105,7 +114,12 @@ export interface IButtonProps
expanded?: boolean;

/**
* Defines whether the button is rendered as a Split Button.
* Defines what type of component the menu is going to be rendered as.
*/
menuType?: ISlottableReactType<IButtonMenuProps>;

/**
* Defines whether the Button is rendered as a Split Button.
* @defaultvalue false
*/
split?: boolean;
Expand All @@ -131,12 +145,12 @@ export interface IButtonViewProps extends IButtonProps {
/**
* Defines a callback that runs after the Button's contextual menu has been closed (removed from the DOM).
*/
onMenuDismiss: () => void;
onMenuDismiss?: () => void;

/**
* Defines the target that the contextual menu uses to position itself.
*/
menuTarget: HTMLElement | undefined;
menuTarget?: HTMLElement | undefined;

/**
* Defines an event callback that is triggered when the secondary action of a Split Button is clicked.
Expand Down
25 changes: 16 additions & 9 deletions packages/experiments/src/components/Button/Button.view.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/** @jsx withSlots */
import { ContextualMenu, DirectionalHint, Stack, Text } from 'office-ui-fabric-react';
import { DirectionalHint, Stack, Text } from 'office-ui-fabric-react';
import { withSlots, getSlots, ISlots, IPropsWithChildren } from '../../Foundation';
import { getNativeProps, buttonProperties } from '../../Utilities';
import { Icon } from '../../utilities/factoryComponents';

import { IButtonComponent, IButtonProps, IButtonSlots, IButtonViewProps } from './Button.types';
import { ButtonMenu } from './ButtonMenu/ButtonMenu';

export const ButtonView: IButtonComponent['view'] = props => {
const { disabled, onClick, split, ...rest } = props;
const { disabled, menu: Menu, expanded, menuType, menuTarget, onMenuDismiss, onClick, split, ...rest } = props;

// TODO: 'href' is anchor property... consider getNativeProps by root type
const buttonProps = { ...getNativeProps(rest, buttonProperties) };
Expand All @@ -17,7 +18,7 @@ export const ButtonView: IButtonComponent['view'] = props => {
stack: Stack,
icon: Icon,
content: Text,
menu: ContextualMenu,
menu: ButtonMenu,
menuIcon: Icon,

primaryActionContainer: Stack,
Expand All @@ -34,12 +35,22 @@ export const ButtonView: IButtonComponent['view'] = props => {
aria-disabled={disabled}
>
{split ? renderSplitButton(props, Slots) : renderButton(props, Slots)}

{expanded && Menu && (
<Slots.menu
menuType={menuType}
target={menuTarget}
onDismiss={onMenuDismiss}
items={[]}
directionalHint={DirectionalHint.bottomRightEdge}
/>
)}
</Slots.root>
);
};

function renderButton(props: IPropsWithChildren<IButtonViewProps>, Slots: ISlots<Required<IButtonSlots>>): JSX.Element {
const { icon, content, children, menu: Menu, expanded, menuTarget, onMenuDismiss } = props;
const { icon, content, children, menu: Menu } = props;

return (
<Slots.stack horizontal as="span" gap={8} verticalAlign="center" horizontalAlign="center" verticalFill>
Expand All @@ -51,13 +62,12 @@ function renderButton(props: IPropsWithChildren<IButtonViewProps>, Slots: ISlots
<Slots.menuIcon iconName="ChevronDown" />
</Stack.Item>
)}
{expanded && Menu && <Slots.menu target={menuTarget} onDismiss={onMenuDismiss} items={[]} />}
</Slots.stack>
);
}

function renderSplitButton(props: IPropsWithChildren<IButtonViewProps>, Slots: ISlots<Required<IButtonSlots>>): JSX.Element {
const { icon, content, children, menu: Menu, expanded, onClick, onSecondaryActionClick, menuTarget, onMenuDismiss } = props;
const { icon, content, children, onClick, onSecondaryActionClick } = props;

return (
<Slots.stack horizontal as="span" gap={8} verticalAlign="stretch" horizontalAlign="center" verticalFill>
Expand All @@ -79,9 +89,6 @@ function renderSplitButton(props: IPropsWithChildren<IButtonViewProps>, Slots: I

<Slots.secondaryActionContainer onClick={onSecondaryActionClick}>
<Slots.menuIcon iconName="ChevronDown" />
{expanded && Menu && (
<Slots.menu target={menuTarget} onDismiss={onMenuDismiss} items={[]} directionalHint={DirectionalHint.bottomRightEdge} />
)}
</Slots.secondaryActionContainer>
</Slots.stack>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getGlobalClassNames } from '../../../Styling';
import { IButtonMenuComponent, IButtonMenuStylesReturnType } from './ButtonMenu.types';

const GlobalClassNames = {
root: 'ms-ButtonMenu'
};

export const ButtonMenuStyles: IButtonMenuComponent['styles'] = (props, theme): IButtonMenuStylesReturnType => {
const classNames = getGlobalClassNames(GlobalClassNames, theme);

return {
root: [theme.fonts.medium, classNames.root]
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** @jsx withSlots */
import * as React from 'react';
import { withSlots, createComponent, getSlots } from '../../../Foundation';
import { IButtonMenuComponent, IButtonMenuProps, IButtonMenuSlots } from './ButtonMenu.types';
import { ButtonMenuStyles as styles } from './ButtonMenu.styles';

const view: IButtonMenuComponent['view'] = props => {
const { menuType = 'div', ...rest } = props;

const Slots = getSlots<IButtonMenuProps, IButtonMenuSlots>(props, {
root: menuType
});

return <Slots.root {...rest} />;
};
Copy link
Member

Choose a reason for hiding this comment

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

Is this intended to be a legitimate viable default menu option? Are there any examples that use it? (It seem all the examples were converted to use ContextualMenu.)


export const ButtonMenu: React.StatelessComponent<IButtonMenuProps> = createComponent({
displayName: 'ButtonMenu',
styles,
view
});

export default ButtonMenu;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { IComponent, IComponentStyles, IHTMLSlot, ISlotProp, ISlottableReactType } from '../../../Foundation';
import { IBaseProps, IPoint } from '../../../Utilities';
import { DirectionalHint } from 'office-ui-fabric-react';

export type IButtonMenuComponent = IComponent<IButtonMenuProps, IButtonMenuTokens, IButtonMenuStyles>;

// These types are redundant with IButtonComponent but are needed until TS function return widening issue is resolved:
// https://github.com/Microsoft/TypeScript/issues/241
// For now, these helper types can be used to provide return type safety when specifying tokens and styles functions.
export type IButtonMenuTokenReturnType = ReturnType<Extract<IButtonMenuComponent['tokens'], Function>>;
export type IButtonMenuStylesReturnType = ReturnType<Extract<IButtonMenuComponent['styles'], Function>>;

export type IButtonMenuSlot = ISlotProp<IButtonMenuProps>;

export interface IButtonMenuSlots {
root?: IHTMLSlot;
}

export interface IButtonMenu {}

export interface IButtonMenuProps extends IButtonMenuSlots, IBaseProps<IButtonMenu> {
/**
* Defines the target that the ButtonMenu should try to position itself based on.
* It can be either an Element a querySelector string of a valid Element
* or a MouseEvent. If MouseEvent is given then the origin point of the event will be used.
*/
target?: Element | string | MouseEvent | IPoint | null;

/**
* Defines the callback that is exectued when the ButtonMenu tries to close.
* If dismissAll is true then all submenus will be dismissed.
*/
onDismiss?: (ev?: any, dismissAll?: boolean) => void;

/**
* Defines a collection of menu items.
* @defaultvalue []
*/
items: any[];

/**
* Defines how the element should be positioned.
* @defaultvalue DirectionalHint.bottomAutoEdge
*/
directionalHint?: DirectionalHint;

/**
* Defines what type of component the menu is going to be rendered as.
*/
menuType?: ISlottableReactType<IButtonMenuProps>;
}
Copy link
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 duplicate props in IContextualMenuProps? We should be able to depend on types without affecting bundle size.


export interface IButtonMenuTokens {}

export type IButtonMenuStyles = IComponentStyles<IButtonMenuSlots>;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Button, IButtonProps } from '../index';
import { Icon, CommandBar, Stack, Text } from 'office-ui-fabric-react';
import { CommandBar, ContextualMenu, Icon, Stack, Text } from 'office-ui-fabric-react';

const menuItems = [{ key: 'a', name: 'Item a' }, { key: 'b', name: 'Item b' }];
const buttonMenu: IButtonProps['menu'] = render => render((MenuType, props) => <MenuType {...props} items={menuItems} />);
Expand Down Expand Up @@ -34,16 +34,40 @@ export class ButtonExample extends React.Component<{}, {}> {
<Button primary disabled content="Disabled primary button" onClick={alertClicked} />
</ButtonStack>
<ButtonStack>
<Button split icon="Add" content="Default split button" menu={buttonMenu} onClick={alertClicked} />
<Button split disabled icon="Add" content="Disabled split button" menu={buttonMenu} onClick={alertClicked} />
<Button split primary icon="Add" content="Primary split button" menu={buttonMenu} onClick={alertClicked} />
<Button
split
icon="Add"
content="Default split button"
menu={buttonMenu}
menuType={ContextualMenu}
onClick={alertClicked}
/>
<Button
split
disabled
icon="Add"
content="Disabled split button"
menu={buttonMenu}
menuType={ContextualMenu}
onClick={alertClicked}
/>
<Button
split
primary
icon="Add"
content="Primary split button"
menu={buttonMenu}
menuType={ContextualMenu}
onClick={alertClicked}
/>
<Button
split
disabled
primary
icon="Add"
content="Disabled primary split button"
menu={buttonMenu}
menuType={ContextualMenu}
onClick={alertClicked}
/>
</ButtonStack>
Expand All @@ -54,6 +78,7 @@ export class ButtonExample extends React.Component<{}, {}> {
icon="Add"
content="First action disabled split button"
menu={buttonMenu}
menuType={ContextualMenu}
onClick={alertClicked}
/>
<Button
Expand All @@ -63,6 +88,7 @@ export class ButtonExample extends React.Component<{}, {}> {
icon="Add"
content="First action disabled primary split button"
menu={buttonMenu}
menuType={ContextualMenu}
onClick={alertClicked}
/>
</ButtonStack>
Expand All @@ -88,14 +114,14 @@ export class ButtonExample extends React.Component<{}, {}> {
</Button>
</ButtonStack>
<ButtonStack>
<Button content="Menu button" menu={buttonMenu} />
<Button primary content="Menu primary button" menu={buttonMenu} />
<Button disabled content="Menu disabled button" menu={buttonMenu} />
<Button content="Menu button" menu={buttonMenu} menuType={ContextualMenu} />
<Button primary content="Menu primary button" menu={buttonMenu} menuType={ContextualMenu} />
<Button disabled content="Menu disabled button" menu={buttonMenu} menuType={ContextualMenu} />
<Button expanded content="Menu expanded button" />
<Button expanded primary content="Menu expanded primary button" />
</ButtonStack>
<ButtonStack>
<Button icon="Share" menu={buttonMenu}>
<Button icon="Share" menu={buttonMenu} menuType={ContextualMenu}>
<Stack padding="8px 0" as="span" horizontalAlign="start">
<Text>I am a compound multiline button.</Text>
<Text variant="small">I can have a caption.</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Button, IButtonProps } from '@uifabric/experiments';
import { createTheme, mergeStyles, Stack } from 'office-ui-fabric-react';
import { createTheme, mergeStyles, Stack, ContextualMenu } from 'office-ui-fabric-react';

const testTheme = createTheme({
semanticColors: {
Expand Down Expand Up @@ -123,6 +123,7 @@ export class ButtonStylesExample extends React.Component<{}, {}> {
content="All Classnames"
icon="PeopleAdd"
menu={buttonMenu}
menuType={ContextualMenu}
styles={{
root: 'root-classname',
stack: 'stack-classname',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Button, IButtonProps } from '@uifabric/experiments';
import { createTheme, Spinner, Stack } from 'office-ui-fabric-react';
import { createTheme, ContextualMenu, Spinner, Stack } from 'office-ui-fabric-react';

const menuItems = [{ key: 'a', name: 'Item a' }, { key: 'b', name: 'Item b' }];
const buttonMenu: IButtonProps['menu'] = render => render((MenuType, props) => <MenuType {...props} items={menuItems} />);
Expand Down Expand Up @@ -53,6 +53,7 @@ export class ButtonTokensExample extends React.Component<{}, {}> {
icon={render => render((IconType, iconProps) => <IconType {...iconProps} iconName="upload" />)}
content="Menu button with icon"
menu={buttonMenu}
menuType={ContextualMenu}
/>
<Stack horizontal disableShrink verticalAlign="center" gap={8}>
<Button
Expand Down
1 change: 1 addition & 0 deletions packages/experiments/src/components/Button/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Button';
export * from './Button.types';
export * from './ButtonMenu/ButtonMenu.types';