Repository: mui/material-ui (TypeScript)
Date: 2023-05-24 14:12:27+00:00
Task type: Bug Fix

Issue:
[joy][base][Menu] MenuItem `tabindex` isn't logical in a changing list
### Duplicates

- [X] I have searched the existing issues

### Latest version

- [X] I have tested the latest version

### Steps to reproduce 🕹

Link to live example: https://codesandbox.io/s/modest-gates-zwyhbd?file=/demo.tsx

This is the same example as #36799

Steps:

1. With the `MenuList` in focus navigate up and down the list, this will behave normally
2. Type a "H" in the search field and remove it again
3. Set focus back to the `MenuList` component and try navigating up and down the list again.


### Current behavior 😯

Navigating up and down the list will now happen in a random order.

### Expected behavior 🤔

The navigation happens predictably by going to the previous and next item instead of jumping up and down the list.

### Context 🔦

_No response_

### Your environment 🌎

_No response_


Issue discussion:
I will check this issue
Agreed with the bug, i can reproduce
1. 'Menuitem' elements run registerItem() when mounted and unregisterItem() when unmounted.(useMenuItem.ts)
2. registerItem() and unregisterItem() run setMenuItems() to add and delete the data of the element to the 'menuitems' variable. (useMenu.ts)

Wherever 'MenuItem' element is added, the registerItem() function adds the element's data to the end of the 'menuitems' variable.

(The id value is generated by useId().)


The 'menuitems' variable is passed to 'options' variable,
The index of options is the order in which they are highlighted (defaultListboxReducer.ts)

A temporary option is to delete "key={item}".

<html>
<body>
<!--StartFragment--><!DOCTYPE html>

step | "This is a text" | "More demo" | "Hello world" | "Menu focus issue" | "Autofocus should be optional"
-- | -- | -- | -- | -- | --
1.id | R1nj9km / | R2nj9km / | R3nj9km | R4nj9km | R5nj9km
2.id |   |   | R3nj9km |   |  
3.id | r1 | r3 | R3nj9km | r5 | r7

<!--EndFragment-->
</body>
</html>

<html>
<body>
<!--StartFragment--><!DOCTYPE html>

1.menuItems | R1nj9km | R2nj9km | R3nj9km | R4nj9km | R5nj9km
-- | -- | -- | -- | -- | --
2.menuItems | R3nj9km |   |   |   |  
3.menuItems | R3nj9km | r1 | r3 | r5 | r7

<!--EndFragment-->
</body>
</html>

I hope someone can solve this problem
@sai6855 I took a stab at it as it was closely related to the work I've been doping recently.
sure no problem
This problem is still exists @michaldudak
We have a similar problem with tabs. When a new tab is added somewhere in the middle or reordered by drag-and-drop, the tab/arrow order becomes inconsistent since new components will always be registered last and can't change their position. This appeared after https://github.com/mui/material-ui/pull/36400 was released in 5.12.1. I think this is related to this bug since they share the same internal hooks. I could also create a separate issue if preferred.

Solution patch:
diff --git a/docs/pages/base-ui/api/menu.json b/docs/pages/base-ui/api/menu.json
--- a/docs/pages/base-ui/api/menu.json
+++ b/docs/pages/base-ui/api/menu.json
@@ -7,6 +7,7 @@
         "description": "HTML element<br>&#124;&nbsp;object<br>&#124;&nbsp;func"
       }
     },
