Popup virtual elements (#1449)

* 1433: POC for comments (+ fix build.watch())

* 1433: consolidate virtualAnchor into anchor

* add virtual element examples

* update changelog

---------

Co-authored-by: Marko <marko@modelcitizen.com>
pull/1459/head
Cory LaViska 2023-07-13 16:49:57 -04:00 zatwierdzone przez GitHub
rodzic 2e2a683d11
commit e8634e4178
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
5 zmienionych plików z 197 dodań i 7 usunięć

Wyświetl plik

@ -98,6 +98,7 @@
"minlength",
"monospace",
"mousedown",
"mousemove",
"mouseup",
"multiselectable",
"nextjs",

Wyświetl plik

@ -1511,3 +1511,178 @@ const App = () => {
);
};
```
### Virtual Elements
In most cases, popups are anchored to an actual element. Sometimes, it can be useful to anchor them to a non-element. To do this, you can pass a `VirtualElement` to the anchor property. A virtual element must contain a function called `getBoundingClientRect()` that returns a [`DOMRect`](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) object as shown below.
```ts
const virtualElement = {
getBoundingClientRect() {
// ...
return { width, height, x, y, top, left, right, bottom };
}
};
```
This example anchors a popup to the mouse cursor using a virtual element. As such, a mouse is required to properly view it.
```html:preview
<div class="popup-virtual-element">
<sl-popup placement="right-start">
<div class="circle"></div>
</sl-popup>
<sl-switch>Highlight mouse cursor</sl-switch>
</div>
<script>
const container = document.querySelector('.popup-virtual-element');
const popup = container.querySelector('sl-popup');
const circle = container.querySelector('.circle');
const enabled = container.querySelector('sl-switch');
let clientX = 0;
let clientY = 0;
// Set the virtual element as a property
popup.anchor = {
getBoundingClientRect() {
return {
width: 0,
height: 0,
x: clientX,
y: clientY,
top: clientY,
left: clientX,
right: clientX,
bottom: clientY
};
}
};
// Only activate the popup when the switch is checked
enabled.addEventListener('sl-change', () => {
popup.active = enabled.checked;
});
// Listen for the mouse to move
document.addEventListener('mousemove', handleMouseMove);
// Update the virtual element as the mouse moves
function handleMouseMove(event) {
clientX = event.clientX;
clientY = event.clientY;
// Reposition the popup when the virtual anchor moves
if (popup.active) {
popup.reposition();
}
}
</script>
<style>
/* If you need to set a z-index, set it on the popup part like this */
.popup-virtual-element sl-popup::part(popup) {
z-index: 1000;
pointer-events: none;
}
.popup-virtual-element .circle {
width: 100px;
height: 100px;
border: solid 4px var(--sl-color-primary-600);
border-radius: 50%;
translate: -50px -50px;
animation: 1s virtual-cursor infinite;
}
@keyframes virtual-cursor {
0% { scale: 1; }
50% { scale: 1.1; }
}
</style>
```
```jsx:react
import { useRef, useState } from 'react';
import { SlPopup, SlSwitch } from '@shoelace-style/shoelace/dist/react';
const css = `
/* If you need to set a z-index, set it on the popup part like this */
.popup-virtual-element sl-popup::part(popup) {
z-index: 1000;
pointer-events: none;
}
.popup-virtual-element .circle {
width: 100px;
height: 100px;
border: solid 4px var(--sl-color-primary-600);
border-radius: 50%;
translate: -50px -50px;
animation: 1s virtual-cursor infinite;
}
@keyframes virtual-cursor {
0% { scale: 1; }
50% { scale: 1.1; }
}
`;
const App = () => {
const [enabled, setEnabled] = useState(false);
const [clientX, setClientX] = useState(0);
const [clientY, setClientY] = useState(0);
const popup = useRef(null);
const circle = useRef(null);
const virtualElement = {
getBoundingClientRect() {
return {
width: 0,
height: 0,
x: clientX,
y: clientY,
top: clientY,
left: clientX,
right: clientX,
bottom: clientY
};
}
};
// Listen for the mouse to move
document.addEventListener('mousemove', handleMouseMove);
// Update the virtual element as the mouse moves
function handleMouseMove(event) {
setClientX(event.clientX);
setClientY(event.clientY);
// Reposition the popup when the virtual anchor moves
if (popup.active) {
popup.current.reposition();
}
}
return (
<>
<div className="popup-virtual-element">
<SlPopup
ref={popup}
placement="right-start"
active={enabled}
anchor={virtualElement}
>
<div ref={circle} className="circle" />
</SlPopup>
<SlSwitch checked={enabled} onSlChange={event => setEnabled(event.target.checked)}>
Highlight mouse cursor
</SlSwitch>
</div>
<style>{css}</style>
</>
);
};
```

Wyświetl plik

@ -16,6 +16,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
- Added tests for `<sl-qr-code>` [#1416]
- Added support for pressing [[Space]] to select/toggle selected `<sl-menu-item>` elements [#1429]
- Added support for virtual elements in `<sl-popup>` [#1449]
- Fixed a bug in focus trapping of modal elements like `<sl-dialog>`. We now manually handle focus ordering as well as added `offsetParent()` check for tabbable boundaries in Safari. Test cases added for `<sl-dialog>` inside a shadowRoot [#1403]
- Fixed a bug in `valueAsDate` on `<sl-input>` where it would always set `type="date"` for the underlying `<input>` element. It now falls back to the native browser implementation for the in-memory input. This may cause unexpected behavior if you're using `valueAsDate` on any input elements that aren't `type="date"`. [#1399]
- Fixed a bug in `<sl-qr-code>` where the `background` attribute was never passed to the QR code [#1416]

Wyświetl plik

@ -263,13 +263,16 @@ if (serve) {
});
// Rebuild and reload when source files change
bs.watch(['src/**/!(*.test).*']).on('change', async filename => {
bs.watch('src/**/!(*.test).*').on('change', async filename => {
console.log('updated file: ', filename);
try {
const isTheme = /^src\/themes/.test(filename);
const isStylesheet = /(\.css|\.styles\.ts)$/.test(filename);
// Rebuild the source
await Promise.all([buildResults.map(result => result.rebuild())]);
const rebuildResults = buildResults.map(result => result.rebuild());
await Promise.all(rebuildResults);
// Rebuild stylesheets when a theme file changes
if (isTheme) {

Wyświetl plik

@ -35,11 +35,20 @@ import type { CSSResultGroup } from 'lit';
* popup can be before overflowing. Useful for positioning child elements that need to overflow. This property is only
* available when using `auto-size`.
*/
export interface VirtualElement {
getBoundingClientRect: () => DOMRect;
}
function isVirtualElement(e: unknown): e is VirtualElement {
return e !== null && typeof e === 'object' && 'getBoundingClientRect' in e;
}
@customElement('sl-popup')
export default class SlPopup extends ShoelaceElement {
static styles: CSSResultGroup = styles;
private anchorEl: Element | null;
private anchorEl: Element | VirtualElement | null;
private cleanup: ReturnType<typeof autoUpdate> | undefined;
/** A reference to the internal popup container. Useful for animating and styling the popup with JavaScript. */
@ -47,10 +56,11 @@ export default class SlPopup extends ShoelaceElement {
@query('.popup__arrow') private arrowEl: HTMLElement;
/**
* The element the popup will be anchored to. If the anchor lives outside of the popup, you can provide its `id` or a
* reference to it here. If the anchor lives inside the popup, use the `anchor` slot instead.
* The element the popup will be anchored to. If the anchor lives outside of the popup, you can provide the anchor
* element `id`, a DOM element reference, or a `VirtualElement`. If the anchor lives inside the popup, use the
* `anchor` slot instead.
*/
@property() anchor: Element | string;
@property() anchor: Element | string | VirtualElement;
/**
* Activates the positioning logic and shows the popup. When this attribute is removed, the positioning logic is torn
@ -224,7 +234,7 @@ export default class SlPopup extends ShoelaceElement {
// Locate the anchor by id
const root = this.getRootNode() as Document | ShadowRoot;
this.anchorEl = root.getElementById(this.anchor);
} else if (this.anchor instanceof Element) {
} else if (this.anchor instanceof Element || isVirtualElement(this.anchor)) {
// Use the anchor's reference
this.anchorEl = this.anchor;
} else {