Skip to content

Commit

Permalink
feat: Implement MenuItemLink components (#28369)
Browse files Browse the repository at this point in the history
* feat: Implement MenuItemLink components

Implements the MenuItemLink component which is similar to the MenuItem
component with a different root slot and a required `href` prop.

* add best practice

* changefile

* update snapshot

* revert

* Update packages/react-components/react-menu/src/components/MenuItemLink/useMenuItemLinkStyles.styles.ts

Co-authored-by: Oleksandr Fediashov <[email protected]>

* Update packages/react-components/react-menu/src/components/MenuItemLink/MenuItemLink.tsx

Co-authored-by: Oleksandr Fediashov <[email protected]>

* fix formatting

---------

Co-authored-by: Oleksandr Fediashov <[email protected]>
  • Loading branch information
ling1726 and layershifter committed Jun 30, 2023
1 parent a542154 commit 0cbd837
Show file tree
Hide file tree
Showing 19 changed files with 345 additions and 0 deletions.
29 changes: 29 additions & 0 deletions apps/vr-tests-react-components/src/stories/Menu/Menu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
MenuGroupHeader,
MenuDivider,
MenuSplitGroup,
MenuItemLink,
} from '@fluentui/react-menu';
import { CutRegular, EditRegular, ClipboardPasteRegular } from '@fluentui/react-icons';

Expand Down Expand Up @@ -40,6 +41,34 @@ storiesOf('Menu Converged - basic', module)
{ includeRtl: true },
);

storiesOf('Menu Converged - MenuItemLinks', module)
.addDecorator(story => (
<StoryWright steps={new Steps().hover('[role="menuitem"]').snapshot('hover menuitemlink').end()}>
{story()}
</StoryWright>
))
.addStory('default', () => (
<Menu open>
<MenuTrigger>
<button>Toggle menu</button>
</MenuTrigger>

<MenuPopover>
<MenuList>
<MenuItemLink href="#" icon={<CutRegular />}>
Cut
</MenuItemLink>
<MenuItemLink href="#" icon={<EditRegular />}>
Edit
</MenuItemLink>
<MenuItemLink href="#" icon={<ClipboardPasteRegular />}>
Paste
</MenuItemLink>
</MenuList>
</MenuPopover>
</Menu>
));

