better-mui-menu 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -34
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,28 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/eggei/better-mui-menu/actions/workflows/ci.yml) [](https://github.com/eggei/better-mui-menu/actions/workflows/security.yml) [](https://codecov.io/gh/eggei/better-mui-menu) [](https://www.npmjs.com/package/better-mui-menu) [](https://www.npmjs.com/package/better-mui-menu) [](https://opensource.org/licenses/MIT)
|
|
4
4
|
|
|
5
|
-
`better-mui-menu` is a
|
|
5
|
+
`better-mui-menu` is a zero-dependency wrapper for Material UI Menu to support nested menus and full keyboard accessibility with **proper focus management**.
|
|
6
6
|
|
|
7
7
|
## Live demo
|
|
8
8
|
|
|
9
|
-
Try it:
|
|
10
|
-
|
|
11
|
-
[StackBlitz playground](https://stackblitz.com/edit/vitejs-vite-autejxh8?embed=1&file=src%2FMenuDemo.tsx&view=preview).
|
|
12
|
-
|
|
13
9
|
[](https://codesandbox.io/p/sandbox/9j2z7n)
|
|
14
10
|
|
|
11
|
+
[Edit in StackBlitz](https://stackblitz.com/edit/vitejs-vite-autejxh8?embed=1&file=src%2FMenuDemo.tsx&view=preview)
|
|
12
|
+
|
|
15
13
|

|
|
16
14
|
|
|
17
15
|
## Features
|
|
18
16
|
|
|
17
|
+
- **Excellent Keyboard Navigation for Accessibility** – navigate with arrow keys, open submenus with right arrow or Enter, close them with left arrow or Escape, and have the menu stack stay in sync with focus.
|
|
19
18
|
- **Unlimited nesting** – describe every submenu with an `items` array and `better-mui-menu` renders `NestedMenuItem` poppers that stay synchronized with their parents.
|
|
20
|
-
- **
|
|
19
|
+
- **Customizable** – style your menu and menu items using MUI's Menu and MenuItem props.
|
|
21
20
|
- **Data-driven API** – you keep work in a single `MenuItem[]` list; leaves, dividers, and nested branches all live alongside each other.
|
|
22
21
|
|
|
23
22
|
## Installation
|
|
24
23
|
|
|
25
24
|
```bash
|
|
26
|
-
npm install better-mui-menu
|
|
25
|
+
(yarn | npm | pnpm) install better-mui-menu
|
|
27
26
|
```
|
|
28
27
|
|
|
29
28
|
Since the component renders Material UI primitives, also install the peer dependencies:
|
|
@@ -32,15 +31,9 @@ Since the component renders Material UI primitives, also install the peer depend
|
|
|
32
31
|
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
The `@mui/icons-material` peer is optional unless you use `startIcon`/`endIcon` helpers but we list it so consumers don’t need extra typings setup.
|
|
36
|
-
|
|
37
34
|
## Usage
|
|
38
35
|
|
|
39
36
|
```tsx
|
|
40
|
-
import { useState } from 'react'
|
|
41
|
-
import Button from '@mui/material/Button'
|
|
42
|
-
import Cloud from '@mui/icons-material/Cloud'
|
|
43
|
-
import Save from '@mui/icons-material/Save'
|
|
44
37
|
import { Menu, type MenuItem } from 'better-mui-menu'
|
|
45
38
|
|
|
46
39
|
const menuItems: MenuItem[] = [
|
|
@@ -52,7 +45,14 @@ const menuItems: MenuItem[] = [
|
|
|
52
45
|
},
|
|
53
46
|
{ type: 'divider' },
|
|
54
47
|
{
|
|
55
|
-
label:
|
|
48
|
+
label: (
|
|
49
|
+
<Stack>
|
|
50
|
+
Cloud actions
|
|
51
|
+
<Typography variant="caption" color="text.secondary">
|
|
52
|
+
Requires internet connection
|
|
53
|
+
</Typography>
|
|
54
|
+
</Stack>
|
|
55
|
+
),
|
|
56
56
|
startIcon: <Cloud fontSize='small' sx={{ ml: 0.5 }} />,
|
|
57
57
|
items: [
|
|
58
58
|
{ label: 'Upload', onClick: () => console.log('Upload') },
|
|
@@ -82,29 +82,12 @@ export function FileMenu() {
|
|
|
82
82
|
|
|
83
83
|
## Items shape
|
|
84
84
|
|
|
85
|
-
`MenuItem` extends `@mui/material/MenuItemProps` (excluding `children`) so you can still pass `dense`, `disabled`, `
|
|
85
|
+
`MenuItem` extends `@mui/material/MenuItemProps` (excluding `children`) so you can still pass `dense`, `disabled`, `aria-selected`, etc.
|
|
86
|
+
The `better-mui-menu` adds:
|
|
86
87
|
|
|
87
88
|
- `type?: 'item' | 'divider'` – render a `Divider` when `'divider'` is supplied.
|
|
88
89
|
- `id?: string` – optional stable ID for ARIA attributes; one is generated automatically otherwise.
|
|
89
90
|
- `label: ReactNode` – the label shown in the menu row.
|
|
90
91
|
- `startIcon` / `endIcon` – pass either a `SvgIconComponent` (for example `Save`) or a JSX element (for example `<Save fontSize='large' sx={{ ml: 0.5 }} />`).
|
|
91
92
|
- `items?: MenuItem[]` – nested entries that render as submenus.
|
|
92
|
-
- `onClick?: MenuItemProps['onClick']` –
|
|
93
|
-
|
|
94
|
-
## Accessibility & interactions
|
|
95
|
-
|
|
96
|
-
- Nested menus render in `Popper` instances positioned `right-start` from the trigger and fade in/out with a shared transition helper.
|
|
97
|
-
- Hover keeps a submenu open while the mouse moves between trigger and popper; leaving the area closes the branch.
|
|
98
|
-
- Keyboard navigation covers `ArrowUp`/`ArrowDown` through siblings, `ArrowRight` or `Enter/Space` to open children, `ArrowLeft` to back out, and `Escape` to dismiss every layer.
|
|
99
|
-
- ARIA helpers (`aria-haspopup`, `aria-controls`, `aria-expanded`, `aria-labelledby`) are wired automatically so assistive tech understands the structure.
|
|
100
|
-
|
|
101
|
-
## Development
|
|
102
|
-
|
|
103
|
-
The library lives inside `package/better-mui-menu`.
|
|
104
|
-
|
|
105
|
-
- `npm run dev` – rebuilds `src` into `dist` with `tsup --watch`.
|
|
106
|
-
- `npm run build` – creates production bundles ready for publication.
|
|
107
|
-
- `npm run test` – runs the Jest suite located at `src/**/*.test.{ts,tsx}`.
|
|
108
|
-
- `npm run test:coverage` – runs tests with coverage and writes reports to `coverage/`.
|
|
109
|
-
|
|
110
|
-
From the repository root you can use `npm run dev:lib` and `npm run dev:demo` together so the demo app consumes the rebuilt workspace link. Keep `npm run dev` (or `npm run build`) running before refreshing the demo because the Vite app imports the package via `file:`.
|
|
93
|
+
- `onClick?: MenuItemProps['onClick']` – callback function when the menu item is clicked.
|
package/dist/index.cjs
CHANGED
|
@@ -155,6 +155,9 @@ var NestedMenuItem = (props) => {
|
|
|
155
155
|
if (item.type === "divider") {
|
|
156
156
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_material3.Divider, {}, `divider-${index}`);
|
|
157
157
|
}
|
|
158
|
+
if (item.type === "header") {
|
|
159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_material3.ListSubheader, { children: item.label }, `header-${index}`);
|
|
160
|
+
}
|
|
158
161
|
const {
|
|
159
162
|
type: __,
|
|
160
163
|
items: entryItems,
|
|
@@ -301,6 +304,9 @@ function Menu({ items, elevation = DEFAULT_ELEVATION, ...rest }) {
|
|
|
301
304
|
if (item.type === "divider") {
|
|
302
305
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_material4.Divider, {}, `divider-${index}`);
|
|
303
306
|
}
|
|
307
|
+
if (item.type === "header") {
|
|
308
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_material4.ListSubheader, { children: item.label }, `header-${index}`);
|
|
309
|
+
}
|
|
304
310
|
const {
|
|
305
311
|
type: _,
|
|
306
312
|
id,
|
package/dist/index.d.cts
CHANGED
|
@@ -4,20 +4,25 @@ import React, { ReactNode, ReactElement } from 'react';
|
|
|
4
4
|
import { SvgIconComponent } from '@mui/icons-material';
|
|
5
5
|
|
|
6
6
|
type MenuIcon = SvgIconComponent | ReactElement;
|
|
7
|
-
type
|
|
7
|
+
type DataAttributes = {
|
|
8
|
+
[K in `data-${string}`]?: string | number | boolean | undefined;
|
|
9
|
+
};
|
|
10
|
+
type MenuDividerItem = {
|
|
8
11
|
type: 'divider';
|
|
9
|
-
}
|
|
12
|
+
} & DataAttributes;
|
|
13
|
+
type MenuHeaderItem = {
|
|
14
|
+
type: 'header';
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
} & DataAttributes;
|
|
17
|
+
type MenuActionItem = {
|
|
10
18
|
type?: 'item';
|
|
11
19
|
id?: string;
|
|
12
20
|
label: ReactNode;
|
|
13
21
|
startIcon?: MenuIcon;
|
|
14
22
|
endIcon?: MenuIcon;
|
|
15
23
|
items?: MenuItem[];
|
|
16
|
-
};
|
|
17
|
-
type
|
|
18
|
-
[K in `data-${string}`]?: string | number | boolean | undefined;
|
|
19
|
-
};
|
|
20
|
-
type MenuItem = MenuItemBase & Omit<MenuItemProps, 'children'> & DataAttributes;
|
|
24
|
+
} & Omit<MenuItemProps, 'children' | 'type'> & DataAttributes;
|
|
25
|
+
type MenuItem = MenuDividerItem | MenuHeaderItem | MenuActionItem;
|
|
21
26
|
|
|
22
27
|
type Props = {
|
|
23
28
|
items: MenuItem[];
|
package/dist/index.d.ts
CHANGED
|
@@ -4,20 +4,25 @@ import React, { ReactNode, ReactElement } from 'react';
|
|
|
4
4
|
import { SvgIconComponent } from '@mui/icons-material';
|
|
5
5
|
|
|
6
6
|
type MenuIcon = SvgIconComponent | ReactElement;
|
|
7
|
-
type
|
|
7
|
+
type DataAttributes = {
|
|
8
|
+
[K in `data-${string}`]?: string | number | boolean | undefined;
|
|
9
|
+
};
|
|
10
|
+
type MenuDividerItem = {
|
|
8
11
|
type: 'divider';
|
|
9
|
-
}
|
|
12
|
+
} & DataAttributes;
|
|
13
|
+
type MenuHeaderItem = {
|
|
14
|
+
type: 'header';
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
} & DataAttributes;
|
|
17
|
+
type MenuActionItem = {
|
|
10
18
|
type?: 'item';
|
|
11
19
|
id?: string;
|
|
12
20
|
label: ReactNode;
|
|
13
21
|
startIcon?: MenuIcon;
|
|
14
22
|
endIcon?: MenuIcon;
|
|
15
23
|
items?: MenuItem[];
|
|
16
|
-
};
|
|
17
|
-
type
|
|
18
|
-
[K in `data-${string}`]?: string | number | boolean | undefined;
|
|
19
|
-
};
|
|
20
|
-
type MenuItem = MenuItemBase & Omit<MenuItemProps, 'children'> & DataAttributes;
|
|
24
|
+
} & Omit<MenuItemProps, 'children' | 'type'> & DataAttributes;
|
|
25
|
+
type MenuItem = MenuDividerItem | MenuHeaderItem | MenuActionItem;
|
|
21
26
|
|
|
22
27
|
type Props = {
|
|
23
28
|
items: MenuItem[];
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/Menu/index.tsx
|
|
2
|
-
import { Divider as Divider2, Menu as MuiMenu } from "@mui/material";
|
|
2
|
+
import { Divider as Divider2, ListSubheader as ListSubheader2, Menu as MuiMenu } from "@mui/material";
|
|
3
3
|
import { useId as useId2 } from "react";
|
|
4
4
|
|
|
5
5
|
// src/Menu/NestedMenuItem.tsx
|
|
@@ -15,7 +15,7 @@ var ArrowRight_default = createSvgIcon(/* @__PURE__ */ _jsx("path", {
|
|
|
15
15
|
}), "ArrowRight");
|
|
16
16
|
|
|
17
17
|
// src/Menu/NestedMenuItem.tsx
|
|
18
|
-
import { Divider, Fade as Fade2, MenuItem as MuiMenuItem2, MenuList, Paper, Popper } from "@mui/material";
|
|
18
|
+
import { Divider, Fade as Fade2, ListSubheader, MenuItem as MuiMenuItem2, MenuList, Paper, Popper } from "@mui/material";
|
|
19
19
|
|
|
20
20
|
// src/Menu/MenuEntry.tsx
|
|
21
21
|
import { forwardRef, isValidElement } from "react";
|
|
@@ -129,6 +129,9 @@ var NestedMenuItem = (props) => {
|
|
|
129
129
|
if (item.type === "divider") {
|
|
130
130
|
return /* @__PURE__ */ jsx2(Divider, {}, `divider-${index}`);
|
|
131
131
|
}
|
|
132
|
+
if (item.type === "header") {
|
|
133
|
+
return /* @__PURE__ */ jsx2(ListSubheader, { children: item.label }, `header-${index}`);
|
|
134
|
+
}
|
|
132
135
|
const {
|
|
133
136
|
type: __,
|
|
134
137
|
items: entryItems,
|
|
@@ -275,6 +278,9 @@ function Menu({ items, elevation = DEFAULT_ELEVATION, ...rest }) {
|
|
|
275
278
|
if (item.type === "divider") {
|
|
276
279
|
return /* @__PURE__ */ jsx3(Divider2, {}, `divider-${index}`);
|
|
277
280
|
}
|
|
281
|
+
if (item.type === "header") {
|
|
282
|
+
return /* @__PURE__ */ jsx3(ListSubheader2, { children: item.label }, `header-${index}`);
|
|
283
|
+
}
|
|
278
284
|
const {
|
|
279
285
|
type: _,
|
|
280
286
|
id,
|