From 2e29a4098ffb2b0beb7004cda3a0d45a3a925101 Mon Sep 17 00:00:00 2001 From: Isa HV Date: Wed, 5 Aug 2026 21:04:07 -0400 Subject: [PATCH] WEBDEV-8458 Drive the navigator's panels from their own state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The panel's open/close was spread across two components, so the code worked around the animation rather than the behaviour that caused it — most visibly a 350ms timer standing in for 'the list is ready'. The navigator now owns whether the drawer and a panel are open, and the slider reports what the user did and renders what comes back. That retires a second copy of the selection that only agreed with the first by coincidence, plus two properties that never did anything: the slider's own open flag, frozen true by a static attribute, and animateMenuOpen, which nothing set. Because the state is now in one place it can be stated in the markup: aria-expanded reflects whether a surface is actually open rather than being hardcoded false, the drawer and panel carry roles and names, the panel is named by its own heading, and closed panels are inert so the tab order and the accessibility tree agree with the screen. Opening a surface moves focus into it and closing returns focus to whatever opened it — including from the shortcut rail, which used to hide the focused button and drop focus to the document. Closing the drawer also closes the panel inside it, so a stale panel can't reappear. The scroll follows the same rule: it runs when the list or the selection changes, so there is nothing to wait for. Menu buttons also carry an aria-label, keeping their name from depending on a styling variable a consumer might not set. Co-Authored-By: Claude Opus 4.8 --- .../ia-item-navigator/ia-item-navigator.ts | 90 ++++++---- .../ia-itemnav-menu-button.ts | 7 +- .../ia-itemnav-menu-slider.test.ts | 150 +++++++++++----- .../ia-itemnav-menu-slider.ts | 167 +++++++++--------- .../ia-itemnav-viewable-files-panel.test.ts | 110 ++++++++---- .../menus/ia-itemnav-viewable-files-panel.ts | 49 ++--- 6 files changed, 362 insertions(+), 211 deletions(-) diff --git a/src/elements/ia-item-navigator/ia-item-navigator.ts b/src/elements/ia-item-navigator/ia-item-navigator.ts index c88808e..ef66ee5 100644 --- a/src/elements/ia-item-navigator/ia-item-navigator.ts +++ b/src/elements/ia-item-navigator/ia-item-navigator.ts @@ -248,23 +248,30 @@ export class IAItemNavigator toggleMenu(forceValue: boolean | undefined = undefined): void { this.menuOpened = forceValue !== undefined ? forceValue : !this.menuOpened; - if (this.menuOpened) { - // Move focus to the - this.updateComplete.then(() => { - const closeButton = this.menuSlider?.shadowRoot?.querySelector( - 'button.close', - ) as HTMLElement; - closeButton?.focus(); - }); - } else { - // Move focus back to the menu toggle button - this.updateComplete.then(() => { + this.moveFocusForDrawer(); + } + + /** + * Opening the drawer moves focus into it; closing hands focus back to the + * toggle. Without this, opening from the shortcut rail would leave focus on + * a button that is about to be hidden, dropping it to the document. + */ + private moveFocusForDrawer(): void { + this.updateComplete.then(() => { + if (this.menuOpened) { + this.menuSlider?.focusDrawer(); + } else { this.toggleMenuButton?.focus(); - }); - } + } + }); } + /** + * Closing the drawer also closes whatever panel was open inside it, so the + * two can't disagree and a stale panel can't reappear on the next open. + */ closeMenu(): void { + this.openMenu = undefined; this.toggleMenu(false); } @@ -307,13 +314,15 @@ export class IAItemNavigator } get menuToggleButton(): TemplateResult { + const label = this.menuOpened ? 'Close side panel' : 'Open side panel'; return html` @@ -326,22 +335,27 @@ export class IAItemNavigator get renderSideMenu(): TemplateResult { return html` -