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 CHANGED
@@ -2,28 +2,27 @@
2
2
 
3
3
  [![CI](https://github.com/eggei/better-mui-menu/actions/workflows/ci.yml/badge.svg)](https://github.com/eggei/better-mui-menu/actions/workflows/ci.yml) [![Security](https://github.com/eggei/better-mui-menu/actions/workflows/security.yml/badge.svg)](https://github.com/eggei/better-mui-menu/actions/workflows/security.yml) [![coverage](https://codecov.io/gh/eggei/better-mui-menu/branch/main/graph/badge.svg)](https://codecov.io/gh/eggei/better-mui-menu) [![npm version](https://img.shields.io/npm/v/better-mui-menu.svg?color=brightgreen)](https://www.npmjs.com/package/better-mui-menu) [![npm downloads](https://img.shields.io/npm/dt/better-mui-menu.svg?color=informational)](https://www.npmjs.com/package/better-mui-menu) [![License: MIT](https://img.shields.io/npm/l/better-mui-menu.svg)](https://opensource.org/licenses/MIT)
4
4
 
5
- `better-mui-menu` is a lightweight drop-in for Material UI that keeps a normal `Menu` structure while adding nested menus and full keyboard accessibility so nothing breaks audits or expectations in an MUI app.
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
  [![Edit better-mui-menu-demo](https://codesandbox.io/static/img/play-codesandbox.svg)](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
  ![Menu demo preview](./app/demo/assets/bmm-demo.gif)
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
- - **Keyboard accessible** – arrow keys, Enter/Space, and Escape behave like desktop menus, while focus management and ARIA attributes make the tree readable by assistive tech.
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: 'Cloud actions',
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`, `divider`, `aria-selected`, etc. The `better-mui-menu` shape adds:
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']` – leaves bubble clicks and close the menu stack so the consumer can handle the action.
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 MenuItemBase = {
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 DataAttributes = {
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 MenuItemBase = {
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 DataAttributes = {
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,
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "bugs": {
10
10
  "url": "https://github.com/eggei/better-mui-menu/issues"
11
11
  },
12
- "version": "1.4.0",
12
+ "version": "1.5.0",
13
13
  "type": "module",
14
14
  "sideEffects": false,
15
15
  "files": [