mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add flip functionality to popovers/tooltips/menus (#1857)
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
getMenuViewportAttrs,
|
||||
getMenuViewTransitionAttrs,
|
||||
getPopupPositionRect,
|
||||
getPositionedSide,
|
||||
getPositioningBoundaryRect,
|
||||
getRootPositionOptions,
|
||||
isMenuNavigationKey,
|
||||
@@ -263,16 +264,25 @@ export class MenuElement extends MediaElement {
|
||||
const boundaryRect = getPositioningBoundaryRect(boundaryElement);
|
||||
const offsets = resolveOffsets(this);
|
||||
const anchorSupported = supportsAnchorPositioning();
|
||||
const getNextStyle = () =>
|
||||
getAnchorPositionStyle(
|
||||
if (!triggerRect) return;
|
||||
|
||||
const getNextPosition = () => {
|
||||
const popupRect = getPopupPositionRect(this, positionOptions.side);
|
||||
const side = getPositionedSide(triggerRect, popupRect, boundaryRect, positionOptions, offsets);
|
||||
const style = getAnchorPositionStyle(
|
||||
this.id,
|
||||
positionOptions,
|
||||
{ ...positionOptions, side },
|
||||
triggerRect,
|
||||
anchorSupported ? undefined : getPopupPositionRect(this),
|
||||
anchorSupported ? undefined : popupRect,
|
||||
boundaryRect,
|
||||
offsets
|
||||
);
|
||||
let nextStyle = getNextStyle();
|
||||
|
||||
return { side, style };
|
||||
};
|
||||
let nextPosition = getNextPosition();
|
||||
let nextStyle = nextPosition.style;
|
||||
this.setAttribute(MenuDataAttrs.side, nextPosition.side);
|
||||
|
||||
if (anchorSupported) {
|
||||
applyStyles(this, nextStyle);
|
||||
@@ -282,7 +292,9 @@ export class MenuElement extends MediaElement {
|
||||
syncMenuViewRoot(this, this.#navState.stack.length > 0, availableWidth ? { availableWidth } : undefined);
|
||||
|
||||
if (!anchorSupported) {
|
||||
nextStyle = getNextStyle();
|
||||
nextPosition = getNextPosition();
|
||||
nextStyle = nextPosition.style;
|
||||
this.setAttribute(MenuDataAttrs.side, nextPosition.side);
|
||||
applyStyles(this, nextStyle);
|
||||
}
|
||||
|
||||
|
||||
@@ -107,10 +107,40 @@ function expectNoMenuStateAttrs(element: HTMLElement): void {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('MenuElement', () => {
|
||||
it('exposes the positioned side on root content', async () => {
|
||||
const trigger = document.createElement('button');
|
||||
const root = createElement(MenuElement);
|
||||
const view = createElement(MenuViewElement);
|
||||
const item = createElement(MenuItemElement);
|
||||
|
||||
root.id = 'menu';
|
||||
root.open = true;
|
||||
root.side = 'top';
|
||||
root.boundary = 'viewport';
|
||||
trigger.setAttribute('commandfor', root.id);
|
||||
item.textContent = 'Auto';
|
||||
view.append(item);
|
||||
root.append(view);
|
||||
|
||||
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue(new DOMRect(100, 10, 40, 20));
|
||||
vi.spyOn(root, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 100, 60));
|
||||
vi.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 300, 200));
|
||||
Object.defineProperty(root, 'offsetWidth', { configurable: true, value: 100 });
|
||||
Object.defineProperty(root, 'offsetHeight', { configurable: true, value: 5 });
|
||||
Object.defineProperty(root, 'scrollHeight', { configurable: true, value: 60 });
|
||||
root.style.setProperty('--media-popover-available-height', '5px');
|
||||
|
||||
document.body.append(trigger, root);
|
||||
await root.updateComplete;
|
||||
|
||||
expect(root.getAttribute('data-side')).toBe('bottom');
|
||||
});
|
||||
|
||||
it('scopes menu state data attributes to menu elements', async () => {
|
||||
const root = createElement(MenuElement);
|
||||
const label = createElement(MenuGroupLabelElement);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getAnchorNameStyle,
|
||||
getAnchorPositionStyle,
|
||||
getPopupPositionRect,
|
||||
getPositionedSide,
|
||||
getPositioningBoundaryRect,
|
||||
type PopoverApi,
|
||||
type PopoverChangeDetails,
|
||||
@@ -181,18 +182,24 @@ export class PopoverElement extends MediaElement {
|
||||
}
|
||||
|
||||
// Apply positioning styles to self.
|
||||
const posOpts = { side: state.side, align: state.align };
|
||||
const preferredOpts = { side: state.side, align: state.align };
|
||||
const boundaryElement = this.#getBoundaryElement();
|
||||
const triggerRect = this.#currentTrigger?.getBoundingClientRect();
|
||||
const boundaryRect = getPositioningBoundaryRect(boundaryElement);
|
||||
const offsets = resolveOffsets(this);
|
||||
const popupRect = getPopupPositionRect(this, preferredOpts.side);
|
||||
|
||||
if (!triggerRect) return;
|
||||
|
||||
const side = getPositionedSide(triggerRect, popupRect, boundaryRect, preferredOpts, offsets);
|
||||
const posOpts = { ...preferredOpts, side };
|
||||
this.setAttribute(PopoverDataAttrs.side, side);
|
||||
|
||||
if (supportsAnchorPositioning()) {
|
||||
applyStyles(this, getAnchorPositionStyle(this.id, posOpts, triggerRect, undefined, boundaryRect, offsets));
|
||||
} else {
|
||||
// JS fallback: measure rects and resolve CSS var offsets.
|
||||
const selfRect = getPopupPositionRect(this);
|
||||
applyStyles(this, getAnchorPositionStyle(this.id, posOpts, triggerRect, selfRect, boundaryRect, offsets));
|
||||
applyStyles(this, getAnchorPositionStyle(this.id, posOpts, triggerRect, popupRect, boundaryRect, offsets));
|
||||
}
|
||||
|
||||
this.#position.sync(this.#currentTrigger, boundaryElement);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PopoverElement } from '../popover-element';
|
||||
|
||||
let tagCounter = 0;
|
||||
|
||||
function createPopover(): PopoverElement {
|
||||
const tag = `test-popover-${tagCounter++}`;
|
||||
customElements.define(tag, class extends PopoverElement {});
|
||||
return document.createElement(tag) as PopoverElement;
|
||||
}
|
||||
|
||||
function makeDOMRect(x: number, y: number, width: number, height: number): DOMRect {
|
||||
return new DOMRect(x, y, width, height);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('PopoverElement', () => {
|
||||
it('exposes the positioned side on the popup', async () => {
|
||||
const trigger = document.createElement('button');
|
||||
const popover = createPopover();
|
||||
|
||||
popover.id = 'popover';
|
||||
popover.open = true;
|
||||
popover.side = 'top';
|
||||
popover.boundary = 'viewport';
|
||||
trigger.setAttribute('commandfor', popover.id);
|
||||
|
||||
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue(makeDOMRect(100, 10, 40, 20));
|
||||
vi.spyOn(popover, 'getBoundingClientRect').mockReturnValue(makeDOMRect(0, 0, 100, 60));
|
||||
vi.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue(makeDOMRect(0, 0, 300, 200));
|
||||
Object.defineProperty(popover, 'offsetWidth', { configurable: true, value: 100 });
|
||||
Object.defineProperty(popover, 'offsetHeight', { configurable: true, value: 60 });
|
||||
|
||||
document.body.append(trigger, popover);
|
||||
await popover.updateComplete;
|
||||
|
||||
expect(popover.getAttribute('data-side')).toBe('bottom');
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { HOTKEY_SHORTCUT_CHANGE_EVENT, playbackFeature } from '@videojs/core/dom
|
||||
import { registerI18n, resetI18nRegistry } from '@videojs/core/i18n';
|
||||
import { ContextProvider } from '@videojs/element/context';
|
||||
import { createState, createStore } from '@videojs/store';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { MediaI18nProviderElement } from '../../../i18n';
|
||||
import { playerContext } from '../../../player/context';
|
||||
@@ -97,11 +97,30 @@ function setup() {
|
||||
defineElement(TestPlayerProviderElement.tagName, TestPlayerProviderElement);
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
resetI18nRegistry();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('TooltipElement', () => {
|
||||
it('exposes the positioned side on the popup', async () => {
|
||||
const { tooltip, trigger } = setup();
|
||||
|
||||
tooltip.open = true;
|
||||
tooltip.side = 'top';
|
||||
tooltip.boundary = 'viewport';
|
||||
|
||||
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue(new DOMRect(100, 10, 40, 20));
|
||||
vi.spyOn(tooltip, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 100, 60));
|
||||
vi.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 300, 200));
|
||||
Object.defineProperty(tooltip, 'offsetWidth', { configurable: true, value: 100 });
|
||||
Object.defineProperty(tooltip, 'offsetHeight', { configurable: true, value: 60 });
|
||||
|
||||
await tooltip.updateComplete;
|
||||
|
||||
expect(tooltip.getAttribute('data-side')).toBe('bottom');
|
||||
});
|
||||
|
||||
it('creates default label and shortcut elements for empty tooltips', async () => {
|
||||
const { tooltip } = setup();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getAnchorNameStyle,
|
||||
getAnchorPositionStyle,
|
||||
getPopupPositionRect,
|
||||
getPositionedSide,
|
||||
getPositioningBoundaryRect,
|
||||
HOTKEY_SHORTCUT_CHANGE_EVENT,
|
||||
type PositioningBoundary,
|
||||
@@ -205,11 +206,18 @@ export class TooltipElement extends MediaElement {
|
||||
}
|
||||
|
||||
// Apply positioning styles to self.
|
||||
const posOpts = { side: state.side, align: state.align };
|
||||
const preferredOpts = { side: state.side, align: state.align };
|
||||
const boundaryElement = this.#getBoundaryElement();
|
||||
const triggerRect = this.#currentTrigger?.getBoundingClientRect();
|
||||
const boundaryRect = getPositioningBoundaryRect(boundaryElement);
|
||||
const offsets = resolveOffsets(this, TooltipCSSVars);
|
||||
const popupRect = getPopupPositionRect(this, preferredOpts.side);
|
||||
|
||||
if (!triggerRect) return;
|
||||
|
||||
const side = getPositionedSide(triggerRect, popupRect, boundaryRect, preferredOpts, offsets);
|
||||
const posOpts = { ...preferredOpts, side };
|
||||
this.setAttribute(TooltipDataAttrs.side, side);
|
||||
|
||||
if (supportsAnchorPositioning()) {
|
||||
applyStyles(
|
||||
@@ -218,10 +226,9 @@ export class TooltipElement extends MediaElement {
|
||||
);
|
||||
} else {
|
||||
// JS fallback: measure rects and resolve CSS var offsets.
|
||||
const selfRect = getPopupPositionRect(this);
|
||||
applyStyles(
|
||||
this,
|
||||
getAnchorPositionStyle(this.id, posOpts, triggerRect, selfRect, boundaryRect, offsets, TooltipCSSVars)
|
||||
getAnchorPositionStyle(this.id, posOpts, triggerRect, popupRect, boundaryRect, offsets, TooltipCSSVars)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user