From 3d14551111100b7275a2cd0bb8b2e7bd0eafee23 Mon Sep 17 00:00:00 2001 From: BryanFRD Date: Sun, 25 Oct 2026 10:53:07 +0100 Subject: [PATCH 1/5] fix(ui-ng): make flr-menu keyboard navigable Focus the first item on open, add arrow/Home/End navigation, close on Tab and restore focus to the trigger. Drop role=menu from the inline brand dropdown, which is a plain list of links. --- packages/showcase/src/stories/Menu.stories.ts | 30 ++++++++++ .../brand-dropdown.component.ts | 4 +- packages/ui-ng/src/lib/menu/menu.component.ts | 59 ++++++++++++++++++- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/packages/showcase/src/stories/Menu.stories.ts b/packages/showcase/src/stories/Menu.stories.ts index 78289708..b91b0808 100644 --- a/packages/showcase/src/stories/Menu.stories.ts +++ b/packages/showcase/src/stories/Menu.stories.ts @@ -7,6 +7,7 @@ import { } from '@ferrlabs/ui-ng'; import type { Meta, StoryObj } from '@storybook/angular'; import { moduleMetadata } from '@storybook/angular'; +import { expect, userEvent, waitFor, within } from 'storybook/test'; const meta: Meta = { title: 'Overlays/Menu', @@ -50,3 +51,32 @@ type Story = StoryObj; export const Default: Story = {}; export const AlignEnd: Story = { args: { align: 'end' } }; + +export const KeyboardNavigation: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const overlay = within(canvasElement.ownerDocument.body); + const trigger = canvas.getByRole('button', { name: 'Actions' }); + + trigger.focus(); + await userEvent.keyboard('{Enter}'); + const first = await overlay.findByRole('menuitem', { name: 'Trigger now' }); + await waitFor(() => expect(first).toHaveFocus()); + + await userEvent.keyboard('{ArrowDown}'); + await expect(overlay.getByRole('menuitem', { name: 'Trigger with overrides' })).toHaveFocus(); + + await userEvent.keyboard('{End}'); + await expect(overlay.getByRole('menuitem', { name: 'Delete agent' })).toHaveFocus(); + + await userEvent.keyboard('{ArrowDown}'); + await expect(first).toHaveFocus(); + + await userEvent.keyboard('{Home}{ArrowUp}'); + await expect(overlay.getByRole('menuitem', { name: 'Delete agent' })).toHaveFocus(); + + await userEvent.keyboard('{Escape}'); + await waitFor(() => expect(overlay.queryByRole('menu')).toBeNull()); + await expect(trigger).toHaveFocus(); + }, +}; diff --git a/packages/ui-ng/src/lib/brand-dropdown/brand-dropdown.component.ts b/packages/ui-ng/src/lib/brand-dropdown/brand-dropdown.component.ts index 5d183b57..7801ac48 100644 --- a/packages/ui-ng/src/lib/brand-dropdown/brand-dropdown.component.ts +++ b/packages/ui-ng/src/lib/brand-dropdown/brand-dropdown.component.ts @@ -59,7 +59,6 @@ interface BrandRow {