Skip to content

Conversation

@Kiarokh
Copy link
Contributor

@Kiarokh Kiarokh commented Jan 21, 2026

fix https://github.com/Lundalogik/crm-client/issues/646

Summary by CodeRabbit

  • Bug Fixes
    • Improved focus restoration for menus and popovers — focus reliably returns to triggers after selection, Escape, outside click, or nested navigation.
  • New Features
    • Added a utility to focus the appropriate element inside trigger components to improve accessibility.
  • Tests
    • Expanded end-to-end tests covering focus restoration across menus, popovers, native and custom triggers, and nested sub-menus.

✏️ Tip: You can customize this high-level summary in your review settings.

Review:

  • Commits are atomic
  • Commits have the correct type for the changes made
  • Commits with breaking changes are marked as such

Browsers tested:

(Check any that applies, it's ok to leave boxes unchecked if testing something didn't seem relevant.)

Windows:

  • Chrome
  • Edge
  • Firefox

Linux:

  • Chrome
  • Firefox

macOS:

  • Chrome
  • Firefox
  • Safari

Mobile:

  • Chrome on Android
  • iOS

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Adds focus-restoration logic for menu and popover components, introduces a reusable focusTriggerElement utility, and expands end-to-end tests to verify focus behavior across triggers, selections, Escape handling, and nested sub-menus.

Changes

Cohort / File(s) Summary
Menu logic
src/components/menu/menu.tsx
Adds private focusTrigger helper, centralizes focus flows, guards setFocus() when closed, and uses delayed focus scheduling (setTimeout) on close/select/navigation.
Menu E2E tests
src/components/menu/menu.e2e.ts
Replaces programmatic opens with user-trigger clicks; adds helpers (waitForActiveElementSlot, waitForMenuOpenState); expands tests for focus restoration after select, Escape, and nested sub-menus with limel-button and native triggers.
Popover logic
src/components/popover/popover.tsx
Tracks trigger slot via ref, adds requestCloseAndRestoreFocus and focusTrigger, defers close paths to flag-and-restore flow, and assigns accessibility attributes to trigger(s).
Popover E2E tests
src/components/popover/popover.e2e.ts
New tests and helpers (waitForCondition, waitForActiveElementId, waitForPopoverOpenState) verifying focus restore on Escape, outside click, and that programmatic close does NOT restore focus.
Focus utility
src/util/focus-trigger-element.ts
New `focusTriggerElement(trigger?: HTMLElement

Sequence Diagram(s)

sequenceDiagram
  participant User as User (click/keyboard)
  participant Trigger as Trigger element
  participant Component as Menu/Popover
  participant Document as Document/Browser

  User->>Trigger: click / keypress (open)
  Trigger->>Component: open()
  Component-->>Trigger: set aria attributes
  Note right of Component: User navigates/selects or presses Escape
  User->>Component: select item / press Escape / click outside
  Component->>Component: requestCloseAndRestoreFocus() / emit close
  Component->>Trigger: (after short delay) focusTrigger()
  Trigger->>Document: focused element updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • devbymadde
  • LucyChyzhova
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title Check ✅ Passed Title check skipped as CodeRabbit has written the PR title.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch menu-set-focus-back-on-trigger

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Kiarokh Kiarokh force-pushed the menu-set-focus-back-on-trigger branch from 7e22e57 to e63e8de Compare January 21, 2026 13:50
@Kiarokh Kiarokh marked this pull request as ready for review January 21, 2026 13:50
@coderabbitai coderabbitai bot changed the title @coderabbitai Add focus restoration helper to menu component Jan 21, 2026
@github-actions
Copy link

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-3793/

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/components/menu/menu.e2e.ts`:
- Around line 141-149: The test "restores focus to the trigger" currently uses a
1ms sleep which is flaky; replace the waitForTimeout(1) with a
wait-for-condition that polls until document.activeElement?.getAttribute('slot')
=== 'trigger' (e.g., use page.waitForFunction or equivalent) and then assert the
slot is 'trigger'; apply the same change to the other focus-restore tests in
this file (the tests that currently use waitForTimeout with tiny delays) so each
waits for the activeElement slot condition instead of relying on a fixed short
timeout.

@Kiarokh Kiarokh enabled auto-merge (rebase) January 21, 2026 14:07
@Kiarokh Kiarokh force-pushed the menu-set-focus-back-on-trigger branch from 472a8d7 to f104322 Compare January 22, 2026 14:52
Focuses the first focusable element inside a trigger element.
Supports custom elements by searching both the element's shadow root
and its light DOM.
…a selection

So the user can easily press Enter to open it again.
…a dismiss

e.g. pressing `Esc` button; so the user can easily press Enter to open it again.
@Kiarokh Kiarokh force-pushed the menu-set-focus-back-on-trigger branch from 78732d2 to 0bc2c1b Compare January 23, 2026 09:07
@Kiarokh
Copy link
Contributor Author

Kiarokh commented Jan 23, 2026

@coderabbitai so full review again please. Commits have now changed and things are restructured

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

