a11y/security: scope Navbar ids + hover styles, Gallery lightbox focus trap
M-1: Navbar.toHtml emitted a fixed id="navbar-links" and unscoped .navbar-link/.navbar-cta :hover selectors -- two Navbars on one page collided on the duplicate id and cross-applied each other's hover colors (later <style> block wins in the cascade). Scope both on the Craft node id via scopeId(), matching the Menu/Tabs pattern: the links container gets a unique id, aria-controls/the hamburger toggle script reference it, and the hover rules are prefixed with a per-instance class on the <nav> root. M-2: Gallery lightbox had no focus management -- opening it left focus wherever it was (behind the now-visible overlay) and closing it never restored it. The inline script now stashes document.activeElement on open, moves focus to a new accessible close button, traps Tab on the close button while the dialog is open, and restores the saved focus on close (Escape, backdrop click, or the close button). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,3 +93,33 @@ describe('Gallery.toHtml deterministic + unique scope ids (thread node id, no Ma
|
||||
expect(html1).toBe(html2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gallery.toHtml lightbox focus management (M-2)', () => {
|
||||
const props = { images: [{ src: '/a.jpg', alt: 'a' }], lightbox: true };
|
||||
|
||||
test('overlay includes a focusable close control with an accessible name and tabindex', () => {
|
||||
const { html } = toHtml(props, '', 'node-gal1');
|
||||
// A close control: a button (or the dialog container) with an accessible
|
||||
// name (aria-label) and an explicit tabindex so it's keyboard-focusable.
|
||||
expect(html).toMatch(/aria-label="[^"]*[Cc]lose[^"]*"[^>]*tabindex="-?\d+"|tabindex="-?\d+"[^>]*aria-label="[^"]*[Cc]lose[^"]*"/);
|
||||
});
|
||||
|
||||
test('script saves document.activeElement on open (for focus restore)', () => {
|
||||
const { html } = toHtml(props, '', 'node-gal1');
|
||||
expect(html).toMatch(/document\.activeElement/);
|
||||
});
|
||||
|
||||
test('script moves focus to the close control / dialog on open', () => {
|
||||
const { html } = toHtml(props, '', 'node-gal1');
|
||||
expect(html).toMatch(/\.focus\(\)/);
|
||||
});
|
||||
|
||||
test('script restores the previously-saved focus on close', () => {
|
||||
const { html } = toHtml(props, '', 'node-gal1');
|
||||
// The close function references a stored "last focused element" variable
|
||||
// and calls .focus() on it, not just moving focus INTO the dialog.
|
||||
const closeFnMatch = html.match(/function\s+\w+_close\s*\(\)\s*\{[^}]*\}/);
|
||||
expect(closeFnMatch).not.toBeNull();
|
||||
expect(closeFnMatch![0]).toMatch(/\.focus\(\)/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user