Skip to content

Commit

Permalink
Support tighter overrides for Contextual Menu Items (#27907)
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasMichon authored Sep 29, 2023
1 parent f1e96a1 commit d3ea7d9
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add contextualMenuItemAs and contextualMenuItemWrapperAs to ContextualMenuItem",
"packageName": "@fluentui/react",
"email": "tmichon@microsoft.com",
"dependentChangeType": "patch"
}
7 changes: 5 additions & 2 deletions packages/react/etc/react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4098,6 +4098,9 @@ export interface IContextualMenuItem {
checked?: boolean;
className?: string;
componentRef?: IRefObject<IContextualMenuRenderItem>;
contextualMenuItemAs?: IComponentAs<IContextualMenuItemProps>;
// Warning: (ae-forgotten-export) The symbol "IContextualMenuItemWrapperProps" needs to be exported by the entry point index.d.ts
contextualMenuItemWrapperAs?: IComponentAs<IContextualMenuItemWrapperProps>;
customOnRenderListLength?: number;
data?: any;
disabled?: boolean;
Expand Down Expand Up @@ -4246,14 +4249,14 @@ export interface IContextualMenuProps extends IBaseProps<IContextualMenu>, React
className?: string;
// @deprecated
componentRef?: IRefObject<IContextualMenu>;
contextualMenuItemAs?: React_2.ComponentClass<IContextualMenuItemProps> | React_2.FunctionComponent<IContextualMenuItemProps>;
contextualMenuItemAs?: IComponentAs<IContextualMenuItemProps>;
coverTarget?: boolean;
delayUpdateFocusOnHover?: boolean;
directionalHint?: DirectionalHint;
directionalHintFixed?: boolean;
directionalHintForRTL?: DirectionalHint;
doNotLayer?: boolean;
focusZoneAs?: React_2.ComponentClass<IFocusZoneProps> | React_2.FunctionComponent<IFocusZoneProps>;
focusZoneAs?: IComponentAs<IFocusZoneProps>;
focusZoneProps?: IFocusZoneProps;
gapSpace?: number;
// @deprecated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
getPropsWithDefaults,
getDocument,
FocusRects,
IComponentAs,
composeComponentAs,
} from '../../Utilities';
import { hasSubmenu, getIsChecked, isItemDisabled } from '../../utilities/contextualMenu/index';
import { Callout } from '../../Callout';
Expand All @@ -29,6 +31,7 @@ import {
ContextualMenuSplitButton,
ContextualMenuButton,
ContextualMenuAnchor,
IContextualMenuItemWrapperProps,
} from './ContextualMenuItemWrapper/index';
import { concatStyleSetsWithProps } from '../../Styling';
import { getItemStyles } from './ContextualMenu.classNames';
Expand Down Expand Up @@ -56,7 +59,11 @@ import type { IMenuItemClassNames, IContextualMenuClassNames } from './Contextua
import type { IRenderFunction, IStyleFunctionOrObject } from '../../Utilities';
import type { ICalloutContentStyleProps, ICalloutContentStyles } from '../../Callout';
import type { IProcessedStyleSet } from '../../Styling';
import type { IContextualMenuItemStyleProps, IContextualMenuItemStyles } from './ContextualMenuItem.types';
import type {
IContextualMenuItemProps,
IContextualMenuItemStyleProps,
IContextualMenuItemStyles,
} from './ContextualMenuItem.types';
import type { IPopupRestoreFocusParams } from '../../Popup';

const getClassNames = classNamesFunction<IContextualMenuStyleProps, IContextualMenuStyles>();
Expand Down Expand Up @@ -1095,12 +1102,27 @@ export const ContextualMenuBase: React.FunctionComponent<IContextualMenuProps> =
} as const;

if (item.href) {
return <ContextualMenuAnchor {...commonProps} onItemClick={onAnchorClick} />;
let ContextualMenuAnchorAs: IComponentAs<IContextualMenuItemWrapperProps> = ContextualMenuAnchor;

if (item.contextualMenuItemWrapperAs) {
ContextualMenuAnchorAs = composeComponentAs(item.contextualMenuItemWrapperAs, ContextualMenuAnchorAs);
}

return <ContextualMenuAnchorAs {...commonProps} onItemClick={onAnchorClick} />;
}

if (item.split && hasSubmenu(item)) {
let ContextualMenuSplitButtonAs: IComponentAs<IContextualMenuItemWrapperProps> = ContextualMenuSplitButton;

if (item.contextualMenuItemWrapperAs) {
ContextualMenuSplitButtonAs = composeComponentAs(
item.contextualMenuItemWrapperAs,
ContextualMenuSplitButtonAs,
);
}

return (
<ContextualMenuSplitButton
<ContextualMenuSplitButtonAs
{...commonProps}
onItemClick={onItemClick}
onItemClickBase={onItemClickBase}
Expand All @@ -1109,7 +1131,13 @@ export const ContextualMenuBase: React.FunctionComponent<IContextualMenuProps> =
);
}

return <ContextualMenuButton {...commonProps} onItemClick={onItemClick} onItemClickBase={onItemClickBase} />;
let ContextualMenuButtonAs: IComponentAs<IContextualMenuItemWrapperProps> = ContextualMenuButton;