@Kiarokh, I'll conduct a complete review of the PR again since the commits have been changed and the code has been restructured.

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/components/popover/popover.e2e.ts`:
- Around line 3-38: Extract the duplicated polling helpers into a shared test
utilities module: move waitForCondition, waitForActiveElementId, and
waitForPopoverOpenState into a new test-helpers file and export them; update
this popover e2e test to import those functions instead of defining them inline;
do the same for the similar helpers waitForActiveElementSlot and
waitForMenuOpenState from the menu e2e test so both tests import the shared
implementations and share consistent timeout/polling behavior; ensure function
signatures and behavior remain identical and update any import paths in the test
files.

In `@src/components/popover/popover.tsx`:
- Around line 192-207: The focusTrigger implementation duplicates logic found
elsewhere; refactor by extending focusTriggerElement to accept a slot (or add a
new helper like focusTriggerFromSlot) and update this.focusTrigger to call that
helper with this.triggerSlot; reference the existing focusTrigger method, the
triggerSlot property, and focusTriggerElement (or new focusTriggerFromSlot) so
both popover.tsx and menu.tsx can delegate to the shared helper and remove the
assignedElements()/[0] boilerplate.

In `@src/util/focus-trigger-element.ts`:
- Around line 1-2: FOCUSABLE_SELECTOR currently omits contenteditable elements,
so add [contenteditable] to the comma-separated list in the FOCUSABLE_SELECTOR
constant to ensure contenteditable elements are treated as focusable triggers;
keep the existing [tabindex]:not([tabindex="-1"]) clause as-is to continue
matching elements with explicit tabindex values.

Comment on lines +3 to +38
const waitForCondition = async (
condition: () => Promise<boolean>,
errorMessage: string
) => {
const timeoutMs = 2000;
const start = Date.now();

while (Date.now() - start < timeoutMs) {
if (await condition()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}

throw new Error(errorMessage);
};

const waitForActiveElementId = async (page: any, id: string) => {
await waitForCondition(async () => {
const activeId = await page.evaluate(() => document.activeElement?.id);
return activeId === id;
}, `Timed out waiting for focus on #${id}`);
};

const waitForPopoverOpenState = async (page: any, isOpen: boolean) => {
await waitForCondition(
async () => {
const open = await page.evaluate(() => {
const popover = document.querySelector('limel-popover') as any;
return Boolean(popover?.open);
});
return open === isOpen;
},
`Timed out waiting for popover open=${String(isOpen)}`
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting shared test utilities to reduce duplication.

The waitForCondition and related helpers here are similar in pattern to waitForActiveElementSlot and waitForMenuOpenState in menu.e2e.ts. Consider extracting these into a shared test utilities file (e.g., src/util/test-helpers.ts) to reduce duplication and ensure consistent timeout/polling behavior across e2e tests.

🤖 Prompt for AI Agents
In `@src/components/popover/popover.e2e.ts` around lines 3 - 38, Extract the
duplicated polling helpers into a shared test utilities module: move
waitForCondition, waitForActiveElementId, and waitForPopoverOpenState into a new
test-helpers file and export them; update this popover e2e test to import those
functions instead of defining them inline; do the same for the similar helpers
waitForActiveElementSlot and waitForMenuOpenState from the menu e2e test so both
tests import the shared implementations and share consistent timeout/polling
behavior; ensure function signatures and behavior remain identical and update
any import paths in the test files.

Comment on lines +192 to +207
private readonly requestCloseAndRestoreFocus = () => {
this.shouldRestoreFocusOnClose = true;
this.close.emit();
};

private readonly setTriggerRef = (elm?: HTMLSlotElement) => {
this.triggerSlot = elm;
};

private readonly focusTrigger = () => {
const trigger = this.triggerSlot?.assignedElements()?.[0] as
| HTMLElement
| undefined;

focusTriggerElement(trigger);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider consolidating the focusTrigger method pattern.

The focusTrigger method implementation (lines 201-207) is nearly identical to the one in menu.tsx (lines 810-816). While the duplication is minimal, consider whether a higher-order helper or a slight refactor of focusTriggerElement could accept the slot directly to reduce this boilerplate.

💡 Alternative approach

You could extend focusTriggerElement to optionally accept a slot element:

// In focus-trigger-element.ts
export const focusTriggerFromSlot = (slot?: HTMLSlotElement | null): boolean => {
    const trigger = slot?.assignedElements()?.[0] as HTMLElement | undefined;
    return focusTriggerElement(trigger);
};

Then both components could use:

private readonly focusTrigger = () => focusTriggerFromSlot(this.triggerSlot);
🤖 Prompt for AI Agents
In `@src/components/popover/popover.tsx` around lines 192 - 207, The focusTrigger
implementation duplicates logic found elsewhere; refactor by extending
focusTriggerElement to accept a slot (or add a new helper like
focusTriggerFromSlot) and update this.focusTrigger to call that helper with
this.triggerSlot; reference the existing focusTrigger method, the triggerSlot
property, and focusTriggerElement (or new focusTriggerFromSlot) so both
popover.tsx and menu.tsx can delegate to the shared helper and remove the
assignedElements()/[0] boilerplate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants