Skip to content

Commit

Permalink
[Menu][base-ui] Add the anchor prop (mui#39297)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaldudak authored and mnajdova committed Oct 9, 2023
1 parent c5da41f commit e83edac
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 5 deletions.
6 changes: 6 additions & 0 deletions docs/pages/base-ui/api/menu.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"props": {
"actions": { "type": { "name": "custom", "description": "ref" } },
"anchor": {
"type": {
"name": "union",
"description": "HTML element<br>&#124;&nbsp;object<br>&#124;&nbsp;func"
}
},
"onItemsChange": { "type": { "name": "func" } },
"slotProps": {
"type": {
Expand Down
1 change: 1 addition & 0 deletions docs/translations/api-docs-base/menu/menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"actions": {
"description": "A ref with imperative actions that can be performed on the menu."
},
"anchor": { "description": "The element based on which the menu is positioned." },
"onItemsChange": {
"description": "Function called when the items displayed in the menu change."
},
Expand Down
64 changes: 64 additions & 0 deletions packages/mui-base/src/Menu/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -409,4 +409,68 @@ describe('<Menu />', () => {
expect(handleItemsChange.callCount).to.equal(2);
});
});

describe('prop: anchor', () => {
it('should be placed near the specified element', async () => {
function TestComponent() {
const [anchor, setAnchor] = React.useState<HTMLElement | null>(null);

return (
<div>
<DropdownContext.Provider value={testContext}>
<Menu
anchor={anchor}
slotProps={{ root: { 'data-testid': 'popup', placement: 'bottom-start' } }}
>
<MenuItem>1</MenuItem>
<MenuItem>2</MenuItem>
</Menu>
</DropdownContext.Provider>
<div data-testid="anchor" style={{ marginTop: '100px' }} ref={setAnchor} />
</div>
);
}

const { getByTestId } = render(<TestComponent />);

const popup = getByTestId('popup');
const anchor = getByTestId('anchor');

const anchorPosition = anchor.getBoundingClientRect();

expect(popup.style.getPropertyValue('transform')).to.equal(
`translate(${anchorPosition.left}px, ${anchorPosition.bottom}px)`,
);
});

it('should be placed at the specified position', async () => {
const boundingRect = {
x: 200,
y: 100,
top: 100,
left: 200,
bottom: 100,
right: 200,
height: 0,
width: 0,
toJSON: () => {},
};

const virtualElement = { getBoundingClientRect: () => boundingRect };

const { getByTestId } = render(
<DropdownContext.Provider value={testContext}>
<Menu
anchor={virtualElement}
slotProps={{ root: { 'data-testid': 'popup', placement: 'bottom-start' } }}
>
<MenuItem>1</MenuItem>
<MenuItem>2</MenuItem>
</Menu>
</DropdownContext.Provider>,
);
const popup = getByTestId('popup');
expect(popup.style.getPropertyValue('transform')).to.equal(`translate(200px, 100px)`);
});
});
});
26 changes: 22 additions & 4 deletions packages/mui-base/src/Menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import { refType } from '@mui/utils';
import { HTMLElementType, refType } from '@mui/utils';
import { PolymorphicComponent } from '../utils/PolymorphicComponent';
import { MenuOwnerState, MenuProps, MenuRootSlotProps, MenuTypeMap } from './Menu.types';
import { getMenuUtilityClass } from './menuClasses';
Expand Down Expand Up @@ -38,12 +38,22 @@ const Menu = React.forwardRef(function Menu<RootComponentType extends React.Elem
props: MenuProps<RootComponentType>,
forwardedRef: React.ForwardedRef<Element>,
) {
const { actions, children, onItemsChange, slotProps = {}, slots = {}, ...other } = props;
const {
actions,
anchor: anchorProp,
children,
onItemsChange,
slotProps = {},
slots = {},
...other
} = props;

const { contextValue, getListboxProps, dispatch, open, triggerElement } = useMenu({
onItemsChange,
});

const anchor = anchorProp ?? triggerElement;

React.useImperativeHandle(
actions,
() => ({
Expand Down Expand Up @@ -79,7 +89,7 @@ const Menu = React.forwardRef(function Menu<RootComponentType extends React.Elem
ownerState,
});

if (open === true && triggerElement == null) {
if (open === true && anchor == null) {
return (
<Root {...rootProps}>
<Listbox {...listboxProps}>
Expand All @@ -90,7 +100,7 @@ const Menu = React.forwardRef(function Menu<RootComponentType extends React.Elem
}

return (
<Popper {...rootProps} open={open} anchorEl={triggerElement} slots={{ root: Root }}>
<Popper {...rootProps} open={open} anchorEl={anchor} slots={{ root: Root }}>
<Listbox {...listboxProps}>
<MenuProvider value={contextValue}>{children}</MenuProvider>
</Listbox>
Expand All @@ -107,6 +117,14 @@ Menu.propTypes /* remove-proptypes */ = {
* A ref with imperative actions that can be performed on the menu.
*/
actions: refType,
/**
* The element based on which the menu is positioned.
*/
anchor: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
HTMLElementType,
PropTypes.object,
PropTypes.func,
]),
/**
* @ignore
*/
Expand Down
6 changes: 5 additions & 1 deletion packages/mui-base/src/Menu/Menu.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Simplify } from '@mui/types';
import { PolymorphicProps, SlotComponentProps } from '../utils';
import { UseMenuListboxSlotProps } from '../useMenu';
import { ListAction } from '../useList';
import { Popper } from '../Popper';
import { Popper, PopperProps } from '../Popper';

export interface MenuRootSlotPropsOverrides {}
export interface MenuListboxSlotPropsOverrides {}
Expand All @@ -24,6 +24,10 @@ export interface MenuOwnProps {
* A ref with imperative actions that can be performed on the menu.
*/
actions?: React.Ref<MenuActions>;
/**
* The element based on which the menu is positioned.
*/
anchor?: PopperProps['anchorEl'];
children?: React.ReactNode;
className?: string;
/**
Expand Down

0 comments on commit e83edac

Please sign in to comment.