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:
@@ -18,7 +18,14 @@ export { mergeLocaleOverlays } from './locale/merge-locale-overlays';
|
||||
export { resolveLangAttr } from './locale/resolve-lang-attr';
|
||||
export { subscribeAmbientLang } from './locale/subscribe-ambient-lang';
|
||||
export { isMacOS } from './platform';
|
||||
export { tryHidePopover, tryShowPopover } from './popover';
|
||||
export {
|
||||
getPositionedSide,
|
||||
type PositionSide,
|
||||
type PositionSideOffsets,
|
||||
type PositionSideOptions,
|
||||
tryHidePopover,
|
||||
tryShowPopover,
|
||||
} from './popover';
|
||||
export { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from './predicates';
|
||||
export { type RafThrottled, rafThrottle } from './raf-throttle';
|
||||
export { loadScript } from './script';
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
export type PositionSide = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
export interface PositionSideOptions {
|
||||
side: PositionSide;
|
||||
}
|
||||
|
||||
export interface PositionSideOffsets {
|
||||
sideOffset: number;
|
||||
boundaryOffset?: number;
|
||||
}
|
||||
|
||||
const ZERO_OFFSETS: PositionSideOffsets = { sideOffset: 0, boundaryOffset: 0 };
|
||||
|
||||
const OPPOSITE_SIDE: Record<PositionSide, PositionSide> = {
|
||||
top: 'bottom',
|
||||
bottom: 'top',
|
||||
left: 'right',
|
||||
right: 'left',
|
||||
};
|
||||
|
||||
function getSideAvailable(
|
||||
triggerRect: DOMRect,
|
||||
boundaryRect: DOMRect,
|
||||
side: PositionSide,
|
||||
offsets: PositionSideOffsets
|
||||
): number {
|
||||
const boundaryOffset = offsets.boundaryOffset ?? 0;
|
||||
|
||||
switch (side) {
|
||||
case 'top':
|
||||
return triggerRect.top - boundaryRect.top - boundaryOffset - offsets.sideOffset;
|
||||
case 'bottom':
|
||||
return boundaryRect.bottom - triggerRect.bottom - boundaryOffset - offsets.sideOffset;
|
||||
case 'left':
|
||||
return triggerRect.left - boundaryRect.left - boundaryOffset - offsets.sideOffset;
|
||||
case 'right':
|
||||
return boundaryRect.right - triggerRect.right - boundaryOffset - offsets.sideOffset;
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the preferred side against a positioning boundary. */
|
||||
export function getPositionedSide(
|
||||
triggerRect: DOMRect,
|
||||
positionedRect: DOMRect,
|
||||
boundaryRect: DOMRect,
|
||||
opts: PositionSideOptions,
|
||||
offsets: PositionSideOffsets = ZERO_OFFSETS
|
||||
): PositionSide {
|
||||
const preferred = opts.side;
|
||||
const opposite = OPPOSITE_SIDE[preferred];
|
||||
const size = preferred === 'top' || preferred === 'bottom' ? positionedRect.height : positionedRect.width;
|
||||
const preferredSpace = getSideAvailable(triggerRect, boundaryRect, preferred, offsets);
|
||||
|
||||
if (preferredSpace >= size) return preferred;
|
||||
|
||||
const oppositeSpace = getSideAvailable(triggerRect, boundaryRect, opposite, offsets);
|
||||
return oppositeSpace > preferredSpace ? opposite : preferred;
|
||||
}
|
||||
|
||||
export function tryShowPopover(el: HTMLElement | null): void {
|
||||
try {
|
||||
el?.showPopover?.();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getPositionedSide } from '../popover';
|
||||
|
||||
function makeDOMRect(x: number, y: number, width: number, height: number): DOMRect {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
top: y,
|
||||
right: x + width,
|
||||
bottom: y + height,
|
||||
left: x,
|
||||
toJSON: () => ({}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('getPositionedSide', () => {
|
||||
const boundary = makeDOMRect(0, 0, 300, 200);
|
||||
const positioned = makeDOMRect(0, 0, 100, 60);
|
||||
const opts = { side: 'top' } as const;
|
||||
|
||||
it('keeps the preferred side when it fits', () => {
|
||||
const trigger = makeDOMRect(100, 100, 40, 20);
|
||||
|
||||
expect(getPositionedSide(trigger, positioned, boundary, opts)).toBe('top');
|
||||
});
|
||||
|
||||
it('flips vertical sides when the opposite side fits better', () => {
|
||||
const topTrigger = makeDOMRect(100, 10, 40, 20);
|
||||
const bottomTrigger = makeDOMRect(100, 170, 40, 20);
|
||||
|
||||
expect(getPositionedSide(topTrigger, positioned, boundary, opts)).toBe('bottom');
|
||||
expect(getPositionedSide(bottomTrigger, positioned, boundary, { side: 'bottom' })).toBe('top');
|
||||
});
|
||||
|
||||
it('flips horizontal sides when the opposite side fits better', () => {
|
||||
const leftTrigger = makeDOMRect(10, 80, 40, 20);
|
||||
const rightTrigger = makeDOMRect(250, 80, 40, 20);
|
||||
|
||||
expect(getPositionedSide(leftTrigger, positioned, boundary, { side: 'left' })).toBe('right');
|
||||
expect(getPositionedSide(rightTrigger, positioned, boundary, { side: 'right' })).toBe('left');
|
||||
});
|
||||
|
||||
it('chooses the side with more space when neither side fits', () => {
|
||||
const smallBoundary = makeDOMRect(0, 0, 300, 100);
|
||||
const largePositioned = makeDOMRect(0, 0, 100, 80);
|
||||
const trigger = makeDOMRect(100, 30, 40, 10);
|
||||
|
||||
expect(getPositionedSide(trigger, largePositioned, smallBoundary, opts)).toBe('bottom');
|
||||
});
|
||||
|
||||
it('keeps the preferred side when neither side fits and available space is tied', () => {
|
||||
const smallBoundary = makeDOMRect(0, 0, 300, 100);
|
||||
const largePositioned = makeDOMRect(0, 0, 100, 80);
|
||||
const trigger = makeDOMRect(100, 45, 40, 10);
|
||||
|
||||
expect(getPositionedSide(trigger, largePositioned, smallBoundary, opts)).toBe('top');
|
||||
});
|
||||
|
||||
it('includes side and boundary offsets when checking available space', () => {
|
||||
const tallBoundary = makeDOMRect(0, 0, 300, 180);
|
||||
const trigger = makeDOMRect(100, 70, 40, 10);
|
||||
|
||||
expect(getPositionedSide(trigger, positioned, tallBoundary, opts)).toBe('top');
|
||||
expect(
|
||||
getPositionedSide(trigger, positioned, tallBoundary, opts, {
|
||||
sideOffset: 8,
|
||||
boundaryOffset: 4,
|
||||
})
|
||||
).toBe('bottom');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user