Skip to content

fix(list): focus pressed row on mousedown to keep clicks on target#4140

Merged
Befkadu1 merged 1 commit into
mainfrom
fix/list-focus-pressed-row
Jun 17, 2026
Merged

fix(list): focus pressed row on mousedown to keep clicks on target#4140
Befkadu1 merged 1 commit into
mainfrom
fix/list-focus-pressed-row

Conversation

@Befkadu1

@Befkadu1 Befkadu1 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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

Summary

  • A click on a row deep in a scrolled limel-list was lost: delegatesFocus redirected focus to the first (off-screen) row and scrolled it into view, moving the pressed row out from under the pointer so mouseup landed on empty <ul> space and MDC computed index -1. This is the cause of the object explorer widget "jumps to top instead of opening the clicked object" bug.
  • Fix: on mousedown, suppress the default focus delegation and focus the pressed row with { preventScroll: true }. Mouse-only (not pointerdown) so touch-drag scrolling isn't blocked; preventDefault for any row (so disabled rows don't scroll-jump either), but focus only moves to non-disabled rows.
  • Added e2e tests for radio/checkbox toggling and pressed-row focus.

Test plan

  • npx stencil-test --project e2e list/list.e2e passes (15/15)
  • Object explorer widget: scroll down → load more → click a deep row → it opens, list does not jump to top
  • radio and checkbox list examples still toggle on click
  • limel-menu-list still activates items (shares the pattern; not changed here)

Refs Lundalogik/crm-client#795

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Tests

    • Expanded E2E test coverage for the list component, including verification of focus behavior for selectable, radio, and checkbox list types.
  • Bug Fixes

    • Improved focus management when interacting with list items via mouse, ensuring the correct item receives focus.

With `delegatesFocus`, pressing a non-focusable part of a row (e.g. its
text) delegated focus to the first focusable row and scrolled it into
view. In a scrolled list this moved the pressed row out from under the
pointer before mouseup, so the click landed on empty space and no item
was selected. Focus the pressed row directly instead, without scrolling.

Refs Lundalogik/crm-client#795

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

limel-list adds a mousedown event listener on the shadow-root list element that calls preventDefault to suppress browser focus delegation and manually focuses the closest non-disabled list row without scrolling. The listener is registered in setupList and removed in teardown. Three new E2E tests verify the focus target and change event payloads for selectable, radio, and checkbox list types.

Changes

limel-list mousedown focus management

Layer / File(s) Summary
mousedown listener and focus handler
src/components/list/list.tsx
Adds a listElement field, wires handleItemMouseDown in setupList (removing before re-adding to avoid duplicates), and removes the listener in teardown. handleItemMouseDown calls preventDefault and focuses the closest non-disabled list row with preventScroll: true.
E2E tests for focus and change events
src/components/list/list.e2e.tsx
Adds a selectable test asserting shadowRoot.activeElement becomes the pressed item after mousedown; a radio test verifying the change event detail has selected: true for the clicked item; a checkbox test verifying the change event detail is an array with the clicked item marked selected: true.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • adrianschmidt
  • devbymadde
  • john-traas
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(list): focus pressed row on mousedown to keep clicks on target' accurately describes the main change—fixing focus behavior on mousedown to prevent clicked items from moving out from under the mouse pointer, which is the core issue addressed by this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/list-focus-pressed-row

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.

@github-actions

