Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
58ef539
fix(library): preserve responsive listings and owned user interactions
elacity2822-beep Oct 16, 2026
fd5b6e8
perf(browser): reclaim completed guest control bridge workers
elacity2822-beep Oct 16, 2026
bcb9aea
test(library): wait for typed folder metadata before menu checks
elacity2822-beep Oct 17, 2026
ba0194b
test(library): await exact copy dispatch before asserting destinations
elacity2822-beep Oct 17, 2026
557cdd3
fix(browser): preserve fail-fast bridge shutdown after worker reaping
elacity2822-beep Oct 17, 2026
70cbe36
Merge commit '63f57e34147f38a4c110716d8f1bfaf24b51d81e' into fix/revi…
elacity2822-beep Oct 17, 2026
589e46a
docs: bind regression guidance to exercised runtime behavior
elacity2822-beep Oct 17, 2026
6026749
Merge develop into Browser bridge review branch
SashaMIT Oct 17, 2026
0005d77
Preserve Library selection for keyboard actions
SashaMIT Oct 17, 2026
1b9cbc3
Exercise Browser bridge reclamation and add its CI checks
SashaMIT Oct 17, 2026
516295c
Align regression guidance with reviewed Runtime and Component behavior
SashaMIT Oct 17, 2026
087b89c
Merge current develop into Browser bridge review branch
SashaMIT Oct 17, 2026
619c096
Use a platform-neutral app in Home link regression
SashaMIT Oct 17, 2026
a2c2215
Merge reviewed Browser bridge fixes into Library review branch
SashaMIT Oct 17, 2026
3943730
Merge current develop with canonical Home-link fixture
SashaMIT Oct 18, 2026
0a9b04d
Merge refreshed Browser bridge with canonical Home-link fixture
SashaMIT Oct 18, 2026
0f6ad1e
Verify Library tab order before row focus
SashaMIT Oct 18, 2026
898ef79
Merge current develop into Browser bridge review
SashaMIT Oct 19, 2026
dcdd115
Merge refreshed Browser bridge into Library review
SashaMIT Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,30 @@ jobs:
retention-days: 1
compression-level: 0

# This bridge is a separate Cargo workspace, outside the Runtime tests.
test-browser-guest-control-bridge:
name: test-browser-guest-control-bridge
runs-on: ubuntu-24.04
timeout-minutes: 10
needs: source-gate
defaults:
run:
working-directory: elastos/tools/browser-vm-guest-control-bridge
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@72504271e02fb719eb4eb3eaece3a8e3d06f57c0 # 1.91.0
with:
components: rustfmt, clippy
- name: bridge formatting
run: cargo fmt -- --check
- name: bridge Clippy
run: cargo clippy --locked --all-targets -- -D warnings
- name: bridge tests
run: cargo test --locked

lint:
name: lint
runs-on: ubuntu-24.04
Expand Down
2 changes: 1 addition & 1 deletion capsules/library/browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ <h2 class="window-sidebar-title">Favorites</h2>
</header>