if (item.contextualMenuItemWrapperAs) {
ContextualMenuButtonAs = composeComponentAs(item.contextualMenuItemWrapperAs, ContextualMenuButtonAs);
}

return <ContextualMenuButtonAs {...commonProps} onItemClick={onItemClick} onItemClickBase={onItemClickBase} />;
};

const renderHeaderMenuItem = (
Expand All @@ -1122,7 +1150,16 @@ export const ContextualMenuBase: React.FunctionComponent<IContextualMenuProps> =
hasCheckmarks: boolean,
hasIcons: boolean,
): React.ReactNode => {
const { contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem } = props;
let ChildrenRenderer: IComponentAs<IContextualMenuItemProps> = ContextualMenuItem;

if (item.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(item.contextualMenuItemAs, ChildrenRenderer);
}

if (props.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(props.contextualMenuItemAs, ChildrenRenderer);
}

const { itemProps, id } = item;
const divHtmlProperties =
itemProps && getNativeProps<React.HTMLAttributes<HTMLDivElement>>(itemProps, divProperties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import type { IIconProps } from '../../Icon';
import type { ICalloutProps, ICalloutContentStyleProps } from '../../Callout';
import type { ITheme, IStyle } from '../../Styling';
import type { IButtonStyles } from '../../Button';
import type { IRefObject, IBaseProps, IRectangle, IRenderFunction, IStyleFunctionOrObject } from '../../Utilities';
import type {
IRefObject,
IBaseProps,
IRectangle,
IRenderFunction,
IStyleFunctionOrObject,
IComponentAs,
} from '../../Utilities';
import type { IWithResponsiveModeState } from '../../ResponsiveMode';
import type { IContextualMenuClassNames, IMenuItemClassNames } from './ContextualMenu.classNames';
import type { IVerticalDividerClassNames } from '../Divider/VerticalDivider.types';
Expand All @@ -18,6 +25,7 @@ import type {
import type { IKeytipProps } from '../../Keytip';
import type { Target } from '@fluentui/react-hooks';
import type { IPopupRestoreFocusParams } from '../../Popup';
import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper/ContextualMenuItemWrapper.types';

export { DirectionalHint } from '../../common/DirectionalHint';

Expand Down Expand Up @@ -250,9 +258,7 @@ export interface IContextualMenuProps
* Custom component to use for rendering individual menu items.
* @defaultvalue ContextualMenuItem
*/
contextualMenuItemAs?:
| React.ComponentClass<IContextualMenuItemProps>
| React.FunctionComponent<IContextualMenuItemProps>;
contextualMenuItemAs?: IComponentAs<IContextualMenuItemProps>;

/**
* Props to pass down to the FocusZone.
Expand All @@ -266,7 +272,7 @@ export interface IContextualMenuProps
* Custom component to use for rendering the focus zone (the root).
* @defaultValue FocusZone
*/
focusZoneAs?: React.ComponentClass<IFocusZoneProps> | React.FunctionComponent<IFocusZoneProps>;
focusZoneAs?: IComponentAs<IFocusZoneProps>;

/**
* If true, renders the ContextualMenu in a hidden state.
Expand Down Expand Up @@ -516,6 +522,16 @@ export interface IContextualMenuItem {
*/
onRender?: (item: any, dismissMenu: (ev?: any, dismissAll?: boolean) => void) => React.ReactNode;

/**
* An override for the child content of the contextual menu item.
*/
contextualMenuItemAs?: IComponentAs<IContextualMenuItemProps>;

/**
* An override for the entire component used to render the contextal menu item.
*/
contextualMenuItemWrapperAs?: IComponentAs<IContextualMenuItemWrapperProps>;

/**
* Method to customize sub-components rendering of this menu item.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import * as React from 'react';
import { anchorProperties, getNativeProps, memoizeFunction, getId, mergeAriaAttributeValues } from '../../../Utilities';
import {
anchorProperties,
getNativeProps,
memoizeFunction,
getId,
mergeAriaAttributeValues,
IComponentAs,
composeComponentAs,
} from '../../../Utilities';
import { ContextualMenuItemWrapper } from './ContextualMenuItemWrapper';
import { KeytipData } from '../../../KeytipData';
import { isItemDisabled, hasSubmenu } from '../../../utilities/contextualMenu/index';
import { ContextualMenuItem } from '../ContextualMenuItem';
import type { IKeytipDataProps } from '../../../KeytipData';
import type { IKeytipProps } from '../../../Keytip';
import { IContextualMenuItemProps } from '../ContextualMenuItem.types';

export class ContextualMenuAnchor extends ContextualMenuItemWrapper {
private _anchor = React.createRef<HTMLAnchorElement>();
Expand All @@ -27,14 +36,23 @@ export class ContextualMenuAnchor extends ContextualMenuItemWrapper {
totalItemCount,
hasCheckmarks,
hasIcons,
contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem,
expandedMenuItemKey,
onItemClick,
openSubMenu,
dismissSubMenu,
dismissMenu,
} = this.props;

let ChildrenRenderer: IComponentAs<IContextualMenuItemProps> = ContextualMenuItem;

if (this.props.item.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(this.props.item.contextualMenuItemAs, ChildrenRenderer);
}

if (this.props.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(this.props.contextualMenuItemAs, ChildrenRenderer);
}

let anchorRel = item.rel;
if (item.target && item.target.toLowerCase() === '_blank') {
anchorRel = anchorRel ? anchorRel : 'nofollow noopener noreferrer'; // Safe default to prevent tabjacking
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import * as React from 'react';
import { buttonProperties, getNativeProps, memoizeFunction, getId, mergeAriaAttributeValues } from '../../../Utilities';
import {
buttonProperties,
getNativeProps,
memoizeFunction,
getId,
mergeAriaAttributeValues,
IComponentAs,
composeComponentAs,
} from '../../../Utilities';
import { ContextualMenuItemWrapper } from './ContextualMenuItemWrapper';
import { KeytipData } from '../../../KeytipData';
import { getIsChecked, isItemDisabled, hasSubmenu, getMenuItemAriaRole } from '../../../utilities/contextualMenu/index';
import { ContextualMenuItem } from '../ContextualMenuItem';
import type { IKeytipDataProps } from '../../../KeytipData';
import type { IKeytipProps } from '../../../Keytip';
import { IContextualMenuItemProps } from '../ContextualMenuItem.types';

export class ContextualMenuButton extends ContextualMenuItemWrapper {
private _btn = React.createRef<HTMLButtonElement>();
Expand All @@ -27,7 +36,7 @@ export class ContextualMenuButton extends ContextualMenuItemWrapper {
totalItemCount,
hasCheckmarks,
hasIcons,
contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem,
contextualMenuItemAs,
expandedMenuItemKey,
onItemMouseDown,
onItemClick,
Expand All @@ -36,6 +45,16 @@ export class ContextualMenuButton extends ContextualMenuItemWrapper {
dismissMenu,
} = this.props;

let ChildrenRenderer: IComponentAs<IContextualMenuItemProps> = ContextualMenuItem;

if (item.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(item.contextualMenuItemAs, ChildrenRenderer);
}

if (contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(contextualMenuItemAs, ChildrenRenderer);
}

const isChecked: boolean | null | undefined = getIsChecked(item);
const canCheck: boolean = isChecked !== null;
const defaultRole = getMenuItemAriaRole(item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { ContextualMenuItemWrapper } from './ContextualMenuItemWrapper';
import type { IContextualMenuItem, IContextualMenuItemProps } from '../../../ContextualMenu';
import type { IMenuItemClassNames } from '../ContextualMenu.classNames';
import type { IRefObject } from '../../../Utilities';
import type { IComponentAs, IRefObject } from '../../../Utilities';

export interface IContextualMenuItemWrapperProps extends React.ClassAttributes<IContextualMenuItem> {
/**
Expand Down Expand Up @@ -51,9 +51,7 @@ export interface IContextualMenuItemWrapperProps extends React.ClassAttributes<I
* Method to override the render of the individual menu items.
* @defaultvalue ContextualMenuItem
*/
contextualMenuItemAs?:
| React.ComponentClass<IContextualMenuItemProps>
| React.FunctionComponent<IContextualMenuItemProps>;
contextualMenuItemAs?: IComponentAs<IContextualMenuItemProps>;

/**
* Callback for when the user's mouse enters the wrapper.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
Async,
EventGroup,
getId,
composeComponentAs,
IComponentAs,
} from '../../../Utilities';
import { ContextualMenuItem } from '../ContextualMenuItem';
import { getSplitButtonVerticalDividerClassNames } from '../ContextualMenu.classNames';
Expand All @@ -19,6 +21,7 @@ import type { IContextualMenuItem } from '../ContextualMenu.types';
import type { IMenuItemClassNames } from '../ContextualMenu.classNames';
import type { IKeytipProps } from '../../../Keytip';
import type { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types';
import { IContextualMenuItemProps } from '../ContextualMenuItem.types';

export interface IContextualMenuSplitButtonState {}

Expand Down Expand Up @@ -214,14 +217,17 @@ export class ContextualMenuSplitButton extends ContextualMenuItemWrapper {
index: number,
keytipAttributes: any,
) {
const {
contextualMenuItemAs: ChildrenRenderer = ContextualMenuItem,
onItemMouseLeave,
onItemMouseDown,
openSubMenu,
dismissSubMenu,
dismissMenu,
} = this.props;
const { onItemMouseLeave, onItemMouseDown, openSubMenu, dismissSubMenu, dismissMenu } = this.props;

let ChildrenRenderer: IComponentAs<IContextualMenuItemProps> = ContextualMenuItem;

if (this.props.item.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(this.props.item.contextualMenuItemAs, ChildrenRenderer);
}

if (this.props.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(this.props.contextualMenuItemAs, ChildrenRenderer);
}

const itemProps: IContextualMenuItem = {
onClick: this._onIconItemClick,
Expand Down

0 comments on commit d3ea7d9

Please sign in to comment.