Copy link
Copy Markdown

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/list/list.e2e.tsx`:
- Around line 238-293: Add a new test case within the describe block for the
disabled row scenario to ensure the mousedown handler properly prevents focus on
disabled items. Create a test with items where at least one item has a disabled
property set to true, dispatch a mousedown event on the disabled item element,
then verify using an assertion that the shadowRoot.activeElement does not point
to that disabled item. This covers the explicit branch for disabled rows
mentioned in the handler implementation.

In `@src/components/list/list.tsx`:
- Around line 239-243: The focus gating check in the list component relies only
on the CSS class check
`itemElement.classList.contains('mdc-deprecated-list-item--disabled')` to
determine if an item is disabled, which is fragile since the aria-disabled
attribute is the source of truth for disabled state in list-item.tsx. Update the
disabled check to also verify the aria-disabled attribute on itemElement by
checking if the element does not have aria-disabled set to "true", either in
addition to or instead of the class-based check to ensure focus is properly
gated based on the accessibility attribute.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b1e3f53-01a9-4862-ae53-bfc1276cd26c

📥 Commits

Reviewing files that changed from the base of the PR and between d31c472 and 675c9fa.

📒 Files selected for processing (2)
  • src/components/list/list.e2e.tsx
  • src/components/list/list.tsx

Comment on lines +238 to 293
describe('is set as `radio`', () => {
it('emits change when a radio item is pressed', async () => {
const items = [{ text: 'item 1' }, { text: 'item 2' }];
const { root, waitForChanges, spyOnEvent } = await render(
<limel-list type="radio" items={items}></limel-list>
);
const changeSpy = spyOnEvent('change');
await waitForChanges();

const itemEls =
root.shadowRoot.querySelectorAll('limel-list-item');
const target = itemEls[1] as HTMLElement;
target.dispatchEvent(
new MouseEvent('mousedown', {
bubbles: true,
composed: true,
cancelable: true,
})
);
target.click();
await waitForChanges();

expect(changeSpy).toHaveReceivedEventDetail({
...items[1],
selected: true,
});
});
});

describe('is set as `checkbox`', () => {
it('emits change when a checkbox item is pressed', async () => {
const items = [{ text: 'item 1' }, { text: 'item 2' }];
const { root, waitForChanges, spyOnEvent } = await render(
<limel-list type="checkbox" items={items}></limel-list>
);
const changeSpy = spyOnEvent('change');
await waitForChanges();

const itemEls =
root.shadowRoot.querySelectorAll('limel-list-item');
const target = itemEls[1] as HTMLElement;
target.dispatchEvent(
new MouseEvent('mousedown', {
bubbles: true,
composed: true,
cancelable: true,
})
);
target.click();
await waitForChanges();

expect(changeSpy).toHaveReceivedEventDetail([
{ ...items[1], selected: true },
]);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add an E2E assertion for disabled-row mousedown path.

The new handler has an explicit branch for disabled rows (prevent default, but no focus move), but current added tests only cover enabled rows. Add one test that dispatches mousedown on a disabled item and verifies shadowRoot.activeElement does not become that item.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/list/list.e2e.tsx` around lines 238 - 293, Add a new test case
within the describe block for the disabled row scenario to ensure the mousedown
handler properly prevents focus on disabled items. Create a test with items
where at least one item has a disabled property set to true, dispatch a
mousedown event on the disabled item element, then verify using an assertion
that the shadowRoot.activeElement does not point to that disabled item. This
covers the explicit branch for disabled rows mentioned in the handler
implementation.

Comment on lines +239 to +243
if (
!itemElement.classList.contains(
'mdc-deprecated-list-item--disabled'
)
) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use ARIA-disabled check for focus gating instead of CSS class-only check.

At Line 240, the non-disabled check relies on mdc-deprecated-list-item--disabled. In src/components/list-item/list-item.tsx (Lines 130-190), disabled state is explicitly exposed via aria-disabled, so this gate is safer if based on that attribute (or both class + attribute) to avoid focusing disabled rows when class wiring changes.

Suggested patch
-        if (
-            !itemElement.classList.contains(
-                'mdc-deprecated-list-item--disabled'
-            )
-        ) {
+        const isDisabled =
+            itemElement.getAttribute('aria-disabled') === 'true' ||
+            itemElement.classList.contains(
+                'mdc-deprecated-list-item--disabled'
+            );
+        if (!isDisabled) {
             itemElement.focus({ preventScroll: true });
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
!itemElement.classList.contains(
'mdc-deprecated-list-item--disabled'
)
) {
const isDisabled =
itemElement.getAttribute('aria-disabled') === 'true' ||
itemElement.classList.contains(
'mdc-deprecated-list-item--disabled'
);
if (!isDisabled) {
itemElement.focus({ preventScroll: true });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/list/list.tsx` around lines 239 - 243, The focus gating check
in the list component relies only on the CSS class check
`itemElement.classList.contains('mdc-deprecated-list-item--disabled')` to
determine if an item is disabled, which is fragile since the aria-disabled
attribute is the source of truth for disabled state in list-item.tsx. Update the
disabled check to also verify the aria-disabled attribute on itemElement by
checking if the element does not have aria-disabled set to "true", either in
addition to or instead of the class-based check to ensure focus is properly
gated based on the accessibility attribute.

@devbymadde devbymadde left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great work, tested it locally and it works well! Code looks good, let's ship it :D

@Befkadu1 Befkadu1 merged commit b3bdf28 into main Jun 17, 2026
17 checks passed
@Befkadu1 Befkadu1 deleted the fix/list-focus-pressed-row branch June 17, 2026 15:01
@lime-opensource

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 39.34.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants