diff --git a/tests/test_file_browser_navigation.py b/tests/test_file_browser_navigation.py index 4ef4ebf84..68bb91339 100644 --- a/tests/test_file_browser_navigation.py +++ b/tests/test_file_browser_navigation.py @@ -123,11 +123,32 @@ def test_file_browser_editor_picker_modes_have_primary_footer_actions() -> None: assert "Open in Editor action visible outside the overflow menu" in dox editor_button_index = html.index("file-editor-open-action") - dropdown_menu_index = html.index('class="dropdown-menu"') + dropdown_menu_index = html.index('class="dropdown-menu file-actions-menu"') assert editor_button_index < dropdown_menu_index assert 'x-show="$store.fileBrowser.canOpenInActionMenu(file)"' in html +def test_file_browser_dropdown_escapes_scroll_container_and_header_is_opaque() -> None: + html = read("webui", "components", "modals", "file-browser", "file-browser.html") + store = read("webui", "components", "modals", "file-browser", "file-browser-store.js") + + assert '
' in html + assert 'overflow: auto;' in html + assert 'x-teleport="body"' in html + assert 'class="dropdown-menu file-actions-menu"' in html + assert ':style="$store.fileBrowser.dropdownStyle"' in html + assert '@click.stop="$store.fileBrowser.toggleDropdown(file.path, $event.currentTarget)"' in html + assert "getDropdownStyle(triggerElement)" in store + assert 'position: "fixed"' in store + assert 'zIndex: "6000"' in store + + assert "var(--secondary-bg)" not in html + assert "var(--border-color)" not in html + assert "var(--text-secondary)" not in html + assert "background: color-mix(in srgb, var(--color-panel) 88%, var(--color-background) 12%);" in html + assert "border-bottom: 1px solid var(--color-border);" in html + + def test_file_browser_empty_api_path_uses_default_workdir_contract() -> None: api_source = read("api", "get_work_dir_files.py") api_dox = read("api", "get_work_dir_files.py.dox.md") diff --git a/tests/test_webui_component_loader.py b/tests/test_webui_component_loader.py new file mode 100644 index 000000000..7ecf2fa3a --- /dev/null +++ b/tests/test_webui_component_loader.py @@ -0,0 +1,17 @@ +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +def read(*parts: str) -> str: + return PROJECT_ROOT.joinpath(*parts).read_text(encoding="utf-8") + + +def test_component_loader_deduplicates_body_assets() -> None: + source = read("webui", "js", "components.js") + + assert 'const componentAssetSelector = "style, script, link[rel=\'stylesheet\']";' in source + assert "...doc.querySelectorAll(componentAssetSelector)" in source + assert "...Array.from(doc.body.childNodes).filter(" in source + assert "(node) => !node.matches?.(componentAssetSelector)" in source diff --git a/webui/components/modals/file-browser/AGENTS.md b/webui/components/modals/file-browser/AGENTS.md index f1dcb53f6..a17eee0d7 100644 --- a/webui/components/modals/file-browser/AGENTS.md +++ b/webui/components/modals/file-browser/AGENTS.md @@ -19,6 +19,7 @@ - Empty mounted startup states must self-heal to the `$WORK_DIR` default instead of rendering a blank path and empty list. - Preserve picker modes for Editor Open and Save As: Editor Open selects one or more Markdown or plain text files with a pinned primary action, and Save As selects the current folder plus a `.md` or `.txt` file name. - Keep the row-level Open in Editor action visible outside the overflow menu for Editor-owned `.md` and `.txt` files. +- Keep row action menus visible without disabling file-list scrolling; menus may float outside the scroll container but must still close on outside click, Escape, action click, and list scroll. - Keep the file list readable in narrow canvas/modal containers by hiding the Modified date column before sacrificing the Name or Size columns. - Keep New file and New folder controls icon-only across canvas and modal modes while preserving accessible labels. - Keep narrow mobile controls compact: Up shares the path row, and New file/New folder share the search row. diff --git a/webui/components/modals/file-browser/file-browser-store.js b/webui/components/modals/file-browser/file-browser-store.js index 74c8c1277..32cae8c34 100644 --- a/webui/components/modals/file-browser/file-browser-store.js +++ b/webui/components/modals/file-browser/file-browser-store.js @@ -88,6 +88,7 @@ const model = { renamePerformAction: null, renameValidateName: null, openDropdownPath: null, // Track which dropdown is currently open + dropdownStyle: {}, searchQuery: "", isBulkBusy: false, pickerMode: PICKER_MODE_NONE, @@ -783,9 +784,14 @@ const model = { }, // --- Dropdown Management ------------------------------------------------- - toggleDropdown(filePath) { + toggleDropdown(filePath, triggerElement = null) { // Toggle: if already open, close it; otherwise open this one (closing any other) - this.openDropdownPath = this.openDropdownPath === filePath ? null : filePath; + if (this.openDropdownPath === filePath) { + this.closeDropdown(); + return; + } + this.openDropdownPath = filePath; + this.dropdownStyle = this.getDropdownStyle(triggerElement); }, isDropdownOpen(filePath) { @@ -794,6 +800,33 @@ const model = { closeDropdown() { this.openDropdownPath = null; + this.dropdownStyle = {}; + }, + + getDropdownStyle(triggerElement) { + if (!triggerElement) return {}; + + const rect = triggerElement.getBoundingClientRect(); + const gap = 6; + const padding = 8; + const minWidth = 180; + const spaceBelow = window.innerHeight - rect.bottom - gap - padding; + const spaceAbove = rect.top - gap - padding; + const openUp = spaceBelow < 160 && spaceAbove > spaceBelow; + const maxHeight = Math.max(96, openUp ? spaceAbove : spaceBelow); + const maxLeft = Math.max(padding, window.innerWidth - minWidth - padding); + const left = Math.min(Math.max(rect.right - minWidth, padding), maxLeft); + + return { + position: "fixed", + left: `${Math.round(left)}px`, + right: "auto", + top: openUp ? "auto" : `${Math.round(rect.bottom + gap)}px`, + bottom: openUp ? `${Math.round(window.innerHeight - rect.top + gap)}px` : "auto", + minWidth: `${minWidth}px`, + maxHeight: `${Math.round(maxHeight)}px`, + zIndex: "6000", + }; }, // --- Navigation ---------------------------------------------------------- diff --git a/webui/components/modals/file-browser/file-browser.html b/webui/components/modals/file-browser/file-browser.html index 25d4a11be..f91966304 100644 --- a/webui/components/modals/file-browser/file-browser.html +++ b/webui/components/modals/file-browser/file-browser.html @@ -194,7 +194,7 @@
-
+
more_vert - + +
+