Skip to content

Commit

Permalink
[Material][Popover] Add support for virtual element as anchorEl (#37465)
Browse files Browse the repository at this point in the history
  • Loading branch information
DiegoAndai committed Jun 6, 2023
1 parent 90f2f79 commit 7aa3fab
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 6 deletions.
58 changes: 58 additions & 0 deletions docs/data/material/components/popover/VirtualElementPopover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';

export default function VirtualElementPopover() {
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);

const handleClose = () => {
setOpen(false);
};

const handleMouseUp = () => {
const selection = window.getSelection();

// Skip if selection has a length of 0
if (!selection || selection.anchorOffset === selection.focusOffset) {
return;
}

const getBoundingClientRect = () => {
return selection.getRangeAt(0).getBoundingClientRect();
};

setOpen(true);

setAnchorEl({ getBoundingClientRect, nodeType: 1 });
};

const id = open ? 'virtual-element-popover' : undefined;

return (
<div>
<Typography aria-describedby={id} onMouseUp={handleMouseUp}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ipsum purus,
bibendum sit amet vulputate eget, porta semper ligula. Donec bibendum
vulputate erat, ac fringilla mi finibus nec. Donec ac dolor sed dolor
porttitor blandit vel vel purus. Fusce vel malesuada ligula. Nam quis
vehicula ante, eu finibus est. Proin ullamcorper fermentum orci, quis finibus
massa. Nunc lobortis, massa ut rutrum ultrices, metus metus finibus ex, sit
amet facilisis neque enim sed neque. Quisque accumsan metus vel maximus
consequat. Suspendisse lacinia tellus a libero volutpat maximus.
</Typography>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
onClose={handleClose}
>
<Paper>
<Typography sx={{ p: 2 }}>The content of the Popover.</Typography>
</Paper>
</Popover>
</div>
);
}
58 changes: 58 additions & 0 deletions docs/data/material/components/popover/VirtualElementPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import Popover, { PopoverProps } from '@mui/material/Popover';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';

export default function VirtualElementPopover() {
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState<PopoverProps['anchorEl']>(null);

const handleClose = () => {
setOpen(false);
};

const handleMouseUp = () => {
const selection = window.getSelection();

// Skip if selection has a length of 0
if (!selection || selection.anchorOffset === selection.focusOffset) {
return;
}

const getBoundingClientRect = () => {
return selection.getRangeAt(0).getBoundingClientRect();
};

setOpen(true);

setAnchorEl({ getBoundingClientRect, nodeType: 1 });
};

const id = open ? 'virtual-element-popover' : undefined;

return (
<div>
<Typography aria-describedby={id} onMouseUp={handleMouseUp}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ipsum purus,
bibendum sit amet vulputate eget, porta semper ligula. Donec bibendum
vulputate erat, ac fringilla mi finibus nec. Donec ac dolor sed dolor
porttitor blandit vel vel purus. Fusce vel malesuada ligula. Nam quis
vehicula ante, eu finibus est. Proin ullamcorper fermentum orci, quis finibus
massa. Nunc lobortis, massa ut rutrum ultrices, metus metus finibus ex, sit
amet facilisis neque enim sed neque. Quisque accumsan metus vel maximus
consequat. Suspendisse lacinia tellus a libero volutpat maximus.
</Typography>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
onClose={handleClose}
>
<Paper>
<Typography sx={{ p: 2 }}>The content of the Popover.</Typography>
</Paper>
</Popover>
</div>
);
}
27 changes: 27 additions & 0 deletions docs/data/material/components/popover/popover.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,33 @@ This demo demonstrates how to use the `Popover` component and the mouseover even

{{"demo": "MouseOverPopover.js"}}

## Virtual element

The value of the `anchorEl` prop can be a reference to a fake DOM element.
You need to provide an object with the following interface:

```ts
interface PopoverVirtualElement {
nodeType: 1;
getBoundingClientRect: () => DOMRect;
}
```

Highlight part of the text to see the popover:

{{"demo": "VirtualElementPopover.js"}}

For more information on the virtual element's properties, see the following resources:

