kopia lustrzana https://github.com/shoelace-style/shoelace
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
rodzic
2e2a683d11
commit
e8634e4178
|
@ -98,6 +98,7 @@
|
|||
"minlength",
|
||||
"monospace",
|
||||
"mousedown",
|
||||
"mousemove",
|
||||
"mouseup",
|
||||
"multiselectable",
|
||||
"nextjs",
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Ładowanie…
Reference in New Issue