diff --git a/packages/trees/package.json b/packages/trees/package.json
index 7cc43a8dc..501636987 100644
--- a/packages/trees/package.json
+++ b/packages/trees/package.json
@@ -50,7 +50,6 @@
"test": "bun test",
"coverage": "bun test --coverage",
"test:e2e": "bun run build && (bun ./test/e2e/check-playwright-binary.ts || bun run test:e2e:installbinary) && bunx playwright test -c test/e2e/playwright.config.ts",
- "test:e2e:docs-anchor": "bun run build && (bun ./test/e2e/check-playwright-binary.ts || bun run test:e2e:installbinary) && PATH_STORE_DOCS_E2E=1 bunx playwright test -c test/e2e/playwright.config.ts test/e2e/path-store-docs-anchor.pw.ts --reporter=line",
"test:e2e:installbinary": "bunx playwright@1.51.1 install chromium",
"test:e2e:server": "vite --config test/e2e/vite.config.ts",
"tsc": "tsgo --noEmit --pretty",
diff --git a/packages/trees/src/path-store/view.tsx b/packages/trees/src/path-store/view.tsx
index dd6723726..d1bf0e66b 100644
--- a/packages/trees/src/path-store/view.tsx
+++ b/packages/trees/src/path-store/view.tsx
@@ -202,10 +202,6 @@ const BLOCKED_CONTEXT_MENU_NAV_KEYS = new Set([
'PageUp',
]);
-const CONTEXT_MENU_ROW_ANCHOR_NAME = '--path-store-context-row';
-let cachedAnchorSupportCss: typeof CSS | null | undefined;
-let cachedAnchorSupportValue: boolean | null = null;
-
function isEventInContextMenu(event: Event): boolean {
for (const entry of event.composedPath()) {
if (!(entry instanceof HTMLElement)) {
@@ -254,25 +250,6 @@ function getContextMenuAnchorTop(
return itemRect.top - scrollRect.top + scrollElement.scrollTop;
}
-function supportsContextMenuAnchorPositioning(): boolean {
- const currentCss = typeof CSS === 'undefined' ? null : CSS;
- if (
- cachedAnchorSupportCss === currentCss &&
- cachedAnchorSupportValue != null
- ) {
- return cachedAnchorSupportValue;
- }
-
- cachedAnchorSupportCss = currentCss;
- cachedAnchorSupportValue =
- currentCss != null &&
- currentCss.supports('anchor-name', CONTEXT_MENU_ROW_ANCHOR_NAME) &&
- currentCss.supports('position-anchor', CONTEXT_MENU_ROW_ANCHOR_NAME) &&
- currentCss.supports('top', 'anchor(top)');
-
- return cachedAnchorSupportValue;
-}
-
function createContextMenuItem(
row: PathStoreTreesVisibleRow,
path: string
@@ -329,8 +306,6 @@ function renderStyledRow(
controller: PathStoreTreesController,
row: PathStoreTreesVisibleRow,
visualFocusPath: string | null,
- contextAnchorName: string | null,
- contextAnchorPath: string | null,
contextHoverPath: string | null,
itemHeight: number,
contextMenuEnabled: boolean,
@@ -361,14 +336,6 @@ function renderStyledRow(
? { 'data-item-focused': true }
: {};
const selectedProps = row.isSelected ? { 'data-item-selected': true } : {};
- const contextAnchorProps =
- contextAnchorPath === targetPath
- ? { 'data-item-context-anchor': 'true' }
- : {};
- const contextAnchorStyle =
- contextAnchorPath === targetPath && contextAnchorName != null
- ? { anchorName: contextAnchorName }
- : undefined;
const contextHoverProps =
contextHoverPath === targetPath
? { 'data-item-context-hover': 'true' }
@@ -424,14 +391,9 @@ function renderStyledRow(
onKeyDown={onKeyDown}
role="treeitem"
tabIndex={row.isFocused ? 0 : -1}
- style={{
- minHeight: `${itemHeight}px`,
- ...contextAnchorStyle,
- ...style,
- }}
+ style={{ minHeight: `${itemHeight}px`, ...style }}
{...focusedProps}
{...selectedProps}
- {...contextAnchorProps}
{...contextHoverProps}
>
{/*
@@ -483,8 +445,6 @@ function renderRangeChildren(
controller: PathStoreTreesController,
range: { start: number; end: number },
activeItemPath: string | null,
- contextAnchorName: string | null,
- contextAnchorPath: string | null,
contextHoverPath: string | null,
itemHeight: number,
contextMenuEnabled: boolean,
@@ -515,8 +475,6 @@ function renderRangeChildren(
controller,
row,
activeItemPath,
- contextAnchorName,
- contextAnchorPath,
contextHoverPath,
itemHeight,
contextMenuEnabled,
@@ -589,8 +547,6 @@ export function PathStoreTreesView({
composition?.contextMenu?.render != null ||
composition?.contextMenu?.onOpen != null ||
composition?.contextMenu?.onClose != null;
- const contextMenuUsesAnchorPositioning =
- supportsContextMenuAnchorPositioning();
const { resolveIcon } = useMemo(
() => createPathStoreIconResolver(icons),
[icons]
@@ -647,11 +603,6 @@ export function PathStoreTreesView({
closeContextMenuRef.current = closeContextMenu;
const updateTriggerPosition = useCallback(
(itemButton: HTMLButtonElement | null): void => {
- if (contextMenuUsesAnchorPositioning) {
- setContextMenuAnchorTop(null);
- return;
- }
-
const nextTop =
itemButton == null
? null
@@ -660,7 +611,7 @@ export function PathStoreTreesView({
previousTop === nextTop ? previousTop : nextTop
);
},
- [contextMenuUsesAnchorPositioning]
+ []
);
const openContextMenuForRow = useCallback(
(row: PathStoreTreesVisibleRow, targetPath: string): void => {
@@ -1185,12 +1136,7 @@ export function PathStoreTreesView({
const guideStyleText = getPathStoreGuideStyleText(
focusedVisibleRow?.ancestorPaths.at(-1) ?? null
);
- const contextAnchorName =
- contextMenuUsesAnchorPositioning && triggerPath != null
- ? CONTEXT_MENU_ROW_ANCHOR_NAME
- : null;
const visualFocusPath = contextMenuState?.path ?? activeItemPath;
- const contextAnchorPath = triggerPath;
const visualContextHoverPath = contextMenuState?.path ?? contextHoverPath;
const triggerButton =
triggerPath == null
@@ -1199,21 +1145,14 @@ export function PathStoreTreesView({
const triggerButtonVisible =
contextMenuEnabled &&
triggerButton != null &&
- triggerPath != null &&
- (contextMenuUsesAnchorPositioning || contextMenuAnchorTop != null);
+ contextMenuAnchorTop != null &&
+ triggerPath != null;
const contextMenuAnchorStyle =
- contextMenuUsesAnchorPositioning && contextAnchorName != null
+ triggerButtonVisible && contextMenuAnchorTop != null
? {
- positionAnchor: contextAnchorName,
- top: 'anchor(top)',
+ top: `${contextMenuAnchorTop}px`,
}
- : !contextMenuUsesAnchorPositioning &&
- triggerButtonVisible &&
- contextMenuAnchorTop != null
- ? {
- top: `${contextMenuAnchorTop}px`,
- }
- : undefined;
+ : undefined;
const openMenuFromTrigger = (): void => {
if (triggerPath == null || triggerButton == null) {
return;
@@ -1278,8 +1217,6 @@ export function PathStoreTreesView({
controller,
range,
visualFocusPath,
- contextAnchorName,
- contextAnchorPath,
visualContextHoverPath,
itemHeight,
contextMenuEnabled,
@@ -1301,8 +1238,6 @@ export function PathStoreTreesView({
controller,
parkedFocusedRow,
visualFocusPath,
- contextAnchorName,
- contextAnchorPath,
visualContextHoverPath,
itemHeight,
contextMenuEnabled,
@@ -1336,9 +1271,6 @@ export function PathStoreTreesView({
diff --git a/packages/trees/test/e2e/path-store-composition.pw.ts b/packages/trees/test/e2e/path-store-composition.pw.ts
index be7791adc..96dc9ea0d 100644
--- a/packages/trees/test/e2e/path-store-composition.pw.ts
+++ b/packages/trees/test/e2e/path-store-composition.pw.ts
@@ -33,6 +33,83 @@ async function pressFocusedRowKey(page: Page, key: string): Promise {
}
test.describe('path-store composition surfaces', () => {
+ test('hovering a scrolled tree does not change the visible slice or scroll position', async ({
+ page,
+ }) => {
+ await page.goto('/test/e2e/fixtures/path-store-composition.html');
+ await page.waitForFunction(
+ () => window.__pathStoreCompositionFixtureReady === true
+ );
+
+ const measurement = await page.evaluate(async () => {
+ const host = document.querySelector('file-tree-container');
+ const shadowRoot = host?.shadowRoot;
+ const scroll = shadowRoot?.querySelector(
+ '[data-file-tree-virtualized-scroll="true"]'
+ );
+ if (!(scroll instanceof HTMLElement)) {
+ return null;
+ }
+
+ const nextFrame = () =>
+ new Promise((resolve) => {
+ requestAnimationFrame(() => resolve());
+ });
+
+ const pickPaths = [
+ 'src/lib/theme.ts',
+ 'src/lib/utils.ts',
+ 'src/index.ts',
+ 'README.md',
+ ];
+ const measure = () => ({
+ rows: pickPaths.map((path) => {
+ const row = shadowRoot?.querySelector(
+ `button[data-item-path="${path}"]`
+ );
+ return row instanceof HTMLElement
+ ? {
+ path,
+ top: row.getBoundingClientRect().top,
+ }
+ : {
+ path,
+ top: null,
+ };
+ }),
+ scrollTop: scroll.scrollTop,
+ });
+
+ scroll.scrollTop = 60;
+ await nextFrame();
+ await nextFrame();
+
+ const before = measure();
+ const hoverRow = shadowRoot?.querySelector(
+ 'button[data-item-path="src/index.ts"]'
+ );
+ if (!(hoverRow instanceof HTMLElement)) {
+ return { before, after: null };
+ }
+
+ hoverRow.dispatchEvent(
+ new PointerEvent('pointerover', { bubbles: true, composed: true })
+ );
+ await nextFrame();
+ await nextFrame();
+
+ return {
+ after: measure(),
+ before,
+ };
+ });
+
+ expect(measurement).not.toBeNull();
+ expect(measurement?.after).not.toBeNull();
+ expect(measurement?.after?.scrollTop).toBe(measurement?.before.scrollTop);
+ expect(measurement?.after?.rows).toEqual(measurement?.before.rows);
+ });
+
test('moves the floating trigger when the active row changes', async ({
page,
}) => {
diff --git a/packages/trees/test/e2e/path-store-docs-anchor.pw.ts b/packages/trees/test/e2e/path-store-docs-anchor.pw.ts
deleted file mode 100644
index 019a371af..000000000
--- a/packages/trees/test/e2e/path-store-docs-anchor.pw.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-const PATH_STORE_DOCS_BASE_URL =
- process.env.PATH_STORE_DOCS_BASE_URL ?? 'http://127.0.0.1:3103';
-
-test.describe('path-store docs route', () => {
- test.skip(
- process.env.PATH_STORE_DOCS_E2E !== '1',
- 'Manual docs-route verification; requires a running docs server.'
- );
-
- test('moves the floating trigger when the active row changes in the docs demo', async ({
- page,
- }) => {
- await page.goto(`${PATH_STORE_DOCS_BASE_URL}/trees-dev/path-store-powered`);
-
- const nextFrame = async () => {
- await page.evaluate(
- () =>
- new Promise((resolve) => {
- requestAnimationFrame(() => resolve());
- })
- );
- };
-
- const getTriggerMeasurement = async (
- path: string
- ): Promise<{
- anchoredPath: string | null;
- rowTop: number;
- triggerTop: number;
- visible: string | null;
- } | null> =>
- page.evaluate((nextPath) => {
- const host = document.querySelector('file-tree-container');
- const shadowRoot = host?.shadowRoot;
- const row = shadowRoot?.querySelector(
- `button[data-item-path="${nextPath}"]`
- );
- if (!(row instanceof HTMLElement)) {
- return null;
- }
-
- const trigger = shadowRoot?.querySelector(
- 'button[data-type="context-menu-trigger"]'
- );
- const anchoredRow = shadowRoot?.querySelector(
- 'button[data-item-context-anchor="true"]'
- );
- if (!(trigger instanceof HTMLElement)) {
- return null;
- }
-
- const triggerRect = trigger.getBoundingClientRect();
- const rowRect = row.getBoundingClientRect();
- return {
- anchoredPath:
- anchoredRow instanceof HTMLElement
- ? (anchoredRow.dataset.itemPath ?? null)
- : null,
- rowTop: rowRect.top,
- triggerTop: triggerRect.top,
- visible: trigger.dataset.visible ?? null,
- };
- }, path);
-
- await expect(
- page.locator('file-tree-container button[data-item-path="arch/"]')
- ).toBeVisible();
-
- const firstRow = page.locator(
- 'file-tree-container button[data-item-path="arch/"]'
- );
- await firstRow.hover();
- await nextFrame();
- await nextFrame();
- const firstMeasurement = await getTriggerMeasurement('arch/');
- expect(firstMeasurement).not.toBeNull();
- expect(firstMeasurement?.visible).toBe('true');
- expect(firstMeasurement?.anchoredPath).toBe('arch/');
-
- const secondRow = page.locator(
- 'file-tree-container button[data-item-path="arch/alpha/boot/"]'
- );
- await secondRow.hover();
- await nextFrame();
- await nextFrame();
- const secondMeasurement = await getTriggerMeasurement('arch/alpha/boot/');
- expect(secondMeasurement).not.toBeNull();
- expect(secondMeasurement?.visible).toBe('true');
- expect(secondMeasurement?.anchoredPath).toBe('arch/alpha/boot/');
- expect(secondMeasurement?.triggerTop).toBeGreaterThan(
- (firstMeasurement?.triggerTop ?? 0) + 20
- );
- });
-});
diff --git a/packages/trees/test/path-store-composition-surfaces.test.ts b/packages/trees/test/path-store-composition-surfaces.test.ts
index 8f5c4d9d7..cde7df237 100644
--- a/packages/trees/test/path-store-composition-surfaces.test.ts
+++ b/packages/trees/test/path-store-composition-surfaces.test.ts
@@ -767,75 +767,6 @@ describe('path-store composition surfaces', () => {
}
});
- test('uses CSS anchor positioning for the floating trigger when supported', async () => {
- const { cleanup, dom } = installDom();
- const originalCss = Reflect.get(globalThis, 'CSS');
- try {
- Object.assign(globalThis, {
- CSS: {
- supports(property: string, value?: string): boolean {
- return (
- (property === 'anchor-name' &&
- value === '--path-store-context-row') ||
- (property === 'position-anchor' &&
- value === '--path-store-context-row') ||
- (property === 'top' && value === 'anchor(top)')
- );
- },
- },
- });
-
- const { PathStoreFileTree } = await import('../src/path-store');
- const mount = dom.window.document.createElement('div');
- dom.window.document.body.appendChild(mount);
-
- const fileTree = new PathStoreFileTree({
- composition: {
- contextMenu: {
- enabled: true,
- },
- },
- flattenEmptyDirectories: true,
- initialExpansion: 'open',
- paths: ['README.md', 'src/index.ts'],
- viewportHeight: 120,
- });
-
- fileTree.render({ containerWrapper: mount });
- await flushDom();
-
- const shadowRoot = fileTree.getFileTreeContainer()?.shadowRoot;
- const itemButton = getItemButton(shadowRoot, dom, 'README.md');
- itemButton.dispatchEvent(
- new dom.window.Event('pointerover', { bubbles: true, composed: true })
- );
- await flushDom();
-
- const contextMenuAnchor = shadowRoot?.querySelector(
- '[data-type="context-menu-anchor"]'
- );
- expect(contextMenuAnchor?.getAttribute('data-anchor-positioning')).toBe(
- 'true'
- );
- expect(itemButton.dataset.itemContextAnchor).toBe('true');
- expect(itemButton.getAttribute('style')).toContain(
- 'anchor-name: --path-store-context-row;'
- );
- expect(contextMenuAnchor?.getAttribute('style')).toContain(
- 'position-anchor: --path-store-context-row;'
- );
-
- fileTree.cleanUp();
- } finally {
- if (originalCss === undefined) {
- Reflect.deleteProperty(globalThis, 'CSS');
- } else {
- Object.assign(globalThis, { CSS: originalCss });
- }
- cleanup();
- }
- });
-
test('adds aria-haspopup=menu only when context menu is enabled', async () => {
const { cleanup, dom } = installDom();
try {