<section id="upload-progress" class="upload-progress hidden" aria-label="Upload progress" aria-live="polite"></section>
<section id="content" class="content" data-view="list" aria-label="Items"></section>
<section id="content" class="content" data-view="list" role="listbox" aria-label="Items" aria-multiselectable="true" tabindex="0"></section>
<footer class="statusbar explorer-footer">
<span id="footer-left">0 items</span>
<span id="footer-right">On this device</span>
Expand Down
63 changes: 12 additions & 51 deletions capsules/library/browser/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { createLibraryActions } from "./actions.js?v=library-20260711d";
import { createLibraryDialog } from "./dialog.js?v=library-20260711d";
import { createLibraryEditor } from "./editor.js";
import { bindLibraryEvents } from "./events.js";
import { createLibraryFolders } from "./folders.js";
import { createLibraryMenu } from "./menu.js";
import { createLibraryNavigation } from "./navigation.js";
import { createLibraryPreview } from "./preview.js";
Expand Down Expand Up @@ -169,6 +170,17 @@ import {
state,
visibleObjects,
});
const { loadCurrentFolder } = createLibraryFolders({
homeNavigation,
perf,
providerApi,
renderAll,
runInitialObjectAction,
setFolderStatus,
setObjects,
setStatus,
state,
});
const {
handleBrowserPopState,
installBrowserHistory,
Expand Down Expand Up @@ -708,57 +720,6 @@ import {
setStatus("Sidebar order saved.");
}

async function loadCurrentFolder(options = {}) {
if (!state.currentUri) return;
const loadSeq = ++state.loadSeq;
const uri = state.currentUri;
const cached = options.useCache ? state.folderCache.get(uri) : null;
let renderedCached = false;
let renderAfterFetch = true;
if (cached) {
perf.folderCacheHits += 1;
state.loading = false;
state.currentObject = cached.object || null;
setObjects(cached.objects);
state.selectedUris.clear();
setFolderStatus(`${state.objects.length} item${state.objects.length === 1 ? "" : "s"}.`);
renderAll();
await runInitialObjectAction();
renderedCached = true;
} else {
state.loading = true;
state.currentObject = null;
setStatus("Loading...");
}
try {
const data = await providerApi("list", { uri });
if (loadSeq !== state.loadSeq || uri !== state.currentUri) {
return;
}
const objects = Array.isArray(data.objects) ? data.objects : [];
const currentObject = data.object || null;
const nextCache = cacheFolderListing(state, perf, uri, objects, currentObject);
state.currentObject = currentObject;
if (state.mode === "browse" && currentObject?.uri === uri && isDirectory(currentObject)) {
homeNavigation.setQuery({ uri });
}
if (renderedCached && cached.signature === nextCache.signature) {
renderAfterFetch = false;
setFolderStatus(`${state.objects.length} item${state.objects.length === 1 ? "" : "s"}.`);
return;
}
setObjects(objects);
state.selectedUris.clear();
setFolderStatus(`${state.objects.length} item${state.objects.length === 1 ? "" : "s"}.`);
} finally {
if (loadSeq === state.loadSeq && uri === state.currentUri) {
state.loading = false;
if (renderAfterFetch) renderAll();
if (renderAfterFetch) await runInitialObjectAction();
}
}
}

async function runInitialObjectAction() {
if (state.initialActionHandled || !state.initialObjectUri || !state.initialAction) {
return;
Expand Down
108 changes: 108 additions & 0 deletions capsules/library/browser/src/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ export function bindLibraryEvents({
uploadFiles,
}) {
let draggingPlaceId = "";
let keyboardMenuReturnTarget = null;
let keyboardNavigationJob = 0;

function restoreKeyboardMenuFocus() {
if (keyboardMenuReturnTarget?.isConnected) keyboardMenuReturnTarget.focus({ preventScroll: true });
keyboardMenuReturnTarget = null;
}

function moveKeyboardFocus(targetItem, event, focusedItem) {
if (!targetItem) return;
if (event.shiftKey) {
if (!state.selectionAnchorUri && focusedItem) {
state.selectionAnchorUri = focusedItem.dataset.uri;
}
selectRangeTo(targetItem.dataset.uri, event.ctrlKey || event.metaKey);
} else if (!event.ctrlKey && !event.metaKey) {
selectOnly(targetItem.dataset.uri);
}
targetItem.focus({ preventScroll: true });
targetItem.scrollIntoView({ block: "nearest", inline: "nearest" });
}

elements.sidebar?.addEventListener("contextmenu", (event) => {
event.preventDefault();
Expand Down Expand Up @@ -147,6 +168,14 @@ export function bindLibraryEvents({
uploadFiles(elements.fileInput.files).catch(showError);
elements.fileInput.value = "";
});
elements.content.addEventListener("focusin", (event) => {
const item = event.target.closest(".item[data-uri]");
if (!item) return;
for (const previous of elements.content.querySelectorAll('.item[tabindex="0"]')) {
if (previous !== item) previous.tabIndex = -1;
}
if (item.tabIndex !== 0) item.tabIndex = 0;
});
elements.content.addEventListener("click", (event) => {
if (isNameEditorTarget(event.target)) return;
const item = event.target.closest(".item");
Expand Down Expand Up @@ -215,19 +244,72 @@ export function bindLibraryEvents({
document.addEventListener("click", (event) => {
if (!event.target.closest(".context-menu")) {
hideMenu();
keyboardMenuReturnTarget = null;
}
});
elements.contextMenu.addEventListener("click", (event) => {
const button = event.target.closest(".menu-item");
if (button && !button.parentElement.classList.contains("menu-entry-has-children")) {
restoreKeyboardMenuFocus();
}
}, true);
bindDialogEvents();
window.addEventListener("pointerdown", () => { keyboardNavigationJob += 1; });
window.addEventListener("keydown", (event) => {
const navigationJob = ++keyboardNavigationJob;
if (event.key === "Escape") {
hideMenu();
hideDialog();
clearSelection();
restoreKeyboardMenuFocus();
return;
}
const editable = event.target.closest?.("input, textarea, select, [contenteditable='true']");
const itemShortcutTarget = isItemShortcutTarget(event.target, elements);
const shortcutSurfaceReady = itemShortcutTarget && !editable && !isDialogOpen(elements) && !isMenuOpen(elements);
const focusedItem = shortcutSurfaceReady ? event.target.closest?.(".item[data-uri]") : null;
if (shortcutSurfaceReady && !event.altKey) {
if (event.key === "End" && elements.content.getAttribute?.("aria-busy") === "true") {
event.preventDefault();
const renderJob = state.contentRenderJob;
if (document.activeElement === elements.content) {
// A later chunk can restore the previous row. Keep this new key request in control of focus.
elements.content.querySelector('.item[tabindex="0"]')?.focus({ preventScroll: true });
}
const initialFocus = document.activeElement;
const finishNavigation = () => {
if (navigationJob !== keyboardNavigationJob || renderJob !== state.contentRenderJob
|| document.activeElement !== initialFocus) return;
if (elements.content.getAttribute("aria-busy") === "true") {
window.requestAnimationFrame(finishNavigation);
} else {
moveKeyboardFocus(keyboardNavigationTarget(elements.content, focusedItem, "End"), event, focusedItem);
}
};
window.requestAnimationFrame(finishNavigation);
return;
}
const targetItem = keyboardNavigationTarget(elements.content, focusedItem, event.key);
if (targetItem) {
event.preventDefault();
moveKeyboardFocus(targetItem, event, focusedItem);
return;
}
if (event.key === " " && focusedItem) {
event.preventDefault();
if (event.shiftKey) {
selectRangeTo(focusedItem.dataset.uri, event.ctrlKey || event.metaKey);
} else {
toggleSelected(focusedItem.dataset.uri);
}
return;
}
}
const itemAction = ["Enter", "F2", "Delete", "ContextMenu"].includes(event.key)
|| (event.shiftKey && event.key === "F10");
if (itemAction && focusedItem && state.selectedUris.size === 0) {
selectOnly(focusedItem.dataset.uri);
}
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a" && shortcutSurfaceReady) {
event.preventDefault();
selectAllVisible();
Expand Down Expand Up @@ -258,12 +340,14 @@ export function bindLibraryEvents({
}
if ((event.key === "ContextMenu" || (event.shiftKey && event.key === "F10")) && shortcutSurfaceReady) {
event.preventDefault();
keyboardMenuReturnTarget = focusedItem || elements.content;
const object = selectedObjects()[0];
if (object) {
showKeyboardObjectMenu(elements, object, showMenuForObject);
} else {
showKeyboardBackgroundMenu(elements, showBackgroundMenu);
}
elements.contextMenu.querySelector(".menu-item:not(:disabled)")?.focus({ preventScroll: true });
}
});
window.addEventListener("popstate", (event) => {
Expand Down Expand Up @@ -304,6 +388,30 @@ function isNameEditorTarget(target) {
return !!target?.closest?.(".rename-input");
}

function keyboardNavigationTarget(content, focusedItem, key) {
const grid = content.dataset.view === "grid";
if (!["ArrowUp", "ArrowDown", "Home", "End"].includes(key)
&& !(grid && ["ArrowLeft", "ArrowRight"].includes(key))) return null;
const items = Array.from(content.querySelectorAll(".item[data-uri]"));
if (!items.length) return null;
if (key === "Home") return items[0];
if (key === "End") return items.at(-1);
const index = items.indexOf(focusedItem);
if (index < 0) return key === "ArrowUp" || key === "ArrowLeft" ? items.at(-1) : items[0];
let step = 1;
if (grid && (key === "ArrowUp" || key === "ArrowDown")) {
const firstTop = items[0].offsetTop;
const nextRow = items.findIndex((item) => item.offsetTop > firstTop);
step = nextRow > 0 ? nextRow : items.length;
if (key === "ArrowUp" && index < step) return items[index];
if (key === "ArrowDown" && Math.floor(index / step) === Math.floor((items.length - 1) / step)) {
return items[index];
}
}
const direction = key === "ArrowUp" || key === "ArrowLeft" ? -1 : 1;
return items[Math.max(0, Math.min(items.length - 1, index + direction * step))];
}

function isItemShortcutTarget(target, elements) {
return !!(target && elements.content && (target === elements.content || elements.content.contains?.(target)));
}
Expand Down
Loading
Loading