Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Material][Popover] Add support for virtual element as anchorEl #37465

Merged
merged 5 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this conditional: selection.anchorOffset === selection.focusOffset?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It checks that the selection doesn't start and ends at the same character. I tested that you could get that by creating the range programmatically, I don't know if you can get it just by interacting with it. In other words is to make sure that the selection spans at least one character.

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