- [getBoundingClientRect](https://developer.mozilla.org/docs/Web/API/Element/getBoundingClientRect)
- [DOMRect](https://drafts.fxtf.org/geometry-1/#domrectreadonly)
- [Node types](https://developer.mozilla.org/docs/Web/API/Node/nodeType)

:::warning
The usage of a virtual element for the Popover component requires the `nodeType` property.
This is different from virtual elements used for the [`Popper`](/material-ui/react-popper/#virtual-element) or [`Tooltip`](/material-ui/react-tooltip/#virtual-element) components, both of which don't require the property.
:::

## Complementary projects

For more advanced use cases, you might be able to take advantage of:
Expand Down
2 changes: 1 addition & 1 deletion docs/translations/api-docs/popover/popover.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"componentDescription": "",
"propDescriptions": {
"action": "A ref for imperative actions. It currently only supports updatePosition() action.",
"anchorEl": "An HTML element, or a function that returns one. It&#39;s used to set the position of the popover.",
"anchorEl": "An HTML element, <a href=\"/material-ui/react-popover/#virtual-element\">PopoverVirtualElement</a>, or a function that returns either. It&#39;s used to set the position of the popover.",
"anchorOrigin": "This is the point on the anchor where the popover&#39;s <code>anchorEl</code> will attach to. This is not used when the anchorReference is &#39;anchorPosition&#39;.<br>Options: vertical: [top, center, bottom]; horizontal: [left, center, right].",
"anchorPosition": "This is the position that may be used to set the position of the popover. The coordinates are relative to the application&#39;s client area.",
"anchorReference": "This determines which anchor prop to refer to when setting the position of the popover.",
Expand Down
15 changes: 13 additions & 2 deletions packages/mui-material/src/Popover/Popover.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export interface PopoverPosition {

export type PopoverReference = 'anchorEl' | 'anchorPosition' | 'none';

interface PopoverVirtualElement {
getBoundingClientRect: () => DOMRect;
nodeType: Node['ELEMENT_NODE'];
}

export interface PopoverProps
extends StandardProps<Omit<ModalProps, 'slots' | 'slotProps'>, 'children'> {
/**
Expand All @@ -28,10 +33,16 @@ export interface PopoverProps
*/
action?: React.Ref<PopoverActions>;
/**
* An HTML element, or a function that returns one.
* An HTML element, [PopoverVirtualElement](/material-ui/react-popover/#virtual-element),
* or a function that returns either.
* It's used to set the position of the popover.
*/
anchorEl?: null | Element | ((element: Element) => Element);
anchorEl?:
| null
| Element
| (() => Element)
| PopoverVirtualElement
| (() => PopoverVirtualElement);
/**
* This is the point on the anchor where the popover's
* `anchorEl` will attach to. This is not used when the
Expand Down
5 changes: 3 additions & 2 deletions packages/mui-material/src/Popover/Popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,8 @@ Popover.propTypes /* remove-proptypes */ = {
*/
action: refType,
/**
* An HTML element, or a function that returns one.
* An HTML element, [PopoverVirtualElement](/material-ui/react-popover/#virtual-element),
* or a function that returns either.
* It's used to set the position of the popover.
*/
anchorEl: chainPropTypes(PropTypes.oneOfType([HTMLElementType, PropTypes.func]), (props) => {
Expand Down Expand Up @@ -457,7 +458,7 @@ Popover.propTypes /* remove-proptypes */ = {
return new Error(
[
'MUI: The `anchorEl` prop provided to the component is invalid.',
`It should be an Element instance but it's \`${resolvedAnchorEl}\` instead.`,
`It should be an Element or PopoverVirtualElement instance but it's \`${resolvedAnchorEl}\` instead.`,
].join('\n'),
);
}
Expand Down
34 changes: 33 additions & 1 deletion packages/mui-material/src/Popover/Popover.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,38 @@ describe('<Popover />', () => {
);
expect(anchorElSpy.callCount).to.be.greaterThanOrEqual(1);
});

it('should accept a virtual element', () => {
const top = 100;
const left = 300;
const virtualElement = {
nodeType: 1,
getBoundingClientRect: () => ({
x: 0,
y: 0,
top,
left,
bottom: 0,
right: 0,
height: 0,
width: 0,
}),
};
render(
<Popover
open
anchorEl={virtualElement}
transitionDuration={0}
slotProps={{ paper: { 'data-testid': 'paper' } }}
>
<div />
</Popover>,
);
expect(screen.getByTestId('paper')).toHaveInlineStyle({
top: `${top}px`,
left: `${left}px`,
});
});
});

describe('positioning on an anchor', () => {
Expand Down Expand Up @@ -569,7 +601,7 @@ describe('<Popover />', () => {
'prop',
'MockedPopover',
);
}).toErrorDev('It should be an Element instance');
}).toErrorDev('It should be an Element or PopoverVirtualElement instance');
});

it('warns if a component for the Paper is used that cant hold a ref', () => {
Expand Down

0 comments on commit 7aa3fab

Please sign in to comment.