Skip to content

Commit

Permalink
Basic MenuList with items (#16865)
Browse files Browse the repository at this point in the history
* Auto-generate react-menu

* use correct react-menu version

* Update packages/react-menu/package.json

Co-authored-by: Oleksandr Fediashov <alexander.mcgarret@gmail.com>

* update files

* remove unnecessary

* modified codeowners

* update codeowners

* modify codeowners

* Change files

* add react-hooks dep

* revert unnecessary files

* fix deps

* remove unnecessary comments

* type ref

* use state for selectors

* add mising type

* remove cast

* remove readme

* remove default div

* remove default div

* fix story type

* follow package structure

* add export

* remove placeholder

* revert package.json

* type refs

* Change files

* fix deps

* move conformance to devdep

* update api

* fixes to types

* remove coment

* fix imports

* update api

* update API

* fix types

* fix conformance tests

* fix exports

Co-authored-by: Oleksandr Fediashov <alexander.mcgarret@gmail.com>
Co-authored-by: Elizabeth Craig <elcraig@microsoft.com>
  • Loading branch information
3 people authored Feb 8, 2021
1 parent e6a1c49 commit fe4959d
Show file tree
Hide file tree
Showing 31 changed files with 465 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Implement basic MenuList example",
"packageName": "@fluentui/react-examples",
"email": "lingfan.gao@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add MenuList and MenuItem components",
"packageName": "@fluentui/react-menu",
"email": "lingfan.gao@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add ObjectSlot type",
"packageName": "@fluentui/react-utils",
"email": "lingfan.gao@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';

import { MenuList, MenuItem } from '@fluentui/react-menu';
import { teamsLightTheme } from '@fluentui/react-theme';
import { FluentProvider } from '@fluentui/react-provider';
import { CutIcon, PasteIcon, EditIcon } from '@fluentui/react-icons-mdl2';
import { makeStyles } from '@fluentui/react-make-styles';

const useContainerStyles = makeStyles([
// This should eventually be the popup container styles
[
null,
theme => ({
backgroundColor: theme.alias.color.neutral.neutralBackground1,
minWidth: '128px',
minHeight: '48px',
maxWidth: '128px',
boxShadow: `${theme.alias.shadow.shadow16}`,
paddingTop: '4px',
paddingBottom: '4px',
}),
],
]);
const Container: React.FC = props => {
const classNames = useContainerStyles({});
return <div className={classNames}>{props.children}</div>;
};

export const MenuListExample = () => (
<FluentProvider theme={teamsLightTheme}>
<Container>
<MenuList>
<MenuItem>Item</MenuItem>
<MenuItem>Item</MenuItem>
<MenuItem>Item</MenuItem>
</MenuList>
</Container>
</FluentProvider>
);

export const MenuListWithIconsExample = () => (
<FluentProvider theme={teamsLightTheme}>
<Container>
<MenuList>
<MenuItem icon={<CutIcon />}>Item</MenuItem>
<MenuItem icon={<PasteIcon />}>Item</MenuItem>
<MenuItem icon={<EditIcon />}>Item</MenuItem>
</MenuList>
</Container>
</FluentProvider>
);
1 change: 0 additions & 1 deletion packages/react-examples/src/react-menu/index.ts

This file was deleted.

2 changes: 0 additions & 2 deletions packages/react-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@
**React Menu components for [Fluent UI React](https://developer.microsoft.com/en-us/fluentui)**

These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release.

To import React Menu components:
55 changes: 55 additions & 0 deletions packages/react-menu/etc/react-menu.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,61 @@
```ts

import { ComponentProps } from '@fluentui/react-utils';
import { ObjectShorthandProps } from '@fluentui/react-utils';
import * as React from 'react';
import { ShorthandProps } from '@fluentui/react-utils';

// @public
export const MenuItem: React.ForwardRefExoticComponent<Pick<MenuItemProps, React.ReactText> & React.RefAttributes<HTMLElement>>;

// @public (undocumented)
export interface MenuItemProps extends ComponentProps, React.HTMLAttributes<HTMLElement> {
icon?: ShorthandProps;
}

// @public
export const menuItemShorthandProps: string[];

// @public (undocumented)
export interface MenuItemState extends MenuItemProps {
icon?: ObjectShorthandProps<HTMLSpanElement>;
ref: React.MutableRefObject<HTMLElement>;
}

// @public
export const MenuList: React.ForwardRefExoticComponent<Pick<MenuListProps, React.ReactText> & React.RefAttributes<HTMLElement>>;

// @public (undocumented)
export interface MenuListProps extends ComponentProps, React.HTMLAttributes<HTMLElement> {
}

// @public (undocumented)
export interface MenuListState extends MenuListProps {
ref: React.MutableRefObject<HTMLElement>;
}

// @public
export const renderMenuItem: (state: MenuItemState) => JSX.Element;

// @public
export const renderMenuList: (state: MenuListState) => JSX.Element;

// @public
export const useIconStyles: (selectors: MenuItemState) => string;

// @public
export const useMenuItem: (props: MenuItemProps, ref: React.Ref<HTMLElement>, defaultProps?: MenuItemProps | undefined) => MenuItemState;

// @public
export const useMenuItemStyles: (state: MenuItemState) => void;

// @public
export const useMenuList: (props: MenuListProps, ref: React.Ref<HTMLElement>, defaultProps?: MenuListProps | undefined) => MenuListState;

// @public
export const useRootStyles: (selectors: MenuItemState) => string;


// (No @packageDocumentation comment for this package)

Expand Down
9 changes: 6 additions & 3 deletions packages/react-menu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@
"update-snapshots": "just-scripts jest -u"
},
"devDependencies": {
"@fluentui/react-conformance": "^1.0.0",
"@fluentui/eslint-plugin": "^1.0.0-beta.1",
"@fluentui/scripts": "^1.0.0",
"@types/enzyme": "3.10.3",
"@types/enzyme-adapter-react-16": "1.0.3",
"@types/jest": "~24.9.0",
"@types/react": "16.9.42",
"@types/react-dom": "16.9.10",
"@types/react-test-renderer": "^16.0.0",
"@types/webpack-env": "1.16.0",
"@fluentui/scripts": "^1.0.0",
"enzyme": "~3.10.0",
"enzyme-adapter-react-16": "^1.15.0",
"react": "16.8.6",
Expand All @@ -43,9 +44,11 @@
"react-test-renderer": "^16.3.0"
},
"dependencies": {
"@fluentui/theme": "^2.0.0-beta.13",
"@fluentui/react-compose": "^1.0.0-beta.11",
"@fluentui/react-hooks": "^8.0.0-beta.10",
"@fluentui/react-make-styles": "^0.2.3",
"@fluentui/react-theme": "^0.3.0",
"@fluentui/react-theme-provider": "^1.0.0-beta.20",
"@fluentui/react-utils": "^0.2.0",
"@fluentui/set-version": "^8.0.0-beta.1",
"tslib": "^1.10.0"
},
Expand Down
1 change: 1 addition & 0 deletions packages/react-menu/src/MenuItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './components/MenuItem/index';
1 change: 1 addition & 0 deletions packages/react-menu/src/MenuList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './components/MenuList/index';
10 changes: 10 additions & 0 deletions packages/react-menu/src/common/isConformant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isConformant as baseIsConformant, IsConformantOptions } from '@fluentui/react-conformance';

export function isConformant(testInfo: Omit<IsConformantOptions, 'componentPath'>) {
const defaultOptions = {
disabledTests: ['has-docblock'],
componentPath: module!.parent!.filename.replace('.test', ''),
};

baseIsConformant(defaultOptions, testInfo);
}
30 changes: 30 additions & 0 deletions packages/react-menu/src/components/MenuItem/MenuItem.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import { MenuItem } from './MenuItem';
import * as renderer from 'react-test-renderer';
import { ReactWrapper } from 'enzyme';
import { isConformant } from '../../common/isConformant';

describe('MenuItem', () => {
isConformant({
Component: MenuItem,
displayName: 'MenuItem',
});

let wrapper: ReactWrapper | undefined;

afterEach(() => {
if (wrapper) {
wrapper.unmount();
wrapper = undefined;
}
});

/**
* Note: see more visual regression tests for MenuItem in /apps/vr-tests.
*/
it('renders a default state', () => {
const component = renderer.create(<MenuItem>Default MenuItem</MenuItem>);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
21 changes: 21 additions & 0 deletions packages/react-menu/src/components/MenuItem/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';
import { useMenuItem } from './useMenuItem';
import { MenuItemProps } from './MenuItem.types';
import { renderMenuItem } from './renderMenuItem';
import { useMenuItemStyles } from './useMenuItemStyles';

/**
* Define a styled MenuItem, using the `useMenuItem` and `useMenuItemStyles` hook.
* {@docCategory MenuItem}
*/
export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>((props, ref) => {
const state = useMenuItem(props, ref, {
role: 'menuitem',
tabIndex: 0, // TODO keyboard navigation
});

useMenuItemStyles(state);
return renderMenuItem(state);
});

MenuItem.displayName = 'MenuItem';
20 changes: 20 additions & 0 deletions packages/react-menu/src/components/MenuItem/MenuItem.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import { ComponentProps, ShorthandProps, ObjectShorthandProps } from '@fluentui/react-utils';

export interface MenuItemProps extends ComponentProps, React.HTMLAttributes<HTMLElement> {
/**
* Icon slot rendered before children content
*/
icon?: ShorthandProps;
}

export interface MenuItemState extends MenuItemProps {
/**
* Ref to the root slot
*/
ref: React.MutableRefObject<HTMLElement>;
/**
* Icon slot when processed by internal state
*/
icon?: ObjectShorthandProps<HTMLSpanElement>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MenuItem renders a default state 1`] = `
<div
className=""
icon={
Object {
"as": "span",
"className": "",
}
}
role="menuitem"
tabIndex={0}
>
Default MenuItem
</div>
`;
5 changes: 5 additions & 0 deletions packages/react-menu/src/components/MenuItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './MenuItem';
export * from './MenuItem.types';
export * from './renderMenuItem';
export * from './useMenuItem';
export * from './useMenuItemStyles';
18 changes: 18 additions & 0 deletions packages/react-menu/src/components/MenuItem/renderMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';
import { getSlots } from '@fluentui/react-utils';
import { MenuItemState } from './MenuItem.types';
import { menuItemShorthandProps } from './useMenuItem';

/**
* Function that renders the final JSX of the component
*/
export const renderMenuItem = (state: MenuItemState) => {
const { slots, slotProps } = getSlots(state, menuItemShorthandProps);

return (
<slots.root {...slotProps.root}>
<slots.icon {...slotProps.icon} />
{state.children}
</slots.root>
);
};
31 changes: 31 additions & 0 deletions packages/react-menu/src/components/MenuItem/useMenuItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { makeMergeProps, resolveShorthandProps } from '@fluentui/react-utils';
import { useMergedRefs } from '@fluentui/react-hooks';
import { MenuItemProps, MenuItemState } from './MenuItem.types';

/**
* Consts listing which props are shorthand props.
*/
export const menuItemShorthandProps = ['icon'];

const mergeProps = makeMergeProps<MenuItemState>({ deepMerge: menuItemShorthandProps });

/**
* Returns the props and state required to render the component
*/
export const useMenuItem = (
props: MenuItemProps,
ref: React.Ref<HTMLElement>,
defaultProps?: MenuItemProps,
): MenuItemState => {
const state = mergeProps(
{
ref: useMergedRefs(ref, React.useRef(null)),
icon: { as: 'span' },
},
defaultProps,
resolveShorthandProps(props, menuItemShorthandProps),
);

return state;
};
55 changes: 55 additions & 0 deletions packages/react-menu/src/components/MenuItem/useMenuItemStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { makeStyles, ax } from '@fluentui/react-make-styles';
import { MenuItemState } from './MenuItem.types';

/**
* Styles for the root slot
*/
export const useRootStyles = makeStyles<MenuItemState>([
[
null,
theme => ({
color: theme.alias.color.neutral.neutralForeground1,
backgroundColor: theme.alias.color.neutral.neutralBackground1,
paddingRight: '12px',
paddingLeft: '12px',
height: '32px',
display: 'flex',
alignItems: 'center',
fontSize: theme.global.type.fontSizes.base[300],

':hover': {
backgroundColor: theme.alias.color.neutral.neutralBackground1Hover,
},

':focus': {
backgroundColor: theme.alias.color.neutral.neutralBackground1Hover,
},
}),
],
]);

/**
* Styles for the icon slot
*/
export const useIconStyles = makeStyles<MenuItemState>([
[
null,
() => ({
width: '20px',
height: '20px',
marginRight: '9px',
}),
],
]);

/** Applies style classnames to slots */
export const useMenuItemStyles = (state: MenuItemState) => {
const rootClassName = useRootStyles(state);
const iconClassName = useIconStyles(state);

state.className = ax(rootClassName, state.className);

if (state.icon) {
state.icon.className = ax(iconClassName, state.icon.className);
}
};
Loading

0 comments on commit fe4959d

Please sign in to comment.