+    "onItemsChange": { "type": { "name": "func" } },
     "onOpenChange": { "type": { "name": "func" } },
     "open": { "type": { "name": "bool" }, "default": "false" },
     "slotProps": {
diff --git a/docs/pages/base-ui/api/use-menu.json b/docs/pages/base-ui/api/use-menu.json
--- a/docs/pages/base-ui/api/use-menu.json
+++ b/docs/pages/base-ui/api/use-menu.json
@@ -5,6 +5,12 @@
     "listboxRef": {
       "type": { "name": "React.Ref&lt;Element&gt;", "description": "React.Ref&lt;Element&gt;" }
     },
+    "onItemsChange": {
+      "type": {
+        "name": "(items: string[]) =&gt; void",
+        "description": "(items: string[]) =&gt; void"
+      }
+    },
     "onOpenChange": {
       "type": { "name": "(open: boolean) =&gt; void", "description": "(open: boolean) =&gt; void" }
     },
diff --git a/docs/pages/joy-ui/api/menu-list.json b/docs/pages/joy-ui/api/menu-list.json
--- a/docs/pages/joy-ui/api/menu-list.json
+++ b/docs/pages/joy-ui/api/menu-list.json
@@ -1,11 +1,6 @@
 {
   "props": {
-    "actions": {
-      "type": {
-        "name": "union",
-        "description": "func<br>&#124;&nbsp;{ current?: { dispatch: func } }"
-      }
-    },
+    "actions": { "type": { "name": "custom", "description": "ref" } },
     "color": {
       "type": {
         "name": "union",
@@ -14,6 +9,7 @@
       "default": "'neutral'"
     },
     "component": { "type": { "name": "elementType" } },
+    "onItemsChange": { "type": { "name": "func" } },
     "size": {
       "type": {
         "name": "union",
diff --git a/docs/pages/joy-ui/api/menu.json b/docs/pages/joy-ui/api/menu.json
--- a/docs/pages/joy-ui/api/menu.json
+++ b/docs/pages/joy-ui/api/menu.json
@@ -20,6 +20,7 @@
       }
     },
     "onClose": { "type": { "name": "func" } },
+    "onItemsChange": { "type": { "name": "func" } },
     "open": { "type": { "name": "bool" }, "default": "false" },
     "size": {
       "type": {
diff --git a/docs/translations/api-docs-base/menu/menu.json b/docs/translations/api-docs-base/menu/menu.json
--- a/docs/translations/api-docs-base/menu/menu.json
+++ b/docs/translations/api-docs-base/menu/menu.json
@@ -1,8 +1,9 @@
 {
   "componentDescription": "",
   "propDescriptions": {
-    "actions": "A ref with imperative actions. It allows to select the first or last menu item.",
+    "actions": "A ref with imperative actions that can be performed on the menu.",
     "anchorEl": "An HTML element, <a href=\"https://popper.js.org/docs/v2/virtual-elements/\">virtualElement</a>, or a function that returns either. It&#39;s used to set the position of the popper.",
+    "onItemsChange": "Function called when the items displayed in the menu change.",
     "onOpenChange": "Triggered when focus leaves the menu and the menu should close.",
     "open": "Controls whether the menu is displayed.",
     "slotProps": "The props used for each slot inside the Menu.",
diff --git a/docs/translations/api-docs-joy/menu-list/menu-list.json b/docs/translations/api-docs-joy/menu-list/menu-list.json
--- a/docs/translations/api-docs-joy/menu-list/menu-list.json
+++ b/docs/translations/api-docs-joy/menu-list/menu-list.json
@@ -4,6 +4,7 @@
     "actions": "A ref with imperative actions. It allows to select the first or last menu item.",
     "color": "The color of the component. It supports those theme colors that make sense for this component. To learn how to add your own colors, check out <a href=\"/joy-ui/customization/themed-components/#extend-colors\">Themed components—Extend colors</a>.",
     "component": "The component used for the root node. Either a string to use a HTML element or a component.",
+    "onItemsChange": "Function called when the items displayed in the menu change.",
     "size": "The size of the component (affect other nested list* components because the <code>Menu</code> inherits <code>List</code>). To learn how to add custom sizes to the component, check out <a href=\"/joy-ui/customization/themed-components/#extend-sizes\">Themed components—Extend sizes</a>.",
     "slotProps": "The props used for each slot inside.",
     "slots": "The components used for each slot inside. See <a href=\"#slots\">Slots API</a> below for more details.",
diff --git a/docs/translations/api-docs-joy/menu/menu.json b/docs/translations/api-docs-joy/menu/menu.json
--- a/docs/translations/api-docs-joy/menu/menu.json
+++ b/docs/translations/api-docs-joy/menu/menu.json
@@ -10,6 +10,7 @@
     "keepMounted": "Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Popper.",
     "modifiers": "Popper.js is based on a &quot;plugin-like&quot; architecture, most of its features are fully encapsulated &quot;modifiers&quot;.<br>A modifier is a function that is called each time Popper.js needs to compute the position of the popper. For this reason, modifiers should be very performant to avoid bottlenecks. To learn how to create a modifier, <a href=\"https://popper.js.org/docs/v2/modifiers/\">read the modifiers documentation</a>.",
     "onClose": "Triggered when focus leaves the menu and the menu should close.",
+    "onItemsChange": "Function called when the items displayed in the menu change.",
     "open": "Controls whether the menu is displayed.",
     "size": "The size of the component (affect other nested list* components because the <code>Menu</code> inherits <code>List</code>). To learn how to add custom sizes to the component, check out <a href=\"/joy-ui/customization/themed-components/#extend-sizes\">Themed components—Extend sizes</a>.",
     "slotProps": "The props used for each slot inside.",
diff --git a/docs/translations/api-docs/use-menu/use-menu.json b/docs/translations/api-docs/use-menu/use-menu.json
--- a/docs/translations/api-docs/use-menu/use-menu.json
+++ b/docs/translations/api-docs/use-menu/use-menu.json
@@ -4,6 +4,7 @@
     "defaultOpen": "If <code>true</code>, the menu will be initially open.",
     "listboxId": "Id of the menu listbox.",
     "listboxRef": "Ref of the menu listbox.",
+    "onItemsChange": "Callback fired when the menu items change.",
     "onOpenChange": "Callback fired when the menu is opened or closed.",
     "open": "If <code>true</code>, the menu will be open.\nThis is the controlled equivalent of the <code>defaultOpen</code> parameter."
   },
diff --git a/packages/mui-base/src/Menu/Menu.tsx b/packages/mui-base/src/Menu/Menu.tsx
--- a/packages/mui-base/src/Menu/Menu.tsx
+++ b/packages/mui-base/src/Menu/Menu.tsx
@@ -11,6 +11,7 @@ import useSlotProps from '../utils/useSlotProps';
 import { useClassNamesOverride } from '../utils/ClassNameConfigurator';
 import { WithOptionalOwnerState } from '../utils';
 import MenuProvider from '../useMenu/MenuProvider';
+import { ListActionTypes } from '../useList';
 
 function useUtilityClasses(ownerState: MenuOwnerState) {
   const { open } = ownerState;
@@ -41,6 +42,7 @@ const Menu = React.forwardRef(function Menu<RootComponentType extends React.Elem
     children,
     defaultOpen,
     listboxId,
+    onItemsChange,
     onOpenChange,
     open: openProp,
     slotProps = {},
@@ -51,6 +53,7 @@ const Menu = React.forwardRef(function Menu<RootComponentType extends React.Elem
   const { contextValue, getListboxProps, dispatch, open } = useMenu({
     defaultOpen,
     open: openProp,
+    onItemsChange,
     onOpenChange,
     listboxId,
   });
@@ -59,6 +62,7 @@ const Menu = React.forwardRef(function Menu<RootComponentType extends React.Elem
     actions,
     () => ({
       dispatch,
+      resetHighlight: () => dispatch({ type: ListActionTypes.resetHighlight, event: null }),
     }),
     [dispatch],
   );
@@ -107,8 +111,7 @@ Menu.propTypes /* remove-proptypes */ = {
   // |     To update them edit TypeScript types and run "yarn proptypes"  |
   // ----------------------------------------------------------------------
   /**
-   * A ref with imperative actions.
-   * It allows to select the first or last menu item.
+   * A ref with imperative actions that can be performed on the menu.
    */
   actions: refType,
   /**
@@ -133,6 +136,10 @@ Menu.propTypes /* remove-proptypes */ = {
    * @ignore
    */
   listboxId: PropTypes.string,
+  /**
+   * Function called when the items displayed in the menu change.
+   */
+  onItemsChange: PropTypes.func,
   /**
    * Triggered when focus leaves the menu and the menu should close.
    */
diff --git a/packages/mui-base/src/Menu/Menu.types.ts b/packages/mui-base/src/Menu/Menu.types.ts
--- a/packages/mui-base/src/Menu/Menu.types.ts
+++ b/packages/mui-base/src/Menu/Menu.types.ts
@@ -9,13 +9,19 @@ export interface MenuRootSlotPropsOverrides {}
 export interface MenuListboxSlotPropsOverrides {}
 
 export interface MenuActions {
+  /**
+   * Dispatches an action that can cause a change to the menu's internal state.
+   */
   dispatch: (action: ListAction<string>) => void;
+  /**
+   * Resets the highlighted item.
+   */
+  resetHighlight: () => void;
 }
 
 export interface MenuOwnProps {
   /**
-   * A ref with imperative actions.
-   * It allows to select the first or last menu item.
+   * A ref with imperative actions that can be performed on the menu.
    */
   actions?: React.Ref<MenuActions>;
   /**
@@ -28,6 +34,10 @@ export interface MenuOwnProps {
   className?: string;
   defaultOpen?: boolean;
   listboxId?: string;
+  /**
+   * Function called when the items displayed in the menu change.
+   */
+  onItemsChange?: (items: string[]) => void;
   /**
    * Triggered when focus leaves the menu and the menu should close.
    */
diff --git a/packages/mui-base/src/useList/listActions.types.ts b/packages/mui-base/src/useList/listActions.types.ts
--- a/packages/mui-base/src/useList/listActions.types.ts
+++ b/packages/mui-base/src/useList/listActions.types.ts
@@ -1,10 +1,11 @@
 export const ListActionTypes = {
   blur: 'list:blur',
   focus: 'list:focus',
-  keyDown: 'list:keyDown',
   itemClick: 'list:itemClick',
   itemHover: 'list:itemHover',
   itemsChange: 'list:itemsChange',
+  keyDown: 'list:keyDown',
+  resetHighlight: 'list:resetHighlight',
   textNavigation: 'list:textNavigation',
 } as const;
 
@@ -49,6 +50,11 @@ interface ItemsChangeAction<ItemValue> {
   previousItems: ItemValue[];
 }
 
+interface ResetHighlightAction {
+  type: typeof ListActionTypes.resetHighlight;
+  event: React.SyntheticEvent | null;
+}
+
 /**
  * A union of all standard actions that can be dispatched to the list reducer.
  */
@@ -59,4 +65,5 @@ export type ListAction<ItemValue> =
   | ItemHoverAction<ItemValue>
   | ItemsChangeAction<ItemValue>
   | KeyDownAction
+  | ResetHighlightAction
   | TextNavigationAction;
diff --git a/packages/mui-base/src/useList/listReducer.ts b/packages/mui-base/src/useList/listReducer.ts
--- a/packages/mui-base/src/useList/listReducer.ts
+++ b/packages/mui-base/src/useList/listReducer.ts
@@ -413,6 +413,16 @@ function handleItemsChange<ItemValue, State extends ListState<ItemValue>>(
   };
 }
 
+function handleResetHighlight<ItemValue, State extends ListState<ItemValue>>(
+  state: State,
+  context: ListActionContext<ItemValue>,
+) {
+  return {
+    ...state,
+    highlightedValue: moveHighlight(null, 'reset', context),
+  };
+}
+
 export default function listReducer<ItemValue, State extends ListState<ItemValue>>(
   state: State,
   action: ListReducerAction<ItemValue> & { context: ListActionContext<ItemValue> },
@@ -430,6 +440,8 @@ export default function listReducer<ItemValue, State extends ListState<ItemValue
       return handleTextNavigation(state, action.searchString, context);
     case ListActionTypes.itemsChange:
       return handleItemsChange(action.items, action.previousItems, state, context);
+    case ListActionTypes.resetHighlight:
+      return handleResetHighlight(state, context);
     default:
       return state;
   }
diff --git a/packages/mui-base/src/useList/useList.ts b/packages/mui-base/src/useList/useList.ts
--- a/packages/mui-base/src/useList/useList.ts
+++ b/packages/mui-base/src/useList/useList.ts
@@ -83,6 +83,7 @@ function useList<
     getItemAsString = defaultItemStringifier,
     onChange,
     onHighlightChange,
+    onItemsChange,
     orientation = 'vertical',
     pageSize = 5,
     reducerActionContext = EMPTY_OBJECT as CustomActionContext,
@@ -251,7 +252,8 @@ function useList<
     });
 
     previousItems.current = items;
-  }, [items, itemComparer, dispatch]);
+    onItemsChange?.(items);
+  }, [items, itemComparer, dispatch, onItemsChange]);
 
   // Subitems are notified of changes to the highlighted and selected values.
   // This is not done via context because we don't want to trigger a re-render of all the subitems each time any of them changes state.
diff --git a/packages/mui-base/src/useList/useList.types.ts b/packages/mui-base/src/useList/useList.types.ts
--- a/packages/mui-base/src/useList/useList.types.ts
+++ b/packages/mui-base/src/useList/useList.types.ts
@@ -157,6 +157,12 @@ export interface UseListParameters<
     option: ItemValue | null,
     reason: string,
   ) => void;
+  /**
+   * Callback fired when the items change.
+   *
+   * @param items The new items collection
+   */
+  onItemsChange?: (items: ItemValue[]) => void;
   /**
    * Callback fired when the any of the state items change.
    * Note that in case of `selectedValues` and `highlightedValue` the strongly typed
diff --git a/packages/mui-base/src/useMenu/useMenu.ts b/packages/mui-base/src/useMenu/useMenu.ts
--- a/packages/mui-base/src/useMenu/useMenu.ts
+++ b/packages/mui-base/src/useMenu/useMenu.ts
@@ -24,7 +24,13 @@ import menuReducer from './menuReducer';
  * - [useMenu API](https://mui.com/base-ui/react-menu/hooks-api/#use-menu)
  */
 export default function useMenu(parameters: UseMenuParameters = {}): UseMenuReturnValue {
-  const { defaultOpen, listboxRef: listboxRefProp, open: openProp, onOpenChange } = parameters;
+  const {
+    defaultOpen,
+    listboxRef: listboxRefProp,
+    open: openProp,
+    onItemsChange,
+    onOpenChange,
+  } = parameters;
 
   const listboxRef = React.useRef<HTMLElement | null>(null);
   const handleRef = useForkRef(listboxRef, listboxRefProp);
@@ -83,6 +89,7 @@ export default function useMenu(parameters: UseMenuParameters = {}): UseMenuRetu
     getItemAsString: (id: string) =>
       subitems.get(id)?.label || subitems.get(id)?.ref.current?.innerText,
     rootRef: handleRef,
+    onItemsChange,
     onStateChange: stateChangeHandler,
     reducerActionContext: { listboxRef },
     selectionMode: 'none',
diff --git a/packages/mui-base/src/useMenu/useMenu.types.ts b/packages/mui-base/src/useMenu/useMenu.types.ts
--- a/packages/mui-base/src/useMenu/useMenu.types.ts
+++ b/packages/mui-base/src/useMenu/useMenu.types.ts
@@ -15,6 +15,10 @@ export interface UseMenuParameters {
    * This is the controlled equivalent of the `defaultOpen` parameter.
    */
   open?: boolean;
+  /**
+   * Callback fired when the menu items change.
+   */
+  onItemsChange?: (items: string[]) => void;
   /**
    * Callback fired when the menu is opened or closed.
    */
diff --git a/packages/mui-joy/src/Menu/Menu.tsx b/packages/mui-joy/src/Menu/Menu.tsx
--- a/packages/mui-joy/src/Menu/Menu.tsx
+++ b/packages/mui-joy/src/Menu/Menu.tsx
@@ -4,6 +4,7 @@ import { unstable_capitalize as capitalize, HTMLElementType, refType } from '@mu
 import { OverridableComponent } from '@mui/types';
 import composeClasses from '@mui/base/composeClasses';
 import useMenu, { MenuProvider } from '@mui/base/useMenu';
+import { ListActionTypes } from '@mui/base/useList';
 import Popper from '@mui/base/Popper';
 import { useSlotProps } from '@mui/base/utils';
 import { StyledList } from '../List/List';
@@ -91,6 +92,7 @@ const Menu = React.forwardRef(function Menu(inProps, ref: React.ForwardedRef<HTM
     invertedColors = false,
     id,
     onClose,
+    onItemsChange,
     open = false,
     modifiers: modifiersProp,
     variant = 'outlined',
@@ -114,6 +116,7 @@ const Menu = React.forwardRef(function Menu(inProps, ref: React.ForwardedRef<HTM
   const { contextValue, getListboxProps, dispatch } = useMenu({
     open,
     onOpenChange: handleOpenChange,
+    onItemsChange,
     listboxId: id,
   });
 
@@ -121,6 +124,7 @@ const Menu = React.forwardRef(function Menu(inProps, ref: React.ForwardedRef<HTM
     actions,
     () => ({
       dispatch,
+      resetHighlight: () => dispatch({ type: ListActionTypes.resetHighlight, event: null }),
     }),
     [dispatch],
   );
@@ -295,6 +299,10 @@ Menu.propTypes /* remove-proptypes */ = {
    * Triggered when focus leaves the menu and the menu should close.
    */
   onClose: PropTypes.func,
+  /**
+   * Function called when the items displayed in the menu change.
+   */
+  onItemsChange: PropTypes.func,
   /**
    * Controls whether the menu is displayed.
    * @default false
diff --git a/packages/mui-joy/src/Menu/MenuProps.ts b/packages/mui-joy/src/Menu/MenuProps.ts
--- a/packages/mui-joy/src/Menu/MenuProps.ts
+++ b/packages/mui-joy/src/Menu/MenuProps.ts
@@ -54,6 +54,10 @@ export interface MenuTypeMap<P = {}, D extends React.ElementType = 'ul'> {
      * Triggered when focus leaves the menu and the menu should close.
      */
     onClose?: () => void;
+    /**
+     * Function called when the items displayed in the menu change.
+     */
+    onItemsChange?: (items: string[]) => void;
     /**
      * Controls whether the menu is displayed.
      * @default false
diff --git a/packages/mui-joy/src/MenuList/MenuList.tsx b/packages/mui-joy/src/MenuList/MenuList.tsx
--- a/packages/mui-joy/src/MenuList/MenuList.tsx
+++ b/packages/mui-joy/src/MenuList/MenuList.tsx
@@ -1,9 +1,10 @@
 import * as React from 'react';
 import PropTypes from 'prop-types';
-import { unstable_capitalize as capitalize } from '@mui/utils';
+import { unstable_capitalize as capitalize, refType } from '@mui/utils';
 import { OverridableComponent } from '@mui/types';
 import composeClasses from '@mui/base/composeClasses';
 import useMenu, { MenuProvider, MenuProviderValue } from '@mui/base/useMenu';
+import { ListActionTypes } from '@mui/base/useList';
 import { styled, useThemeProps } from '../styles';
 import { useColorInversion } from '../styles/ColorInversion';
 import { StyledList } from '../List/List';
@@ -72,6 +73,7 @@ const MenuList = React.forwardRef(function MenuList(inProps, ref) {
     size = 'md',
     variant = 'outlined',
     color: colorProp = 'neutral',
+    onItemsChange,
     slots = {},
     slotProps = {},
     ...other
@@ -82,12 +84,14 @@ const MenuList = React.forwardRef(function MenuList(inProps, ref) {
   const { contextValue, getListboxProps, dispatch } = useMenu({
     listboxRef: ref,
     listboxId: idProp,
+    onItemsChange,
   });
 
   React.useImperativeHandle(
     actions,
     () => ({
       dispatch,
+      resetHighlight: () => dispatch({ type: ListActionTypes.resetHighlight, event: null }),
     }),
     [dispatch],
   );
@@ -143,14 +147,7 @@ MenuList.propTypes /* remove-proptypes */ = {
    * A ref with imperative actions.
    * It allows to select the first or last menu item.
    */
-  actions: PropTypes.oneOfType([
-    PropTypes.func,
-    PropTypes.shape({
-      current: PropTypes.shape({
-        dispatch: PropTypes.func.isRequired,
-      }),
-    }),
-  ]),
+  actions: refType,
   /**
    * @ignore
    */
@@ -172,6 +169,10 @@ MenuList.propTypes /* remove-proptypes */ = {
    * @ignore
    */
   id: PropTypes.string,
+  /**
+   * Function called when the items displayed in the menu change.
+   */
+  onItemsChange: PropTypes.func,
   /**
    * The size of the component (affect other nested list* components because the `Menu` inherits `List`).
    * @default 'md'
diff --git a/packages/mui-joy/src/MenuList/MenuListProps.ts b/packages/mui-joy/src/MenuList/MenuListProps.ts
--- a/packages/mui-joy/src/MenuList/MenuListProps.ts
+++ b/packages/mui-joy/src/MenuList/MenuListProps.ts
@@ -37,6 +37,10 @@ export interface MenuListTypeMap<P = {}, D extends React.ElementType = 'ul'> {
      * @default 'neutral'
      */
     color?: OverridableStringUnion<ColorPaletteProp, MenuListPropsColorOverrides>;
+    /**
+     * Function called when the items displayed in the menu change.
+     */
+    onItemsChange?: (items: string[]) => void;
     /**
      * The size of the component (affect other nested list* components because the `Menu` inherits `List`).
      * @default 'md'


Test patch:
diff --git a/packages/mui-base/src/Menu/Menu.test.tsx b/packages/mui-base/src/Menu/Menu.test.tsx
--- a/packages/mui-base/src/Menu/Menu.test.tsx
+++ b/packages/mui-base/src/Menu/Menu.test.tsx
@@ -358,4 +358,26 @@ describe('<Menu />', () => {
       });
     });
   });
+
+  describe('prop: onItemsChange', () => {
+    it('should be called when the menu items change', () => {
+      const handleItemsChange = spy();
+
+      const { setProps } = render(
+        <Menu {...defaultProps} onItemsChange={handleItemsChange}>
+          <MenuItem key="1">1</MenuItem>
+          <MenuItem key="2">2</MenuItem>
+        </Menu>,
+      );
+
+      // The first call is the initial render.
+      expect(handleItemsChange.callCount).to.equal(1);
+
+      setProps({
+        children: [<MenuItem key="1">1</MenuItem>, <MenuItem key="3">3</MenuItem>],
+      });
+
+      expect(handleItemsChange.callCount).to.equal(2);
+    });
+  });
 });
diff --git a/packages/mui-base/src/useList/listReducer.test.ts b/packages/mui-base/src/useList/listReducer.test.ts
--- a/packages/mui-base/src/useList/listReducer.test.ts
+++ b/packages/mui-base/src/useList/listReducer.test.ts
@@ -1057,4 +1057,60 @@ describe('listReducer', () => {
       });
     });
   });
+
+  describe('action: resetHighlight', () => {
+    it('highlights the first item', () => {
+      const state: ListState<string> = {
+        highlightedValue: 'three',
+        selectedValues: [],
+      };
+
+      const action: ListReducerAction<string> = {
+        type: ListActionTypes.resetHighlight,
+        event: null,
+        context: {
+          items: ['one', 'two', 'three'],
+          disableListWrap: false,
+          disabledItemsFocusable: false,
+          focusManagement: 'DOM',
+          isItemDisabled: () => false,
+          itemComparer: (o, v) => o === v,
+          getItemAsString: (option) => option,
+          orientation: 'vertical',
+          pageSize: 5,
+          selectionMode: 'none',
+        },
+      };
+
+      const result = listReducer(state, action);
+      expect(result.highlightedValue).to.equal('one');
+    });
+
+    it('highlights the first non-disabled item', () => {
+      const state: ListState<string> = {
+        highlightedValue: 'three',
+        selectedValues: [],
+      };
+
+      const action: ListReducerAction<string> = {
+        type: ListActionTypes.resetHighlight,
+        event: null,
+        context: {
+          items: ['one', 'two', 'three'],
+          disableListWrap: false,
+          disabledItemsFocusable: false,
+          focusManagement: 'DOM',
+          isItemDisabled: (item) => item === 'one',
+          itemComparer: (o, v) => o === v,
+          getItemAsString: (option) => option,
+          orientation: 'vertical',
+          pageSize: 5,
+          selectionMode: 'none',
+        },
+      };
+
+      const result = listReducer(state, action);
+      expect(result.highlightedValue).to.equal('two');
+    });
+  });
 });