Drop hoist from public API, use Popover API where supported (#351)

* First stab at using the Popover API for PE

* fix popup

* First stab at using the Popover API for PE

* fix popup

* Prettier

* Fix TS error

* Remove workaround

* Default to `strategy = fixed` if Popover API is not supported

* Clear out default UA styles for popovers

* Kill `hoist` with fire 🔥

* Refactor

* Update `@floating-ui/dom`

* Fix flipping and shofting

* Fix autosize

* Use `defaultBoundary` for `flip` too

That way we get the previous behavior for it.

* Remove `strategy`, just use `SUPPORTS_POPOVER` check where relevant

* Remove uses of `strategy`

* Use viewport as the default boundary for shifting and autosizing and add `boundary=scroll` to override

---------

Co-authored-by: konnorrogers <konnor5456@gmail.com>
This commit is contained in:
Lea Verou
2025-04-09 16:47:40 -05:00
committed by GitHub
parent deb9fd70b3
commit f13deb87bb
17 changed files with 85 additions and 196 deletions

View File

@@ -210,12 +210,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
/** Disables the color picker. */
@property({ type: Boolean }) disabled = false;
/**
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
* `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
*/
@property({ type: Boolean }) hoist = false;
/** Shows the opacity slider. Enabling this will cause the formatted value to be HEXA, RGBA, or HSLA. */
@property({ type: Boolean }) opacity = false;
@@ -1097,7 +1091,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
aria-disabled=${this.disabled ? 'true' : 'false'}
.containingElement=${this}
?disabled=${this.disabled}
?hoist=${this.hoist}
@wa-after-show=${this.handleAfterShow}
@wa-after-hide=${this.handleAfterHide}
>

View File

@@ -97,13 +97,6 @@ export default class WaCopyButton extends WebAwesomeElement {
/** The preferred placement of the tooltip. */
@property({ attribute: 'tooltip-placement' }) tooltipPlacement: 'top' | 'right' | 'bottom' | 'left' = 'top';
/**
* Enable this option to prevent the tooltip from being clipped when the component is placed inside a container with
* `overflow: auto|hidden|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all,
* scenarios.
*/
@property({ type: Boolean }) hoist = false;
private async handleCopy() {
if (this.disabled || this.isCopying) {
return;
@@ -220,7 +213,6 @@ export default class WaCopyButton extends WebAwesomeElement {
for="copy-button"
placement=${this.tooltipPlacement}
?disabled=${this.disabled}
?hoist=${this.hoist}
exportparts="
base:tooltip__base,
base__popup:tooltip__base__popup,

View File

@@ -95,12 +95,6 @@ export default class WaDropdown extends WebAwesomeElement {
/** The distance in pixels from which to offset the panel along its trigger. */
@property({ type: Number }) skidding = 0;
/**
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
* `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
*/
@property({ type: Boolean }) hoist = false;
/**
* Syncs the popup width or height to that of the trigger element.
*/
@@ -415,7 +409,6 @@ export default class WaDropdown extends WebAwesomeElement {
placement=${this.placement}
distance=${this.distance}
skidding=${this.skidding}
strategy=${this.hoist ? 'fixed' : 'absolute'}
flip
shift
auto-size="vertical"

View File

@@ -275,7 +275,6 @@ export class SubmenuController implements ReactiveController {
flip
flip-fallback-strategy="best-fit"
skidding="${this.skidding}"
strategy="fixed"
auto-size="vertical"
auto-size-padding="10"
>

View File

@@ -19,6 +19,19 @@
isolation: isolate;
max-width: var(--auto-size-available-width, none);
max-height: var(--auto-size-available-height, none);
/* Clear UA styles for [popover] */
:where(&) {
inset: unset;
padding: unset;
margin: unset;
width: unset;
height: unset;
color: unset;
background: unset;
border: unset;
overflow: unset;
}
}
.popup--fixed {

View File

@@ -1,4 +1,14 @@
import { arrow, autoUpdate, computePosition, flip, offset, platform, shift, size } from '@floating-ui/dom';
import {
arrow,
autoUpdate,
computePosition,
flip,
getOverflowAncestors,
offset,
platform,
shift,
size,
} from '@floating-ui/dom';
import { offsetParent } from 'composed-offset-position';
import type { PropertyValues } from 'lit';
import { html } from 'lit';
@@ -23,6 +33,8 @@ function isVirtualElement(e: unknown): e is VirtualElement {
);
}
const SUPPORTS_POPOVER = globalThis?.HTMLElement?.prototype.hasOwnProperty('popover');
/**
* @summary Popup is a utility that lets you declaratively anchor "popup" containers to another element.
* @documentation https://backers.webawesome.com/docs/components/popup
@@ -97,11 +109,8 @@ export default class WaPopup extends WebAwesomeElement {
| 'left-start'
| 'left-end' = 'top';
/**
* Determines how the popup is positioned. The `absolute` strategy works well in most cases, but if overflow is
* clipped, using a `fixed` position strategy can often workaround it.
*/
@property({ reflect: true }) strategy: 'absolute' | 'fixed' = 'absolute';
/** Which bounding box to use for flipping, shifting, and auto-sizing? */
@property() boundary: 'viewport' | 'scroll' = 'viewport';
/** The distance in pixels from which to offset the panel away from its anchor. */
@property({ type: Number }) distance = 0;
@@ -281,6 +290,8 @@ export default class WaPopup extends WebAwesomeElement {
return;
}
this.popup.showPopover?.();
this.cleanup = autoUpdate(this.anchorEl, this.popup, () => {
this.reposition();
});
@@ -288,6 +299,8 @@ export default class WaPopup extends WebAwesomeElement {
private async stop(): Promise<void> {
return new Promise(resolve => {
this.popup.hidePopover?.();
if (this.cleanup) {
this.cleanup();
this.cleanup = undefined;
@@ -334,11 +347,25 @@ export default class WaPopup extends WebAwesomeElement {
this.popup.style.height = '';
}
let overflowBoundary, defaultBoundary;
if (SUPPORTS_POPOVER && !isVirtualElement(this.anchor)) {
// When using the Popover API, the floating element is no longer in the same DOM context
// as the overflow ancestors so Floating-UI can't find them.
// For flip, `elementContext: 'reference'` gets it to use the anchor element instead,
// but the option is not available for shift() or size(), so we basically need to implement it ourselves.
overflowBoundary = getOverflowAncestors(this.anchorEl as Element).filter(el => el instanceof Element);
}
if (this.boundary === 'scroll') {
defaultBoundary = overflowBoundary;
}
// Then we flip
if (this.flip) {
middleware.push(
flip({
boundary: this.flipBoundary,
boundary: this.flipBoundary || overflowBoundary,
// @ts-expect-error - We're converting a string attribute to an array here
fallbackPlacements: this.flipFallbackPlacements,
fallbackStrategy: this.flipFallbackStrategy === 'best-fit' ? 'bestFit' : 'initialPlacement',
@@ -351,7 +378,7 @@ export default class WaPopup extends WebAwesomeElement {
if (this.shift) {
middleware.push(
shift({
boundary: this.shiftBoundary,
boundary: this.shiftBoundary || defaultBoundary,
padding: this.shiftPadding,
}),
);
@@ -361,7 +388,7 @@ export default class WaPopup extends WebAwesomeElement {
if (this.autoSize) {
middleware.push(
size({
boundary: this.autoSizeBoundary,
boundary: this.autoSizeBoundary || defaultBoundary,
padding: this.autoSizePadding,
apply: ({ availableWidth, availableHeight }) => {
if (this.autoSize === 'vertical' || this.autoSize === 'both') {
@@ -399,15 +426,14 @@ export default class WaPopup extends WebAwesomeElement {
//
// More info: https://github.com/shoelace-style/shoelace/issues/1135
//
const getOffsetParent =
this.strategy === 'absolute'
? (element: Element) => platform.getOffsetParent(element, offsetParent)
: platform.getOffsetParent;
const getOffsetParent = SUPPORTS_POPOVER
? (element: Element) => platform.getOffsetParent(element, offsetParent)
: platform.getOffsetParent;
computePosition(this.anchorEl, this.popup, {
placement: this.placement,
middleware,
strategy: this.strategy,
strategy: SUPPORTS_POPOVER ? 'absolute' : 'fixed',
platform: {
...platform,
getOffsetParent,
@@ -563,11 +589,12 @@ export default class WaPopup extends WebAwesomeElement {
></span>
<div
popover="manual"
part="popup"
class=${classMap({
popup: true,
'popup--active': this.active,
'popup--fixed': this.strategy === 'fixed',
'popup--fixed': !SUPPORTS_POPOVER,
'popup--has-arrow': this.arrow,
})}
>

View File

@@ -229,12 +229,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
*/
@property({ type: Boolean, reflect: true }) open = false;
/**
* Enable this option to prevent the listbox from being clipped when the component is placed inside a container with
* `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
*/
@property({ type: Boolean }) hoist = false;
/** The select's visual appearance. */
@property({ reflect: true }) appearance: 'filled' | 'outlined' = 'outlined';
@@ -891,7 +885,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
'placeholder-visible': isPlaceholderVisible,
})}
placement=${this.placement}
strategy=${this.hoist ? 'fixed' : 'absolute'}
flip
shift
sync="width"

View File

@@ -92,13 +92,6 @@ export default class WaTooltip extends WebAwesomeElement {
*/
@property() trigger = 'hover focus';
/**
* Enable this option to prevent the tooltip from being clipped when the component is placed inside a container with
* `overflow: auto|hidden|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all,
* scenarios.
*/
@property({ type: Boolean }) hoist = false;
@property() for: string | null = null;
@state() anchor: null | Element = null;
@@ -290,7 +283,7 @@ export default class WaTooltip extends WebAwesomeElement {
this.anchor = newAnchor;
}
@watch(['distance', 'hoist', 'placement', 'skidding'])
@watch(['distance', 'placement', 'skidding'])
async handleOptionsChange() {
if (this.hasUpdated) {
await this.updateComplete;
@@ -340,7 +333,6 @@ export default class WaTooltip extends WebAwesomeElement {
placement=${this.placement}
distance=${this.distance}
skidding=${this.skidding}
strategy=${this.hoist ? 'fixed' : 'absolute'}
flip
shift
arrow