Skip to content

Commit 0cdeff7

Browse files
committed
feat(menu): Better floating action button default behavior
1 parent 7202dd0 commit 0cdeff7

File tree

9 files changed

+165
-14
lines changed

9 files changed

+165
-14
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Since the `DropdownMenu` supports all the props for a `Button`, you can render a
2+
`DropdownMenu` as a floating action button if needed.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ReactElement } from "react";
2+
import { FABPosition } from "@react-md/button";
3+
import { MoreVertSVGIcon } from "@react-md/material-icons";
4+
5+
import SimpleExample from "./SimpleExample";
6+
7+
const positions: FABPosition[] = [
8+
"top-left",
9+
"top-right",
10+
"bottom-left",
11+
"bottom-right",
12+
];
13+
14+
export default function FloatingActionButtonMenus(): ReactElement {
15+
return (
16+
<>
17+
{positions.map((position) => (
18+
<SimpleExample
19+
id={`fab-menu-${position}`}
20+
key={position}
21+
aria-label="Options..."
22+
floating={position}
23+
buttonChildren={<MoreVertSVGIcon />}
24+
/>
25+
))}
26+
</>
27+
);
28+
}

packages/documentation/src/components/Demos/Menu/SimpleExample.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import type { ReactElement } from "react";
22
import { HomeSVGIcon, InfoOutlineSVGIcon } from "@react-md/material-icons";
33
import {
44
DropdownMenu,
5+
DropdownMenuProps,
56
MenuItem,
67
MenuItemLink,
78
MenuItemSeparator,
89
} from "@react-md/menu";
910

