fix: correct popup fallback positioning offsets (#981)

This commit is contained in:
Sam Potts
2026-03-17 13:14:35 +11:00
committed by GitHub
parent 561d03eb5a
commit 82ede77322
12 changed files with 498 additions and 20 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ export { tryHidePopover, tryShowPopover } from './popover';
export { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from './predicates';
export { type RafThrottled, rafThrottle } from './raf-throttle';
export { getSlottedElement, querySlot } from './slotted';
export { applyStyles } from './style';
export { applyStyles, resolveCSSLength } from './style';
export { supportsAnchorPositioning, supportsAnimationFrame, supportsIdleCallback } from './supports';
export { findTrackElement, getTextTrackList } from './text-track';
export { serializeTimeRanges } from './time-ranges';
+47
View File
@@ -9,3 +9,50 @@ export function applyStyles(element: HTMLElement, styles: Record<string, string
}
}
}
export function resolveCSSLength(el: Element, value: string): number {
const trimmed = value.trim();
if (!trimmed) return 0;
const parsed = Number.parseFloat(trimmed);
if (Number.isNaN(parsed)) return 0;
if (/^-?\d*\.?\d+$/.test(trimmed) || trimmed.endsWith('px')) return parsed;
const doc = el.ownerDocument;
const root = doc?.documentElement;
if (trimmed.endsWith('rem')) {
const rootFontSize = root ? Number.parseFloat(getComputedStyle(root).fontSize) || 16 : 16;
return parsed * rootFontSize;
}
if (trimmed.endsWith('em')) {
const fontSize = el instanceof HTMLElement ? Number.parseFloat(getComputedStyle(el).fontSize) || 16 : 16;
return parsed * fontSize;
}
if (!doc) return parsed;
const measurementEl = doc.createElement('div');
measurementEl.style.position = 'absolute';
measurementEl.style.visibility = 'hidden';
measurementEl.style.pointerEvents = 'none';
measurementEl.style.inlineSize = trimmed;
measurementEl.style.blockSize = '0';
measurementEl.style.padding = '0';
measurementEl.style.border = '0';
measurementEl.style.inset = '0';
const parent = doc.body ?? doc.documentElement;
if (!parent) return parsed;
parent.appendChild(measurementEl);
const pixels = measurementEl.getBoundingClientRect().width;
measurementEl.remove();
return Number.isFinite(pixels) ? pixels : parsed;
}
+101
View File
@@ -0,0 +1,101 @@
import { describe, expect, it, vi } from 'vitest';
import { resolveCSSLength } from '../style';
describe('resolveCSSLength', () => {
it('Returns px values directly', () => {
const el = document.createElement('div');
expect(resolveCSSLength(el, '8px')).toBe(8);
});
it('Resolves rem values using the root font size', () => {
const el = document.createElement('div');
const getComputedStyleSpy = vi.spyOn(globalThis, 'getComputedStyle').mockImplementation(
(target: Element) =>
({
fontSize: target === document.documentElement ? '16px' : '14px',
}) as CSSStyleDeclaration
);
expect(resolveCSSLength(el, '0.5rem')).toBe(8);
getComputedStyleSpy.mockRestore();
});
it('Resolves em values using the element font size', () => {
const el = document.createElement('div');
const getComputedStyleSpy = vi.spyOn(globalThis, 'getComputedStyle').mockImplementation(
() =>
({
fontSize: '14px',
}) as CSSStyleDeclaration
);
expect(resolveCSSLength(el, '1em')).toBe(14);
getComputedStyleSpy.mockRestore();
});
it('Falls back to measurement for other CSS lengths', () => {
const el = document.createElement('div');
const createElement = document.createElement.bind(document);
const appendChildSpy = vi.spyOn(document.body, 'appendChild');
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const node = createElement(tagName);
if (tagName === 'div') {
vi.spyOn(node, 'getBoundingClientRect').mockImplementation(() => {
const width = node.style.inlineSize === '10vw' ? 24 : 0;
return {
x: 0,
y: 0,
width,
height: 0,
top: 0,
left: 0,
right: width,
bottom: 0,
toJSON() {},
};
});
}
return node;
});
expect(resolveCSSLength(el, '10vw')).toBe(24);
expect(appendChildSpy).toHaveBeenCalled();
createElementSpy.mockRestore();
appendChildSpy.mockRestore();
});
it('Preserves measured zero pixel values', () => {
const el = document.createElement('div');
const createElement = document.createElement.bind(document);
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const node = createElement(tagName);
if (tagName === 'div') {
vi.spyOn(node, 'getBoundingClientRect').mockImplementation(() => ({
x: 0,
y: 0,
width: 0,
height: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
toJSON() {},
}));
}
return node;
});
expect(resolveCSSLength(el, 'calc(1px - 1px)')).toBe(0);
createElementSpy.mockRestore();
});
});