Skip to content
This repository has been archived by the owner on Jan 20, 2022. It is now read-only.

Commit

Permalink
✨ implement Menus
Browse files Browse the repository at this point in the history
  • Loading branch information
justinanastos committed Jan 29, 2020
1 parent 2655270 commit e21ebdd
Show file tree
Hide file tree
Showing 9 changed files with 671 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tsconfig.tsbuildinfo
/SpaceKitProvider
/Table
/Tooltip
/Menu*
/typography

# Generated icons stored in src directory
Expand Down
1 change: 1 addition & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function CJS() {
path.join("src", "illustrations", "scripts"),
path.join("src", "shared"),
path.join("src", "AbstractTooltip"),
path.join("src", "MenuIconSize"),
].some(excludedPathname => filename.includes(excludedPathname))
),
external: [
Expand Down
231 changes: 231 additions & 0 deletions src/Menu/Menu.story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { Button } from "../Button";
import { colors } from "../colors";
import { Menu } from "../Menu";
import { MenuItem } from "../MenuItem";
import { MenuDivider } from "../MenuDivider";
import { MenuHeading } from "../MenuHeading";
import {
Description,
Meta,
Story,
Props,
Preview,
} from "@storybook/addon-docs/blocks";
import { IconAstronaut1 } from "../icons/IconAstronaut1";
import { IconAstronaut2 } from "../icons/IconAstronaut2";
import { IconAstronaut3 } from "../icons/IconAstronaut3";
import { IconLander } from "../icons/IconLander";
import { IconPlanet3 } from "../icons/IconPlanet3";
import { IconShip3 } from "../icons/IconShip3";
import { IconCheck } from "../icons/IconCheck";

<Meta title="Components|Menu" />

# Menu

<Description
of={Menu}
markdown="**Menus** provide a way for sures to interact with the application by selecting from a set of options. While that’s simple in theory, menus can be complex and not eveything is detailed below. Menus can emerge from Select buttons or a simple button."
/>

## Simple Menu

The simple menu provides a list of options for a user to choose from and updates the button or select button with the choice the user has selected.

<Preview>
<Story name="basic">
<Menu
content={
<React.Fragment>
<MenuItem>Root</MenuItem>
<MenuItem selected>Objects</MenuItem>
<MenuItem>Inputs</MenuItem>
<MenuItem>Interfaces</MenuItem>
<MenuItem>Unions</MenuItem>
<MenuItem>Scalars and Enums</MenuItem>
</React.Fragment>
}
forceVisibleForTestingOnly
>
<Button>open menu</Button>
</Menu>
</Story>
</Preview>

## Segmented Menu

The segmented menu provides a way to group related options. This can be achieved
by a divider and in other cases, headers can be used to group similar
information and provide a nice visual grouping.

<Preview>
<Story name="divided">
<Menu
content={
<React.Fragment>
<MenuItem>All Types</MenuItem>
<MenuDivider />
<MenuItem>Root</MenuItem>
<MenuItem>Objects</MenuItem>
<MenuItem>Inputs</MenuItem>
<MenuItem>Interfaces</MenuItem>
<MenuItem>Unions</MenuItem>
<MenuItem>Scalars and Enums</MenuItem>
<MenuDivider />
<MenuItem>Deprecations</MenuItem>
</React.Fragment>
}
forceVisibleForTestingOnly
>
<Button>Test</Button>
</Menu>
</Story>
</Preview>

## Header

<Preview>
<Story name="heading">
<Menu
content={
<React.Fragment>
<MenuHeading count={5}>Filter by</MenuHeading>
<MenuItem>Root</MenuItem>
<MenuItem>Objects</MenuItem>
<MenuItem>Inputs</MenuItem>
<MenuItem>Interfaces</MenuItem>
<MenuItem>Unions</MenuItem>
<MenuItem>Scalars and Enums</MenuItem>
</React.Fragment>
}
forceVisibleForTestingOnly
>
<Button>Test</Button>
</Menu>
</Story>
</Preview>

## Icons

<Preview>
<Story name="icons">
<Menu
content={
<React.Fragment>
<MenuItem
icon={
<IconAstronaut1
style={{ width: "100%", height: "100%" }}
weight="thin"
/>
}
>
Cones
</MenuItem>
<MenuItem
icon={
<IconAstronaut2
style={{ width: "100%", height: "100%" }}
weight="thin"
/>
}
>
Rhomboid
</MenuItem>
<MenuItem
icon={
<IconAstronaut3
style={{ width: "100%", height: "100%" }}
weight="thin"
/>
}
>
Cubes
</MenuItem>
<MenuItem
icon={
<IconLander
style={{ width: "100%", height: "100%" }}
weight="thin"
/>
}
>
Diamonds
</MenuItem>
<MenuItem
icon={
<IconPlanet3
style={{ width: "100%", height: "100%" }}
weight="thin"
/>
}
>
Cylinders
</MenuItem>
<MenuItem
icon={
<IconShip3
style={{ width: "100%", height: "100%" }}
weight="thin"
/>
}
>
Pyramids
</MenuItem>
</React.Fragment>
}
forceVisibleForTestingOnly
>
<Button>Test</Button>
</Menu>
</Story>
</Preview>

## Icons with custom sizes

<Preview>
<Story name="menu-icon-size">
<Menu
iconSize="small"
maxWidth={280}
content={
<React.Fragment>
<MenuHeading icon={null}>mdg-private-graphs</MenuHeading>
<MenuItem icon={null}>apollo-day-nyc</MenuItem>
<MenuItem icon={null} selected>
engine
</MenuItem>
<MenuItem icon={null}>galaxy-eu-west</MenuItem>
<MenuItem
icon={
<IconCheck
style={{
color: colors.blue.base,
width: "100%",
height: "100%",
}}
/>
}
>
galaxy-ties
</MenuItem>
<MenuItem icon={null}>space-explorer</MenuItem>
<MenuItem icon={null}>z-end-to-end-ingestion-20</MenuItem>
</React.Fragment>
}
forceVisibleForTestingOnly
>
<Button>Test</Button>
</Menu>
</Story>
</Preview>

## Props

### `Menu`

<Props of={Menu} />

### `MenuItem`

<Props of={MenuItem} />
145 changes: 145 additions & 0 deletions src/Menu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
import React from "react";
import { AbstractTooltip } from "../AbstractTooltip";
import { TippyMenuStyles } from "./menu/TippyMenuStyles";
import {
IconSize,
MenuIconSizeProvider,
useMenuIconSize,
} from "../MenuIconSize";
import { Instance, ReferenceElement } from "tippy.js";

interface Props extends React.ComponentProps<typeof AbstractTooltip> {
// extends Pick<
// React.ComponentProps<typeof AbstractTooltip>,
// "content" | "children" | "trigger" | "maxWidth"
// > {
style?: React.CSSProperties;

/**
* Optionally set the icon size to use for all `MenuItem` descendents. This
* value will automatically be passed down via context and can be overriden by
* child `Menu` components
*
* Is inherited by antecedent definitions, or defaulted to `normal` if there
* are none
*/
iconSize?: IconSize;

/**
* Popper instance reference
*
* You'll need this to be able to call methods on the popper instance itself,
* such as wishing to hide the menu when a user clicks something.
*
* When using TypeScript, use this to create your own refs:
*
* ```tsx
* const menuInstanceRef = React.useRef<
* NonNullable<React.ComponentProps<typeof Menu>['instanceRef']>['current']
* >()
* ```
*/
instanceRef?: React.MutableRefObject<
| Parameters<
NonNullable<React.ComponentProps<typeof AbstractTooltip>["onCreate"]>
>[0]
| undefined
>;
}

/**
* Given an popper `Instance`, calculate the maximum height popper can take
*/
function calculateMaxHeight(instance: Instance) {
const parentHeight =
instance.props.appendTo === "parent"
? (instance.reference as any).offsetParent.offsetHeight
: document.body.clientHeight;

const {
height: referenceHeight,
top: referenceTop,
} = instance.reference.getBoundingClientRect();

const {
height: arrowHeight,
} = instance.popperChildren.arrow?.getBoundingClientRect() ?? { height: 0 };

const { distance } = instance.props;

return (
parentHeight -
referenceTop -
referenceHeight -
arrowHeight -
parseFloat(getComputedStyle(instance.popperChildren.tooltip).paddingTop) -
parseFloat(
getComputedStyle(instance.popperChildren.tooltip).paddingBottom
) -
(typeof distance === "number" ? distance : 0) -
// Margin from bottom of the page
5
);
}

function isReferenceObject(reference: any): reference is ReferenceElement {
return typeof (reference as ReferenceElement)._tippy !== "undefined";
}

export const Menu: React.FC<Props> = ({
children,
iconSize,
onCreate,
instanceRef,
...props
}) => {
const inheritedIconSize = useMenuIconSize();

return (
<MenuIconSizeProvider iconSize={iconSize ?? inheritedIconSize}>
<TippyMenuStyles />
<AbstractTooltip
onCreate={instance => {
if (instanceRef) {
instanceRef.current = instance;
}

onCreate?.(instance);
}}
hideOnClick
theme="space-kit-menu"
appendTo="parent"
trigger="mouseenter"
popperOptions={{
modifiers: {
preventOverflow: {
boundariesElement: "window",
// This will ensure that the menu is correctly placed when a
// parent is using an overflow container @see
// https://github.com/popperjs/popper-core/issues/535#issuecomment-361628222
escapeWithReference: true,
},
setMaxHeight: {
enabled: true,
order: 0,
fn: data => {
const reference = data.instance.reference;
if (isReferenceObject(reference) && reference._tippy) {
const tippy = reference._tippy;
const calculatedMaxHeight = calculateMaxHeight(tippy);
tippy.popperChildren.content.style.maxHeight = `${calculatedMaxHeight}px`;
}
return data;
},
},
},
}}
{...props}
interactive
>
{children}
</AbstractTooltip>
</MenuIconSizeProvider>
);
};
Loading

0 comments on commit e21ebdd

Please sign in to comment.