10-
export default function SimpleExamples(): ReactElement {
11+
export default function SimpleExample(
12+
props: Partial<DropdownMenuProps>
13+
): ReactElement {
1114
return (
12-
<DropdownMenu id="dropdown-menu-1" buttonChildren="Options...">
15+
<DropdownMenu id="dropdown-menu-1" buttonChildren="Options..." {...props}>
1316
<MenuItem>Item 1</MenuItem>
1417
<MenuItem>Item 2</MenuItem>
1518
<MenuItem leftAddon={<HomeSVGIcon />}>Item 3 </MenuItem>

packages/documentation/src/components/Demos/Menu/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import {
55
} from "@react-md/menu";
66
import { IconProvider } from "@react-md/icon";
77

8+
import { DemoConfig } from "../types";
89
import DemoPage from "../DemoPage";
910

1011
import README from "./README.md";
1112

1213
import SimpleExample from "./SimpleExample";
1314
import simpleExample from "./SimpleExample.md";
1415

16+
import FloatingActionButtonMenus from "./FloatingActionButtonMenus";
17+
import floatingActionButtonMenus from "./FloatingActionButtonMenus.md";
18+
1519
import ConfigurableDropdownMenu from "./ConfigurableDropdownMenu";
1620
import configurableDropdownMenu from "./ConfigurableDropdownMenu.md";
1721

@@ -30,12 +34,18 @@ import menusWithFormComponents from "./MenusWithFormComponents.md";
3034
import HoverableMenus from "./HoverableMenus";
3135
import hoverableMenus from "./HoverableMenus.md";
3236

33-
const demos = [
37+
const demos: DemoConfig[] = [
3438
{
3539
name: "Simple Example",
3640
description: simpleExample,
3741
children: <SimpleExample />,
3842
},
43+
{
44+
name: "Floating Action Button Menus",
45+
description: floatingActionButtonMenus,
46+
children: <FloatingActionButtonMenus />,
47+
emulated: true,
48+
},
3949
{
4050
name: "Configurable Dropdown Menu",
4151
description: configurableDropdownMenu,

packages/menu/src/DropdownMenu.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ReactElement, RefObject, useState } from "react";
2+
import { FABPosition } from "@react-md/button";
23
import { useUserInteractionMode } from "@react-md/utils";
34

45
import { useMenuBarContext } from "./MenuBarProvider";
@@ -165,6 +166,11 @@ export function DropdownMenu({
165166
};
166167
}
167168

169+
let floating: FABPosition = null;
170+
if (!menuitem) {
171+
({ floating = null } = props as DropdownMenuButtonProps);
172+
}
173+
168174
const [visible, setVisible] = useState(false);
169175
const { menuRef, menuProps, toggleRef, toggleProps } = useMenu<
170176
HTMLButtonElement | HTMLLIElement
@@ -181,6 +187,7 @@ export function DropdownMenu({
181187
onToggleMouseLeave: onMouseLeave,
182188
onMenuClick: propMenuProps?.onClick,
183189
onMenuKeyDown: propMenuProps?.onKeyDown,
190+
floating,
184191
onEnter,
185192
onEntering,
186193
onEntered,

packages/menu/src/MenuButton.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ export const MenuButton = forwardRef<HTMLButtonElement, MenuButtonProps>(
3131
iconAfter = true,
3232
iconRotatorProps,
3333
textIconSpacingProps,
34-
theme = "clear",
35-
themeType = "flat",
36-
buttonType = "text",
34+
floating,
35+
theme = floating ? "secondary" : "clear",
36+
themeType = floating ? "contained" : "flat",
37+
buttonType = floating ? "icon" : "text",
3738
disableDropdownIcon = buttonType === "icon",
3839
children,
3940
visible,
@@ -58,6 +59,7 @@ export const MenuButton = forwardRef<HTMLButtonElement, MenuButtonProps>(
5859
theme={theme}
5960
themeType={themeType}
6061
buttonType={buttonType}
62+
floating={floating}
6163
>
6264
<TextIconSpacing
6365
icon={icon}

packages/menu/src/__tests__/utils.ts

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,109 @@
11
import {
22
BELOW_CENTER_ANCHOR,
33
BELOW_INNER_LEFT_ANCHOR,
4+
BOTTOM_INNER_LEFT_ANCHOR,
5+
BOTTOM_INNER_RIGHT_ANCHOR,
46
CENTER_RIGHT_ANCHOR,
7+
TOP_INNER_LEFT_ANCHOR,
58
TOP_INNER_RIGHT_ANCHOR,
69
TOP_RIGHT_ANCHOR,
710
} from "@react-md/utils";
11+
812
import { getDefaultAnchor } from "../utils";
913

1014
describe("getDefaultAnchor", () => {
1115
it("should return a default anchor based on the menubar, menuitem, and horizontal flags", () => {
1216
expect(
13-
getDefaultAnchor({ menubar: true, menuitem: false, horizontal: false })
17+
getDefaultAnchor({
18+
menubar: false,
19+
menuitem: false,
20+
floating: "bottom-left",
21+
horizontal: false,
22+
})
23+
).toBe(BOTTOM_INNER_LEFT_ANCHOR);
24+
expect(
25+
getDefaultAnchor({
26+
menubar: false,
27+
menuitem: false,
28+
floating: "bottom-right",
29+
horizontal: false,
30+
})
31+
).toBe(BOTTOM_INNER_RIGHT_ANCHOR);
32+
expect(
33+
getDefaultAnchor({
34+
menubar: false,
35+
menuitem: false,
36+
floating: "top-left",
37+
horizontal: false,
38+
})
39+
).toBe(TOP_INNER_LEFT_ANCHOR);
40+
expect(
41+
getDefaultAnchor({
42+
menubar: false,
43+
menuitem: false,
44+
floating: "top-right",
45+
horizontal: false,
46+
})
47+
).toBe(TOP_INNER_RIGHT_ANCHOR);
48+
49+
expect(
50+
getDefaultAnchor({
51+
menubar: true,
52+
menuitem: false,
53+
floating: null,
54+
horizontal: false,
55+
})
1456
).toBe(BELOW_INNER_LEFT_ANCHOR);
1557
expect(
16-
getDefaultAnchor({ menubar: true, menuitem: false, horizontal: true })
58+
getDefaultAnchor({
59+
menubar: true,
60+
menuitem: false,
61+
floating: null,
62+
horizontal: true,
63+
})
1764
).toBe(BELOW_INNER_LEFT_ANCHOR);
1865
expect(
19-
getDefaultAnchor({ menubar: true, menuitem: true, horizontal: false })
66+
getDefaultAnchor({
67+
menubar: true,
68+
menuitem: true,
69+
floating: null,
70+
horizontal: false,
71+
})
2072
).toBe(CENTER_RIGHT_ANCHOR);
2173
expect(
22-
getDefaultAnchor({ menubar: true, menuitem: true, horizontal: true })
74+
getDefaultAnchor({
75+
menubar: true,
76+
menuitem: true,
77+
floating: null,
78+
horizontal: true,
79+
})
2380
).toBe(CENTER_RIGHT_ANCHOR);
2481

2582
expect(
26-
getDefaultAnchor({ menubar: false, menuitem: false, horizontal: true })
83+
getDefaultAnchor({
84+
menubar: false,
85+
menuitem: false,
86+
floating: null,
87+
horizontal: true,
88+
})
2789
).toBe(BELOW_CENTER_ANCHOR);
2890

2991
expect(
30-
getDefaultAnchor({ menubar: false, menuitem: true, horizontal: false })
92+
getDefaultAnchor({
93+
menubar: false,
94+
menuitem: true,
95+
floating: null,
96+
horizontal: false,
97+
})
3198
).toBe(TOP_RIGHT_ANCHOR);
3299

33100
expect(
34-
getDefaultAnchor({ menubar: false, menuitem: false, horizontal: false })
101+
getDefaultAnchor({
102+
menubar: false,
103+
menuitem: false,
104+
floating: null,
105+
horizontal: false,
106+
})
35107
).toBe(TOP_INNER_RIGHT_ANCHOR);
36108
});
37109
});

packages/menu/src/useMenu.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useEffect,
66
useRef,
77
} from "react";
8+
import { FABPosition } from "@react-md/button";
89
import { useFixedPositioning } from "@react-md/transition";
910
import { containsElement, useScrollLock } from "@react-md/utils";
1011

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

31+
/**
32+
* This is just used to update the default anchor behavior.
33+
*
34+
* @see {@link FABPosition}
35+
* @defaultValue `null`
36+
*/
37+
floating?: FABPosition;
38+
3039
/**
3140
* An optional click handler to merge with the
3241
* {@link MenuHookReturnValue.onClick} behavior.
@@ -161,6 +170,7 @@ export function useMenu<ToggleEl extends HTMLElement>(
161170
menuLabel,
162171
visible,
163172
setVisible,
173+
floating = null,
164174
onMenuClick = noop,
165175
onMenuKeyDown = noop,
166176
onToggleClick = noop,
@@ -199,7 +209,7 @@ export function useMenu<ToggleEl extends HTMLElement>(
199209
// interacting with the menu
200210
const cancelExitFocus = useRef(false);
201211
const anchor =
202-
propAnchor ?? getDefaultAnchor({ menubar, menuitem, horizontal });
212+
propAnchor ?? getDefaultAnchor({ menubar, menuitem, floating, horizontal });
203213
const menuNodeRef = useRef<HTMLDivElement>(null);
204214
const toggleRef = useRef<ToggleEl | null>(null);
205215
const {

packages/menu/src/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { FABPosition } from "@react-md/button";
12
import {
23
BELOW_CENTER_ANCHOR,
34
BELOW_INNER_LEFT_ANCHOR,
5+
BOTTOM_INNER_LEFT_ANCHOR,
6+
BOTTOM_INNER_RIGHT_ANCHOR,
47
CENTER_RIGHT_ANCHOR,
58
PositionAnchor,
9+
TOP_INNER_LEFT_ANCHOR,
610
TOP_INNER_RIGHT_ANCHOR,
711
TOP_RIGHT_ANCHOR,
812
} from "@react-md/utils";
@@ -22,6 +26,7 @@ export const noop = (): void => {
2226
interface DefaultAnchorOptions {
2327
menubar: boolean;
2428
menuitem: boolean;
29+
floating: FABPosition;
2530
horizontal: boolean;
2631
}
2732

@@ -32,8 +37,20 @@ interface DefaultAnchorOptions {
3237
export const getDefaultAnchor = ({
3338
menubar,
3439
menuitem,
40+
floating,
3541
horizontal,
3642
}: DefaultAnchorOptions): PositionAnchor => {
43+
switch (floating) {
44+
case "bottom-left":
45+
return BOTTOM_INNER_LEFT_ANCHOR;
46+
case "bottom-right":
47+
return BOTTOM_INNER_RIGHT_ANCHOR;
48+
case "top-left":
49+
return TOP_INNER_LEFT_ANCHOR;
50+
case "top-right":
51+
return TOP_INNER_RIGHT_ANCHOR;
52+
}
53+
3754
if (menubar) {
3855
return menuitem ? CENTER_RIGHT_ANCHOR : BELOW_INNER_LEFT_ANCHOR;
3956
}

0 commit comments

Comments
 (0)