From 68aab718a1518ecbe393849e0001e42b84e2ec87 Mon Sep 17 00:00:00 2001 From: jelveh Date: Sun, 19 Jul 2026 18:43:57 -0700 Subject: [PATCH] Add keyboard navigation to the Apps tab --- src/gui/src/UI/Dashboard/TabApps.js | 64 +++++++++++++++++++++++++---- src/gui/src/css/dashboard.css | 11 +++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js index e088330e4..f447702ed 100644 --- a/src/gui/src/UI/Dashboard/TabApps.js +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -80,7 +80,7 @@ function buildTileHtml (app) { ? window.icons['app-default.svg'] : (app.iconUrl || window.icons['app.svg']); - let h = `
`; + let h = `
`; h += '
'; h += ``; h += '
'; @@ -440,20 +440,64 @@ const TabApps = { self.goToPage($el_window, self._page + (oe.deltaY > 0 ? 1 : -1), true); }); - // Left/right arrow keys flip pages, including while the (empty) search - // box holds focus — it's auto-focused on desktop, Launchpad-style. - $(document).off('keydown.myapps-pager').on('keydown.myapps-pager', function (e) { - if ( e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' ) return; + // -- Keyboard navigation -- + // Arrow keys move focus between the current page's tiles and + // Enter/Space launches the focused one. Navigation is deliberately + // clamped to the visible page — the keyboard never flips pages; the + // dots, hover arrows, and wheel remain the paging affordances. + // updatePagerUI keeps one tile per render in the tab order (roving + // tabindex), so Tab lands on the grid and arrows take over from there. + $(document).off('keydown.myapps-keyboard').on('keydown.myapps-keyboard', function (e) { + if ( ! ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter', ' '].includes(e.key) ) return; if ( ! $el_window.find('.dashboard-section-apps').hasClass('active') ) return; if ( ! $el_window.is(':visible') ) return; if ( $el_window.find('.myapps-modal-overlay').length ) return; if ( $('.window').not($el_window[0]).filter(':visible').length ) return; + + const pageTiles = $el_window.find('.myapps-page').eq(self._page).find('.myapps-tile').toArray(); + if ( pageTiles.length === 0 ) return; + const ae = document.activeElement; - if ( ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.tagName === 'SELECT' || ae.isContentEditable) ) { - if ( ! ($(ae).hasClass('myapps-search') && ae.value === '') ) return; + const onTile = ae && ae.classList && ae.classList.contains('myapps-tile'); + + if ( ! onTile ) { + // ArrowDown steps from the search box into the grid; when the + // (auto-focused, Launchpad-style) search is empty, any arrow + // does. Other keys are left alone so the caret and native + // button behavior keep working. + if ( ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.tagName === 'SELECT' || ae.isContentEditable) ) { + const emptySearch = $(ae).hasClass('myapps-search') && ae.value === ''; + if ( e.key !== 'ArrowDown' && ! (emptySearch && e.key.startsWith('Arrow')) ) return; + } else if ( ! e.key.startsWith('Arrow') ) { + return; + } + e.preventDefault(); + pageTiles[0].focus({ preventScroll: true }); + return; } + + if ( e.key === 'Enter' || e.key === ' ' ) { + e.preventDefault(); + $(ae).trigger('click'); + return; + } + e.preventDefault(); - self.goToPage($el_window, self._page + (e.key === 'ArrowRight' ? 1 : -1), true); + const idx = pageTiles.indexOf(ae); + if ( idx === -1 ) { + // Focus is on a tile of an offscreen page (the page changed + // under it via dots/wheel); pull it back to the visible one. + pageTiles[0].focus({ preventScroll: true }); + return; + } + const cols = (self._layout && self._layout.cols) || 1; + let next = idx; + if ( e.key === 'ArrowLeft' ) next = idx - 1; + else if ( e.key === 'ArrowRight' ) next = idx + 1; + else if ( e.key === 'ArrowUp' ) next = idx - cols; + else if ( e.key === 'ArrowDown' ) next = idx + cols; + if ( next === idx || next < 0 || next >= pageTiles.length ) return; + pageTiles[next].focus({ preventScroll: true }); }); // Re-paginate when the container resizes (window resize, sidebar @@ -603,6 +647,10 @@ const TabApps = { .toggleClass('myapps-pager-arrow-hidden', page <= 0); $container.find('.myapps-pager-arrow-next') .toggleClass('myapps-pager-arrow-hidden', page >= this._pageCount - 1); + // Roving tabindex: exactly one tile — the current page's first — sits + // in the tab order; arrow keys move real focus from there. + $container.find('.myapps-tile').attr('tabindex', '-1'); + $container.find('.myapps-page').eq(page).find('.myapps-tile').first().attr('tabindex', '0'); }, goToPage ($el_window, index, smooth) { diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index f04157ad8..b0b1af539 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -943,6 +943,17 @@ input.myapps-search::-webkit-search-decoration { transform: scale(0.95); } +/* Keyboard navigation: keyboard focus mirrors the mouse hover enlargement. + :focus-visible only, so a mouse click doesn't pin the effect after the + pointer moves away. */ +.myapps-tile:focus { + outline: none; +} + +.myapps-tile:focus-visible .myapps-tile-icon { + transform: scale(1.08); +} + /* Uninstall: the tile shrinks away in place, then the surviving tiles FLIP-slide into the closed-up grid (see showUninstallModal). Duration is mirrored by TILE_REMOVE_ANIM_MS in TabApps.js. */