diff --git a/docs/components/popup.md b/docs/components/popup.md index 4de3f3944..bfd418cac 100644 --- a/docs/components/popup.md +++ b/docs/components/popup.md @@ -219,7 +219,7 @@ const App = () => { ## Examples -### Active +### Activating Popups are inactive and hidden until the `active` attribute is applied. Removing the attribute will tear down all positioning logic and listeners, meaning you can have many idle popups on the page without affecting performance. @@ -304,6 +304,35 @@ const App = () => { }; ``` +### External Anchors + +By default, anchors are slotted into the popup using the `anchor` slot. If your anchor needs to live outside of the popup, you can pass the anchor's `id` to the `anchor` attribute. Alternatively, you can pass an element reference to the `anchor` property to achieve the same effect without using an `id`. + +```html preview + + + +
+
+ + +``` + ### Placement Use the `placement` attribute to tell the popup the preferred placement of the popup. Note that the actual position will vary to ensure the panel remains in the viewport if you're using positioning features such as `flip` and `shift`. diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 816c08bd4..4849a052e 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -11,6 +11,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis ## Next - 🚨 BREAKING: removed the `base` part from `` and removed an unnecessary `
` that made styling more difficult +- Added the `anchor` property to `` to support external anchors - Added read-only custom properties `--auto-size-available-width` and `--auto-size-available-height` to `` to improve support for overflowing popup content - Added `label` to `` to improve accessibility for screen readers - Fixed a bug where auto-size wasn't being applied to `` and `` diff --git a/src/components/popup/popup.ts b/src/components/popup/popup.ts index fbf5bb1f7..bc11d6e68 100644 --- a/src/components/popup/popup.ts +++ b/src/components/popup/popup.ts @@ -14,7 +14,8 @@ import type { CSSResultGroup } from 'lit'; * operations in your listener or consider debouncing it. * * @slot - The popup's content. - * @slot anchor - The element the popup will be anchored to. + * @slot anchor - The element the popup will be anchored to. If the anchor lives outside of the popup, you can use the + * `anchor` attribute or property instead. * * @csspart arrow - The arrow's container. Avoid setting `top|bottom|left|right` properties, as these values are * assigned dynamically as the popup moves. This is most useful for applying a background color to match the popup, and @@ -39,9 +40,15 @@ export default class SlPopup extends LitElement { @query('.popup') public popup: HTMLElement; @query('.popup__arrow') private arrowEl: HTMLElement; - private anchor: HTMLElement | null; + private anchorEl: HTMLElement | null; private cleanup: ReturnType | undefined; + /** + * 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. + */ + @property() anchor: Element | string; + /** * Activates the positioning logic and shows the popup. When this attribute is removed, the positioning logic is torn * down and the popup will be hidden. @@ -174,19 +181,31 @@ export default class SlPopup extends LitElement { this.stop(); } - async handleAnchorSlotChange() { + async handleAnchorChange() { await this.stop(); - this.anchor = this.querySelector('[slot="anchor"]'); + if (this.anchor && typeof this.anchor === 'string') { + // Locate the anchor by id + const root = this.getRootNode() as Document | ShadowRoot; + this.anchorEl = root.getElementById(this.anchor); + } else if (this.anchor instanceof HTMLElement) { + // Use the anchor's reference + this.anchorEl = this.anchor; + } else { + // Look for a slotted anchor + this.anchorEl = this.querySelector('[slot="anchor"]'); + } // If the anchor is a , we'll use the first assigned element as the target since slots use `display: contents` // and positioning can't be calculated on them - if (this.anchor instanceof HTMLSlotElement) { - this.anchor = this.anchor.assignedElements({ flatten: true })[0] as HTMLElement; + if (this.anchorEl instanceof HTMLSlotElement) { + this.anchorEl = this.anchorEl.assignedElements({ flatten: true })[0] as HTMLElement; } - if (!this.anchor) { - throw new Error('Invalid anchor element: no child with slot="anchor" was found.'); + if (!this.anchorEl) { + throw new Error( + 'Invalid anchor element: no anchor could be found using the anchor slot or the anchor attribute.' + ); } this.start(); @@ -194,11 +213,11 @@ export default class SlPopup extends LitElement { private start() { // We can't start the positioner without an anchor - if (!this.anchor) { + if (!this.anchorEl) { return; } - this.cleanup = autoUpdate(this.anchor, this.popup, () => { + this.cleanup = autoUpdate(this.anchorEl, this.popup, () => { this.reposition(); }); } @@ -221,26 +240,31 @@ export default class SlPopup extends LitElement { async updated(changedProps: Map) { super.updated(changedProps); + // Start or stop the positioner when active changes if (changedProps.has('active')) { - // Start or stop the positioner when active changes if (this.active) { this.start(); } else { this.stop(); } - } else { - // All other properties will trigger a reposition when active - if (this.active) { - await this.updateComplete; - this.reposition(); - } + } + + // Update the anchor when anchor changes + if (changedProps.has('anchor')) { + this.handleAnchorChange(); + } + + // All other properties will trigger a reposition when active + if (this.active) { + await this.updateComplete; + this.reposition(); } } /** Recalculate and repositions the popup. */ reposition() { // Nothing to do if the popup is inactive or the anchor doesn't exist - if (!this.active || !this.anchor) { + if (!this.active || !this.anchorEl) { return; } @@ -303,7 +327,7 @@ export default class SlPopup extends LitElement { ); } - computePosition(this.anchor, this.popup, { + computePosition(this.anchorEl, this.popup, { placement: this.placement, middleware, strategy: this.strategy @@ -336,7 +360,7 @@ export default class SlPopup extends LitElement { render() { return html` - +