storiesOf('Menu Converged - secondary content', module)
.addDecorator(story => (
<StoryWright steps={new Steps().hover('[role="menuitem"]').snapshot('hover menuitem').end()}>{story()}</StoryWright>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Implement MenuItemLink components",
"packageName": "@fluentui/react-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Implement MenuItemLink components",
"packageName": "@fluentui/react-menu",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ import { menuItemCheckboxClassNames } from '@fluentui/react-menu';
import { MenuItemCheckboxProps } from '@fluentui/react-menu';
import { MenuItemCheckboxState } from '@fluentui/react-menu';
import { menuItemClassNames } from '@fluentui/react-menu';
import { MenuItemLink } from '@fluentui/react-menu';
import { menuItemLinkClassNames } from '@fluentui/react-menu';
import { MenuItemLinkProps } from '@fluentui/react-menu';
import { MenuItemLinkSlots } from '@fluentui/react-menu';
import { MenuItemLinkState } from '@fluentui/react-menu';
import { MenuItemProps } from '@fluentui/react-menu';
import { MenuItemRadio } from '@fluentui/react-menu';
import { menuItemRadioClassNames } from '@fluentui/react-menu';
Expand Down Expand Up @@ -519,6 +524,7 @@ import { renderMenuGroup_unstable } from '@fluentui/react-menu';
import { renderMenuGroupHeader_unstable } from '@fluentui/react-menu';
import { renderMenuItem_unstable } from '@fluentui/react-menu';
import { renderMenuItemCheckbox_unstable } from '@fluentui/react-menu';
import { renderMenuItemLink_unstable } from '@fluentui/react-menu';
import { renderMenuItemRadio_unstable } from '@fluentui/react-menu';
import { renderMenuList_unstable } from '@fluentui/react-menu';
import { renderMenuPopover_unstable } from '@fluentui/react-menu';
Expand Down Expand Up @@ -907,6 +913,8 @@ import { useMenuGroupStyles_unstable } from '@fluentui/react-menu';
import { useMenuItem_unstable } from '@fluentui/react-menu';
import { useMenuItemCheckbox_unstable } from '@fluentui/react-menu';
import { useMenuItemCheckboxStyles_unstable } from '@fluentui/react-menu';
import { useMenuItemLink_unstable } from '@fluentui/react-menu';
import { useMenuItemLinkStyles_unstable } from '@fluentui/react-menu';
import { useMenuItemRadio_unstable } from '@fluentui/react-menu';
import { useMenuItemRadioStyles_unstable } from '@fluentui/react-menu';
import { useMenuItemStyles_unstable } from '@fluentui/react-menu';
Expand Down Expand Up @@ -1730,6 +1738,16 @@ export { MenuItemCheckboxState }

export { menuItemClassNames }

export { MenuItemLink }

export { menuItemLinkClassNames }

export { MenuItemLinkProps }

export { MenuItemLinkSlots }

export { MenuItemLinkState }

export { MenuItemProps }

export { MenuItemRadio }
Expand Down Expand Up @@ -2058,6 +2076,8 @@ export { renderMenuItem_unstable }

export { renderMenuItemCheckbox_unstable }

export { renderMenuItemLink_unstable }

export { renderMenuItemRadio_unstable }

export { renderMenuList_unstable }
Expand Down Expand Up @@ -2834,6 +2854,10 @@ export { useMenuItemCheckbox_unstable }

export { useMenuItemCheckboxStyles_unstable }

export { useMenuItemLink_unstable }

export { useMenuItemLinkStyles_unstable }

export { useMenuItemRadio_unstable }

export { useMenuItemRadioStyles_unstable }
Expand Down
8 changes: 8 additions & 0 deletions packages/react-components/react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ export {
MenuGroupContextProvider,
MenuGroupHeader,
MenuItem,
MenuItemLink,
MenuItemCheckbox,
MenuItemRadio,
MenuList,
Expand All @@ -418,6 +419,7 @@ export {
menuGroupHeaderClassNames,
menuItemCheckboxClassNames,
menuItemClassNames,
menuItemLinkClassNames,
menuItemRadioClassNames,
menuListClassNames,
menuPopoverClassNames,
Expand All @@ -427,6 +429,7 @@ export {
renderMenuGroup_unstable,
renderMenuGroupHeader_unstable,
renderMenuItem_unstable,
renderMenuItemLink_unstable,
renderMenuItemCheckbox_unstable,
renderMenuItemRadio_unstable,
renderMenuList_unstable,
Expand All @@ -446,11 +449,13 @@ export {
useMenuGroupHeaderStyles_unstable,
useMenuGroupStyles_unstable,
useMenuItem_unstable,
useMenuItemLink_unstable,
useMenuItemCheckbox_unstable,
useMenuItemCheckboxStyles_unstable,
useMenuItemRadio_unstable,
useMenuItemRadioStyles_unstable,
useMenuItemStyles_unstable,
useMenuItemLinkStyles_unstable,
useMenuList_unstable,
useMenuListContext_unstable,
useMenuListContextValues_unstable,
Expand Down Expand Up @@ -481,12 +486,15 @@ export type {
MenuItemCheckboxProps,
MenuItemCheckboxState,
MenuItemProps,
MenuItemLinkProps,
MenuItemRadioProps,
MenuItemRadioState,
MenuItemSelectableProps,
MenuItemSelectableState,
MenuItemSlots,
MenuItemState,
MenuItemLinkSlots,
MenuItemLinkState,
MenuListContextValue,
MenuListContextValues,
MenuListProps,
Expand Down
28 changes: 28 additions & 0 deletions packages/react-components/react-menu/etc/react-menu.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ export type MenuItemCheckboxState = MenuItemState & MenuItemSelectableState;
// @public (undocumented)
export const menuItemClassNames: SlotClassNames<MenuItemSlots>;

// @public
export const MenuItemLink: ForwardRefComponent<MenuItemLinkProps>;

// @public (undocumented)
export const menuItemLinkClassNames: SlotClassNames<MenuItemLinkSlots>;

// @public
export type MenuItemLinkProps = ComponentProps<MenuItemLinkSlots> & Pick<MenuItemProps, 'disabled'> & {
href: string;
};

// @public (undocumented)
export type MenuItemLinkSlots = {
root: Slot<'a'>;
} & Pick<MenuItemSlots, 'icon' | 'content' | 'secondaryContent' | 'checkmark'>;

// @public
export type MenuItemLinkState = ComponentState<MenuItemLinkSlots>;

// @public (undocumented)
export type MenuItemProps = ComponentProps<Partial<MenuItemSlots>> & {
hasSubmenu?: boolean;
Expand Down Expand Up @@ -385,6 +404,9 @@ export const renderMenuItem_unstable: (state: MenuItemState) => JSX.Element;
// @public
export const renderMenuItemCheckbox_unstable: (state: MenuItemCheckboxState) => JSX.Element;

// @public
export const renderMenuItemLink_unstable: (state: MenuItemLinkState) => JSX.Element;

// @public
export const renderMenuItemRadio_unstable: (state: MenuItemRadioState) => JSX.Element;

Expand Down Expand Up @@ -451,6 +473,12 @@ export const useMenuItemCheckbox_unstable: (props: MenuItemCheckboxProps, ref: R
// @public (undocumented)
export const useMenuItemCheckboxStyles_unstable: (state: MenuItemCheckboxState) => void;

// @public
export const useMenuItemLink_unstable: (props: MenuItemLinkProps, ref: React_2.Ref<HTMLAnchorElement>) => MenuItemLinkState;

// @public
export const useMenuItemLinkStyles_unstable: (state: MenuItemLinkState) => MenuItemLinkState;

// @public
export const useMenuItemRadio_unstable: (props: MenuItemRadioProps, ref: React_2.Ref<ARIAButtonElement<'div'>>) => MenuItemRadioState;

Expand Down
1 change: 1 addition & 0 deletions packages/react-components/react-menu/src/MenuItemLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './components/MenuItemLink/index';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { MenuItemLink } from './MenuItemLink';
import { isConformant } from '../../testing/isConformant';

describe('MenuItemLink', () => {
isConformant({
Component: MenuItemLink,
displayName: 'MenuItemLink',
testOptions: {
'has-static-classnames': [
{
props: {
icon: 'Test Icon',
checkmark: 'Test Checkmark',
submenuIndicator: 'Test Submenu Indicator',
content: 'Test Content',
secondaryContent: 'Test Secondary Content',
},
},
],
},
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<MenuItemLink href="#">Default MenuItemLink</MenuItemLink>);
expect(result.container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';
import { useMenuItemLink_unstable } from './useMenuItemLink';
import { renderMenuItemLink_unstable } from './renderMenuItemLink';
import { useMenuItemLinkStyles_unstable } from './useMenuItemLinkStyles.styles';
import type { MenuItemLinkProps } from './MenuItemLink.types';
import type { ForwardRefComponent } from '@fluentui/react-utilities';

/**
* MenuItemLink component
*/
export const MenuItemLink: ForwardRefComponent<MenuItemLinkProps> = React.forwardRef((props, ref) => {
const state = useMenuItemLink_unstable(props, ref);

useMenuItemLinkStyles_unstable(state);
return renderMenuItemLink_unstable(state);
});

MenuItemLink.displayName = 'MenuItemLink';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import { MenuItemProps, MenuItemSlots } from '../MenuItem/MenuItem.types';

export type MenuItemLinkSlots = {
root: Slot<'a'>;
} & Pick<MenuItemSlots, 'icon' | 'content' | 'secondaryContent' | 'checkmark'>;

/**
* MenuItemLink Props
*/
export type MenuItemLinkProps = ComponentProps<MenuItemLinkSlots> & Pick<MenuItemProps, 'disabled'> & { href: string };

/**
* State used in rendering MenuItemLink
*/
export type MenuItemLinkState = ComponentState<MenuItemLinkSlots>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MenuItemLink renders a default state 1`] = `
<div>
<a
class="fui-MenuItemLink fui-MenuItem"
href="#"
role="menuitem"
>
<span
class="fui-MenuItemLink__content fui-MenuItem__content"
>
Default MenuItemLink
</span>
</a>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './MenuItemLink';
export * from './MenuItemLink.types';
export * from './renderMenuItemLink';
export * from './useMenuItemLink';
export * from './useMenuItemLinkStyles.styles';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** @jsxRuntime classic */
/** @jsx createElement */

import { createElement } from '@fluentui/react-jsx-runtime';
import { getSlotsNext } from '@fluentui/react-utilities';
import type { MenuItemLinkState, MenuItemLinkSlots } from './MenuItemLink.types';

/**
* Render the final JSX of MenuItemLink
*/
export const renderMenuItemLink_unstable = (state: MenuItemLinkState) => {
const { slots, slotProps } = getSlotsNext<MenuItemLinkSlots>(state);

// TODO Add additional slots in the appropriate place
return (
<slots.root {...slotProps.root}>
{slots.checkmark && <slots.checkmark {...slotProps.checkmark} />}
{slots.icon && <slots.icon {...slotProps.icon} />}
{slots.content && <slots.content {...slotProps.content} />}
{slots.secondaryContent && <slots.secondaryContent {...slotProps.secondaryContent} />}
</slots.root>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';
import { getNativeElementProps } from '@fluentui/react-utilities';
import type { MenuItemLinkProps, MenuItemLinkState } from './MenuItemLink.types';
import { useMenuItem_unstable } from '../MenuItem/useMenuItem';
import { MenuItemProps } from '../MenuItem/MenuItem.types';

/**
* Create the state required to render MenuItemLink.
*
* The returned state can be modified with hooks such as useMenuItemLinkStyles_unstable,
* before being passed to renderMenuItemLink_unstable.
*
* @param props - props from this instance of MenuItemLink
* @param ref - reference to root HTMLElement of MenuItemLink
*/
export const useMenuItemLink_unstable = (
props: MenuItemLinkProps,
ref: React.Ref<HTMLAnchorElement>,
): MenuItemLinkState => {
// casting because the root slot changes from div to a
const baseState = useMenuItem_unstable(props as MenuItemProps, null);
return {
...baseState,
components: {
...baseState.components,
root: 'a',
},
root: getNativeElementProps('a', {
ref,
role: 'menuitem',
...props,
}),
};
};
Loading

0 comments on commit 0cbd837

Please sign in to comment.