Skip to content

Commit

Permalink
feat(menu): Better floating action button default behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Jan 30, 2022
1 parent 7202dd0 commit 0cdeff7
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Since the `DropdownMenu` supports all the props for a `Button`, you can render a
`DropdownMenu` as a floating action button if needed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ReactElement } from "react";
import { FABPosition } from "@react-md/button";
import { MoreVertSVGIcon } from "@react-md/material-icons";

import SimpleExample from "./SimpleExample";

const positions: FABPosition[] = [
"top-left",
"top-right",
"bottom-left",
"bottom-right",
];

export default function FloatingActionButtonMenus(): ReactElement {
return (
<>
{positions.map((position) => (
<SimpleExample
id={`fab-menu-${position}`}
key={position}
aria-label="Options..."
floating={position}
buttonChildren={<MoreVertSVGIcon />}
/>
))}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import type { ReactElement } from "react";
import { HomeSVGIcon, InfoOutlineSVGIcon } from "@react-md/material-icons";
import {
DropdownMenu,
DropdownMenuProps,
MenuItem,
MenuItemLink,
MenuItemSeparator,
} from "@react-md/menu";

export default function SimpleExamples(): ReactElement {
export default function SimpleExample(
props: Partial<DropdownMenuProps>
): ReactElement {
return (
<DropdownMenu id="dropdown-menu-1" buttonChildren="Options...">
<DropdownMenu id="dropdown-menu-1" buttonChildren="Options..." {...props}>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem leftAddon={<HomeSVGIcon />}>Item 3 </MenuItem>
Expand Down
12 changes: 11 additions & 1 deletion packages/documentation/src/components/Demos/Menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import {
} from "@react-md/menu";
import { IconProvider } from "@react-md/icon";

import { DemoConfig } from "../types";
import DemoPage from "../DemoPage";

import README from "./README.md";

import SimpleExample from "./SimpleExample";
import simpleExample from "./SimpleExample.md";

import FloatingActionButtonMenus from "./FloatingActionButtonMenus";
import floatingActionButtonMenus from "./FloatingActionButtonMenus.md";

import ConfigurableDropdownMenu from "./ConfigurableDropdownMenu";
import configurableDropdownMenu from "./ConfigurableDropdownMenu.md";

Expand All @@ -30,12 +34,18 @@ import menusWithFormComponents from "./MenusWithFormComponents.md";
import HoverableMenus from "./HoverableMenus";
import hoverableMenus from "./HoverableMenus.md";

const demos = [
const demos: DemoConfig[] = [
{
name: "Simple Example",
description: simpleExample,
children: <SimpleExample />,
},
{
name: "Floating Action Button Menus",
description: floatingActionButtonMenus,
children: <FloatingActionButtonMenus />,
emulated: true,
},
{
name: "Configurable Dropdown Menu",
description: configurableDropdownMenu,
Expand Down
7 changes: 7 additions & 0 deletions packages/menu/src/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ReactElement, RefObject, useState } from "react";
import { FABPosition } from "@react-md/button";
import { useUserInteractionMode } from "@react-md/utils";

import { useMenuBarContext } from "./MenuBarProvider";
Expand Down Expand Up @@ -165,6 +166,11 @@ export function DropdownMenu({
};
}

let floating: FABPosition = null;
if (!menuitem) {
({ floating = null } = props as DropdownMenuButtonProps);
}

const [visible, setVisible] = useState(false);
const { menuRef, menuProps, toggleRef, toggleProps } = useMenu<
HTMLButtonElement | HTMLLIElement
Expand All @@ -181,6 +187,7 @@ export function DropdownMenu({
onToggleMouseLeave: onMouseLeave,
onMenuClick: propMenuProps?.onClick,
onMenuKeyDown: propMenuProps?.onKeyDown,
floating,
onEnter,
onEntering,
onEntered,
Expand Down
8 changes: 5 additions & 3 deletions packages/menu/src/MenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ export const MenuButton = forwardRef<HTMLButtonElement, MenuButtonProps>(
iconAfter = true,
iconRotatorProps,
textIconSpacingProps,
theme = "clear",
themeType = "flat",
buttonType = "text",
floating,
theme = floating ? "secondary" : "clear",
themeType = floating ? "contained" : "flat",
buttonType = floating ? "icon" : "text",
disableDropdownIcon = buttonType === "icon",
children,
visible,
Expand All @@ -58,6 +59,7 @@ export const MenuButton = forwardRef<HTMLButtonElement, MenuButtonProps>(
theme={theme}
themeType={themeType}
buttonType={buttonType}
floating={floating}
>
<TextIconSpacing
icon={icon}
Expand Down
86 changes: 79 additions & 7 deletions packages/menu/src/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,109 @@
import {
BELOW_CENTER_ANCHOR,
BELOW_INNER_LEFT_ANCHOR,
BOTTOM_INNER_LEFT_ANCHOR,
BOTTOM_INNER_RIGHT_ANCHOR,
CENTER_RIGHT_ANCHOR,
TOP_INNER_LEFT_ANCHOR,
TOP_INNER_RIGHT_ANCHOR,
TOP_RIGHT_ANCHOR,
} from "@react-md/utils";

import { getDefaultAnchor } from "../utils";

describe("getDefaultAnchor", () => {
it("should return a default anchor based on the menubar, menuitem, and horizontal flags", () => {
expect(
getDefaultAnchor({ menubar: true, menuitem: false, horizontal: false })
getDefaultAnchor({
menubar: false,
menuitem: false,
floating: "bottom-left",
horizontal: false,
})
).toBe(BOTTOM_INNER_LEFT_ANCHOR);
expect(
getDefaultAnchor({
menubar: false,
menuitem: false,
floating: "bottom-right",
horizontal: false,
})
).toBe(BOTTOM_INNER_RIGHT_ANCHOR);
expect(
getDefaultAnchor({
menubar: false,
menuitem: false,
floating: "top-left",
horizontal: false,
})
).toBe(TOP_INNER_LEFT_ANCHOR);
expect(
getDefaultAnchor({
menubar: false,
menuitem: false,
floating: "top-right",
horizontal: false,
})
).toBe(TOP_INNER_RIGHT_ANCHOR);

expect(
getDefaultAnchor({
menubar: true,
menuitem: false,
floating: null,
horizontal: false,
})
).toBe(BELOW_INNER_LEFT_ANCHOR);
expect(
getDefaultAnchor({ menubar: true, menuitem: false, horizontal: true })
getDefaultAnchor({
menubar: true,
menuitem: false,
floating: null,
horizontal: true,
})
).toBe(BELOW_INNER_LEFT_ANCHOR);
expect(
getDefaultAnchor({ menubar: true, menuitem: true, horizontal: false })
getDefaultAnchor({
menubar: true,
menuitem: true,
floating: null,
horizontal: false,
})
).toBe(CENTER_RIGHT_ANCHOR);
expect(
getDefaultAnchor({ menubar: true, menuitem: true, horizontal: true })
getDefaultAnchor({
menubar: true,
menuitem: true,
floating: null,
horizontal: true,
})
).toBe(CENTER_RIGHT_ANCHOR);

expect(
getDefaultAnchor({ menubar: false, menuitem: false, horizontal: true })
getDefaultAnchor({
menubar: false,
menuitem: false,
floating: null,
horizontal: true,
})
).toBe(BELOW_CENTER_ANCHOR);

expect(
getDefaultAnchor({ menubar: false, menuitem: true, horizontal: false })
getDefaultAnchor({
menubar: false,
menuitem: true,
floating: null,
horizontal: false,
})
).toBe(TOP_RIGHT_ANCHOR);

expect(
getDefaultAnchor({ menubar: false, menuitem: false, horizontal: false })
getDefaultAnchor({
menubar: false,
menuitem: false,
floating: null,
horizontal: false,
})
).toBe(TOP_INNER_RIGHT_ANCHOR);
});
});
12 changes: 11 additions & 1 deletion packages/menu/src/useMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useEffect,
useRef,
} from "react";
import { FABPosition } from "@react-md/button";
import { useFixedPositioning } from "@react-md/transition";
import { containsElement, useScrollLock } from "@react-md/utils";

Expand All @@ -27,6 +28,14 @@ export interface MenuHookOptions<ToggleEl extends HTMLElement>
*/
disabled?: boolean;

/**
* This is just used to update the default anchor behavior.
*
* @see {@link FABPosition}
* @defaultValue `null`
*/
floating?: FABPosition;

/**
* An optional click handler to merge with the
* {@link MenuHookReturnValue.onClick} behavior.
Expand Down Expand Up @@ -161,6 +170,7 @@ export function useMenu<ToggleEl extends HTMLElement>(
menuLabel,
visible,
setVisible,
floating = null,
onMenuClick = noop,
onMenuKeyDown = noop,
onToggleClick = noop,
Expand Down Expand Up @@ -199,7 +209,7 @@ export function useMenu<ToggleEl extends HTMLElement>(
// interacting with the menu
const cancelExitFocus = useRef(false);
const anchor =
propAnchor ?? getDefaultAnchor({ menubar, menuitem, horizontal });
propAnchor ?? getDefaultAnchor({ menubar, menuitem, floating, horizontal });
const menuNodeRef = useRef<HTMLDivElement>(null);
const toggleRef = useRef<ToggleEl | null>(null);
const {
Expand Down
17 changes: 17 additions & 0 deletions packages/menu/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { FABPosition } from "@react-md/button";
import {
BELOW_CENTER_ANCHOR,
BELOW_INNER_LEFT_ANCHOR,
BOTTOM_INNER_LEFT_ANCHOR,
BOTTOM_INNER_RIGHT_ANCHOR,
CENTER_RIGHT_ANCHOR,
PositionAnchor,
TOP_INNER_LEFT_ANCHOR,
TOP_INNER_RIGHT_ANCHOR,
TOP_RIGHT_ANCHOR,
} from "@react-md/utils";
Expand All @@ -22,6 +26,7 @@ export const noop = (): void => {
interface DefaultAnchorOptions {
menubar: boolean;
menuitem: boolean;
floating: FABPosition;
horizontal: boolean;
}

Expand All @@ -32,8 +37,20 @@ interface DefaultAnchorOptions {
export const getDefaultAnchor = ({
menubar,
menuitem,
floating,
horizontal,
}: DefaultAnchorOptions): PositionAnchor => {
switch (floating) {
case "bottom-left":
return BOTTOM_INNER_LEFT_ANCHOR;
case "bottom-right":
return BOTTOM_INNER_RIGHT_ANCHOR;
case "top-left":
return TOP_INNER_LEFT_ANCHOR;
case "top-right":
return TOP_INNER_RIGHT_ANCHOR;
}

if (menubar) {
return menuitem ? CENTER_RIGHT_ANCHOR : BELOW_INNER_LEFT_ANCHOR;
}
Expand Down

0 comments on commit 0cdeff7

Please sign in to comment.