Skip to content

feat(table): drag and drop for table rows#3983

Open
albinhallen wants to merge 3 commits intomainfrom
feat/table-rows-drag-n-drop
Open

feat(table): drag and drop for table rows#3983
albinhallen wants to merge 3 commits intomainfrom
feat/table-rows-drag-n-drop

Conversation

@albinhallen
Copy link
Copy Markdown

@albinhallen albinhallen commented Mar 27, 2026

Summary by CodeRabbit

  • New Features

    • Draggable row reordering for tables with visible drag handles
    • New reorder event emitted when rows are moved
  • Examples

    • Added an interactive example showing movable rows and current row order display
  • Style

    • New and updated styles for drag handles, sticky row-selector behavior, and spacing
  • Tests

    • Added tests covering drag-handle rendering and row-reorder behavior

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
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

Adds row drag-and-drop reordering to the table: new RowDragManager class, drag-handle styles, a movableRows prop and reorder event on the table, tests, and an example demonstrating movable rows and order updates.

Changes

Cohort / File(s) Summary
Row Drag Manager & Tests
src/components/table/row-drag-manager.ts, src/components/table/row-drag-manager.spec.ts
New RowDragManager that provides a Tabulator rowHeader formatter using an ElementPool, handles cellClick, and emits RowReorderEvent via handleRowMoved; accompanied by unit tests for formatter, pool interaction, click handling, and reorder emission cases.
Table Integration & Types
src/components/table/table.tsx, src/components/table/table.types.ts
Adds @Prop() movableRows: boolean and @Event() reorder: EventEmitter<RowReorderEvent<any>>; integrates RowDragManager lifecycle, merges row-drag options into Tabulator, conditionally registers rowMoved listener, and introduces RowReorderEvent<T> type.
Examples
src/components/table/examples/table-movable-rows.tsx
New Stencil example limel-example-table-movable-rows demonstrating movable rows and updating tableData on onReorder, rendering current row order.
Styles: Drag Handle & Selection
src/components/table/partial-styles/_row-drag-handle.scss, src/components/table/table.scss, src/components/table/partial-styles/_row-selection.scss
New drag-handle partial with sizing, opacity and interaction rules; forwarded in table.scss and exposes --limel-table-drag-handle-width; row-selection partial refactored to share sticky positioning and offset when .has-movable-rows.

Sequence Diagram(s)

sequenceDiagram
  participant Table as "Table Component\n(limel-table)"
  participant Tabulator as "Tabulator"
  participant RowMgr as "RowDragManager"
  participant Pool as "ElementPool"
  participant Emitter as "reorder EventEmitter"

  Table->>RowMgr: init(pool, reorderEmitter, language)
  RowMgr->>Pool: get('limel-drag-handle')
  Pool-->>RowMgr: HTMLElement
  RowMgr-->>Table: provide rowHeader formatter (uses element + setElementProperties)
  Table->>Tabulator: create(options including rowHeader)
  Note over Tabulator,RowMgr: User drags a row
  Tabulator->>RowMgr: rowMoved(row)
  RowMgr->>Tabulator: inspect prev/next rows
  RowMgr->>Emitter: emit RowReorderEvent{fromRow,toRow,above}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

maintenance, visual design

Suggested reviewers

  • jgroth
  • eketorp
  • Befkadu1
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(table): drag and drop for table rows' directly and clearly summarizes the main feature added: drag-and-drop functionality for table rows, which is the primary focus of all changes across the PR.

✏️ 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 feat/table-rows-drag-n-drop

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.

@albinhallen albinhallen force-pushed the feat/table-rows-drag-n-drop branch 2 times, most recently from 1c87160 to 778f86f Compare March 30, 2026 13:24
@github-actions
Copy link
Copy Markdown

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

@albinhallen albinhallen force-pushed the feat/table-rows-drag-n-drop branch from ff87b90 to 2121ff7 Compare March 31, 2026 13:11
@albinhallen albinhallen marked this pull request as ready for review March 31, 2026 13:12
@albinhallen albinhallen requested a review from a team as a code owner March 31, 2026 13:12
Copy link
Copy Markdown
Contributor

@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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 27-41: The handleReorder method uses items.indexOf(fromRow) and
indexOf(toRow) which relies on reference equality and can fail if rows are
cloned; change to use findIndex comparing a stable identifier (e.g. row.id) for
both fromRow and toRow (use items.findIndex(item => item.id === fromRow.id) and
similarly for toRow), handle the case where a findIndex returns -1 (abort or
no-op), and remove the unnecessary "as Bird" cast since fromRow is already
typed; update references to tableData and RowReorderEvent<Bird> accordingly.
- Around line 46-57: The render method currently returns an array of elements;
replace that array literal with a single <Host> wrapper containing the
<limel-table ... /> and <limel-example-value ... /> elements so StencilJS
guidelines are followed. Locate the return in the render function that returns
the array with limel-table (using props tableData, columns, movableRows and
onReorder pointing to handleReorder) and limel-example-value (using rowOrder)
and wrap those JSX elements in <Host>...</Host> instead of returning an array.

In `@src/components/table/row-drag-manager.spec.ts`:
- Around line 60-80: The tests are for an old implementation; update them to use
the current RowDragManager constructor signature (RowDragManager(pool:
ElementPool, reorderEvent: EventEmitter<RowReorderEvent<any>>, language:
Languages)) and remove the five-parameter setup; create simple mocks for
ElementPool (with get returning an element) and EventEmitter (with emit spy),
instantiate manager with those mocks and 'en', then replace the old
pointer-event tests with tests for getRowHeaderDefinition() (assert
headerSort:false, resizable:false, frozen:true, rowHandle:true,
cssClass:'limel-table--drag-handle') and for handleRowMoved(row) (mock rows via
getData/getPrevRow/getNextRow and assert reorderEvent.emit called with correct
fromRow/toRow and above:true|false). Ensure references to RowDragManager,
getRowHeaderDefinition, and handleRowMoved are used to locate code.

In `@src/components/table/row-drag-manager.ts`:
- Around line 15-19: The constructor parameters in RowDragManager (pool,
reorderEvent, language) are never reassigned and should be made immutable;
update the constructor signature in row-drag-manager.ts to mark these parameters
as readonly (e.g., change "private pool: ElementPool, private reorderEvent:
EventEmitter<RowReorderEvent<any>>, private language: Languages" to use "private
readonly" for each) so the fields are readonly members of the class.
- Around line 14-79: The implementation must match the tests' expected API and
lifecycle: change the RowDragManager constructor signature to (getTableFunc,
getShadowRootFunc, pool, emitter, language) and remove reliance on Tabulator's
rowMoved event (handleRowMoved binding can remain but update usage), then add
the missing public methods getColumnDefinitions(columns), setup(), destroy(),
and setDisabled(disabled) implementing the custom drag strategy the tests expect
(attach pointer event listeners via addEventListener in setup using
getTableFunc/getShadowRootFunc and store handlers to remove in destroy; use pool
and emitter to create/emit drag events instead of Tabulator rowMoved). Ensure
getColumnDefinitions returns a column definition that renders the
limel-drag-handle (reuse getDragHandleFormatter), setup registers
pointerdown/pointermove/pointerup listeners and setDisabled toggles
adding/removing listeners or a flag to ignore events, and destroy removes all
listeners and clears any references.
🪄 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: 2c207983-4aca-4cb7-b8f2-940053d9a519

📥 Commits

Reviewing files that changed from the base of the PR and between 8f08916 and 2121ff7.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (8)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.scss
  • src/components/table/table.tsx
  • src/components/table/table.types.ts
  • src/components/tooltip/tooltip.tsx

Comment thread src/components/table/examples/table-movable-rows.tsx
Comment thread src/components/table/examples/table-movable-rows.tsx Outdated
Comment thread src/components/table/row-drag-manager.spec.ts
Comment thread src/components/table/row-drag-manager.ts
Comment thread src/components/table/row-drag-manager.ts
Copy link
Copy Markdown
Contributor

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/row-drag-manager.spec.ts`:
- Around line 64-73: Tests are flaking because the module-level mock
setElementProperties retains call history between tests; add vi.clearAllMocks()
to the test suite setup (inside the existing beforeEach) so each test runs with
a fresh mock state—update the spec's beforeEach to call vi.clearAllMocks()
before invoking any setup that uses manager.getRowHeaderDefinition or other
helpers so the expectation on setElementProperties only reflects calls from the
current test.
🪄 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: fe0162bb-a8a7-41b8-b13f-b9525f14a94e

📥 Commits

Reviewing files that changed from the base of the PR and between 2121ff7 and 5d23eb6.

📒 Files selected for processing (1)
  • src/components/table/row-drag-manager.spec.ts

Comment thread src/components/table/row-drag-manager.spec.ts
@albinhallen albinhallen force-pushed the feat/table-rows-drag-n-drop branch 2 times, most recently from d895486 to a685c48 Compare March 31, 2026 14:42
Copy link
Copy Markdown
Contributor

@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: 4

♻️ Duplicate comments (1)
src/components/table/examples/table-movable-rows.tsx (1)

34-39: ⚠️ Potential issue | 🟡 Minor

Use findIndex for toIndex lookup and remove unnecessary cast.

Line 34 uses indexOf(toRow) which relies on reference equality and will return -1 if Tabulator clones row objects. Use findIndex consistently with line 31. Also, the as Bird cast on line 39 is unnecessary since fromRow is already typed as Bird.

♻️ Proposed fix
-        let toIndex = items.indexOf(toRow);
+        let toIndex = items.findIndex((bird) => bird.name === toRow.name);
         if (!above) {
             toIndex += 1;
         }

-        items.splice(toIndex, 0, fromRow as Bird);
+        items.splice(toIndex, 0, fromRow);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 34 - 39,
Replace the indexOf-based lookup and the unnecessary cast: use findIndex like
the earlier lookup (e.g., change items.indexOf(toRow) to items.findIndex(r =>
r.id === toRow.id or the same predicate used at line 31) to avoid
reference-equality failures when Tabulator clones rows) and remove the redundant
"as Bird" cast on fromRow so items.splice(toIndex, 0, fromRow) is used; ensure
the same identification predicate as used elsewhere (line 31) is applied for
consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 20-25: The class has members that are never reassigned — mark the
fields as readonly: change the columns declaration to readonly columns:
Array<Column<Bird>> and mark the handler method signature as private readonly
handleReorder (or make the method a readonly arrow property if pattern requires)
so both columns and handleReorder are immutable; update any usage sites (e.g.,
where handleReorder is passed as a callback) to the new readonly member form if
necessary.
- Around line 46-61: In the Host JSX return for the table-movable-rows example
remove the stray commas that are rendered as text nodes: delete the comma after
the <limel-table ... onReorder={this.handleReorder} /> element and the comma
after the <limel-example-value ... /> element so the Host children are just the
two JSX elements (Host, limel-table, limel-example-value, handleReorder).

In `@src/components/table/row-drag-manager.ts`:
- Around line 63-66: The property handleCellClick on the RowDragManager class is
an arrow-function field that is never reassigned; mark it readonly to reflect
immutability. Update the declaration of handleCellClick to be a readonly class
property (e.g., readonly handleCellClick = (ev: Event): void => { ... }) so
static analysis no longer flags it as mutable.

In `@src/components/tooltip/tooltip.tsx`:
- Around line 121-124: The teardown calls this.tooltipTimer.hide() but
TooltipTimer's constructor incorrectly assigns the hide callback to the show
callback, causing hide() to run the wrong function and flip open to true; update
the TooltipTimer constructor in tooltip-timer.ts so the constructor parameters
are stored to the proper fields (assign the hide callback to the hide callback
member and the show callback to the show callback member) and verify
TooltipTimer.hide() invokes the hide callback (and TooltipTimer.show() invokes
the show callback).

---

Duplicate comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 34-39: Replace the indexOf-based lookup and the unnecessary cast:
use findIndex like the earlier lookup (e.g., change items.indexOf(toRow) to
items.findIndex(r => r.id === toRow.id or the same predicate used at line 31) to
avoid reference-equality failures when Tabulator clones rows) and remove the
redundant "as Bird" cast on fromRow so items.splice(toIndex, 0, fromRow) is
used; ensure the same identification predicate as used elsewhere (line 31) is
applied for consistency.
🪄 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: 28711bfd-d459-4bd1-ab0a-ab533ca76632

📥 Commits

Reviewing files that changed from the base of the PR and between 5d23eb6 and d895486.

📒 Files selected for processing (6)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.tsx
  • src/components/tooltip/tooltip.tsx

Comment thread src/components/table/examples/table-movable-rows.tsx
Comment thread src/components/table/examples/table-movable-rows.tsx
Comment thread src/components/table/row-drag-manager.ts
Comment on lines 121 to 124
public disconnectedCallback() {
this.removeListeners();
this.tooltipTimer.hide();
}
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 | 🔴 Critical

Line 123 now deterministically triggers the wrong callback during teardown

this.tooltipTimer.hide() (Line 123) calls TooltipTimer.hide(), and in src/components/tooltip/tooltip-timer.ts the constructor currently assigns hideCallback to showCallback. That means teardown can set open to true, which is a functional regression.

💡 Proposed fix (root cause in tooltip-timer.ts)
--- a/src/components/tooltip/tooltip-timer.ts
+++ b/src/components/tooltip/tooltip-timer.ts
@@
     constructor(
         showCallback: Function,
         hideCallback: Function,
         delayForShowing: number = DEFAULT_DELAY_FOR_SHOWING
     ) {
         this.showCallback = showCallback;
-        this.hideCallback = showCallback;
+        this.hideCallback = hideCallback;
         this.delayForShowing = delayForShowing;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/tooltip/tooltip.tsx` around lines 121 - 124, The teardown
calls this.tooltipTimer.hide() but TooltipTimer's constructor incorrectly
assigns the hide callback to the show callback, causing hide() to run the wrong
function and flip open to true; update the TooltipTimer constructor in
tooltip-timer.ts so the constructor parameters are stored to the proper fields
(assign the hide callback to the hide callback member and the show callback to
the show callback member) and verify TooltipTimer.hide() invokes the hide
callback (and TooltipTimer.show() invokes the show callback).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hallucination? I doesn't look like that

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.

@albinhallen could you please make a separate PR for this change; and explain the issue in that PR? 🙏

I'm guessing you are changing this because the tooltip remains after drag and drop or something like that. But that's a separate problem and it's better to be handled in a separate PR

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.

If my assumption is correct about why you are doing it, you can test and use this example in limel-form for that secondary PR

Image

Copy link
Copy Markdown
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/table/table.tsx (1)

316-319: ⚠️ Potential issue | 🟠 Major

Recreate the drag manager when its prerequisites change.

pageSizeChanged() only updates max page, and there are no @Watch('mode') / @Watch('language') hooks, even though initRowDragManager() depends on all three props. That means a live table can keep row dragging enabled after pagination or remote mode is turned on, stay disabled after those constraints are removed, and keep using the old drag-handle language after language changes.

Also applies to: 444-449, 608-623

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 316 - 319, The pageSizeChanged
handler only calls updateMaxPage but does not recreate the row drag manager even
though initRowDragManager depends on pageSize, mode and language; add
`@Watch`('mode') and `@Watch`('language') handlers and update pageSizeChanged to
also recreate the drag manager (or add a shared helper like
recreateRowDragManager called by all three watchers) so that changes to
pageSize, mode or language tear down any existing manager (call an existing
destroyRowDragManager or null-check and remove handlers) and then call
initRowDragManager() to reinitialize with the current props; reference
initRowDragManager, pageSizeChanged, and the new Watch('mode')/Watch('language')
methods when making the change.
♻️ Duplicate comments (1)
src/components/table/examples/table-movable-rows.tsx (1)

27-40: ⚠️ Potential issue | 🟡 Minor

Use the same stable lookup for toRow as fromRow.

fromRow already avoids reference equality, but toRow still uses indexOf(). If the event detail is equivalent but not the exact same object instance, toIndex becomes -1 and this example inserts in the wrong place. Guard the lookup failure as well, and drop the redundant cast.

♻️ Suggested update
     private handleReorder = (event: CustomEvent<RowReorderEvent<Bird>>) => {
         const { fromRow, toRow, above } = event.detail;
         const items = [...this.tableData];

         const fromIndex = items.findIndex((bird) => bird.name === fromRow.name);
+        if (fromIndex < 0) {
+            return;
+        }
         items.splice(fromIndex, 1);

-        let toIndex = items.indexOf(toRow);
+        let toIndex = items.findIndex((bird) => bird.name === toRow.name);
+        if (toIndex < 0) {
+            return;
+        }
         if (!above) {
             toIndex += 1;
         }

-        items.splice(toIndex, 0, fromRow as Bird);
+        items.splice(toIndex, 0, fromRow);
         this.tableData = items;
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 27 - 40,
The handleReorder method uses a stable lookup for fromRow but uses
indexOf(toRow) which can fail if toRow is an equivalent object rather than the
same instance; change the toIndex lookup to the same stable findIndex strategy
(e.g., match by unique key such as bird.name) and guard if findIndex returns -1
before adjusting for above, and remove the redundant "as Bird" cast when
splicing fromRow into items; update references to fromRow, toRow, items and
this.tableData in handleReorder accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/partial-styles/_row-drag-handle.scss`:
- Line 1: The selector .limel-table--drag-handle is a BEM-style modifier used
inside a shadow-DOM component; rename this class to a small local non-BEM name
(e.g., .row-drag-handle or .dragHandle) in _row-drag-handle.scss and update all
places that reference it (the component's cssClass usage and any spec
assertions/tests) within the same PR so the style and tests stay in sync; ensure
you only change the class name string, run tests, and verify the shadowed
component still applies the class where needed.

---

Outside diff comments:
In `@src/components/table/table.tsx`:
- Around line 316-319: The pageSizeChanged handler only calls updateMaxPage but
does not recreate the row drag manager even though initRowDragManager depends on
pageSize, mode and language; add `@Watch`('mode') and `@Watch`('language') handlers
and update pageSizeChanged to also recreate the drag manager (or add a shared
helper like recreateRowDragManager called by all three watchers) so that changes
to pageSize, mode or language tear down any existing manager (call an existing
destroyRowDragManager or null-check and remove handlers) and then call
initRowDragManager() to reinitialize with the current props; reference
initRowDragManager, pageSizeChanged, and the new Watch('mode')/Watch('language')
methods when making the change.

---

Duplicate comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 27-40: The handleReorder method uses a stable lookup for fromRow
but uses indexOf(toRow) which can fail if toRow is an equivalent object rather
than the same instance; change the toIndex lookup to the same stable findIndex
strategy (e.g., match by unique key such as bird.name) and guard if findIndex
returns -1 before adjusting for above, and remove the redundant "as Bird" cast
when splicing fromRow into items; update references to fromRow, toRow, items and
this.tableData in handleReorder accordingly.
🪄 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: 223ca053-66d9-4836-bb55-a661f536c5a7

📥 Commits

Reviewing files that changed from the base of the PR and between d895486 and a685c48.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.tsx
  • src/components/tooltip/tooltip.tsx

Comment thread src/components/table/partial-styles/_row-drag-handle.scss Outdated
@albinhallen albinhallen force-pushed the feat/table-rows-drag-n-drop branch 2 times, most recently from c964863 to 3d1f294 Compare April 1, 2026 07:30
Copy link
Copy Markdown
Contributor

@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: 2

♻️ Duplicate comments (1)
src/components/table/examples/table-movable-rows.tsx (1)

27-40: ⚠️ Potential issue | 🟡 Minor

Stop using reference equality for the drop target.

Line 34 still uses items.indexOf(toRow). If the event detail is not the same object instance stored in tableData, indexOf returns -1 and the demo reorders incorrectly. Match both rows by the same stable field and move the existing array item instead of casting fromRow.

Suggested fix
     private handleReorder = (event: CustomEvent<RowReorderEvent<Bird>>) => {
         const { fromRow, toRow, above } = event.detail;
         const items = [...this.tableData];
 
         const fromIndex = items.findIndex((bird) => bird.name === fromRow.name);
-        items.splice(fromIndex, 1);
+        if (fromIndex === -1) {
+            return;
+        }
+
+        const [movedBird] = items.splice(fromIndex, 1);
 
-        let toIndex = items.indexOf(toRow);
+        let toIndex = items.findIndex((bird) => bird.name === toRow.name);
+        if (toIndex === -1) {
+            return;
+        }
+
         if (!above) {
             toIndex += 1;
         }
 
-        items.splice(toIndex, 0, fromRow as Bird);
+        items.splice(toIndex, 0, movedBird);
         this.tableData = items;
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 27 - 40,
The handler handleReorder uses reference equality (items.indexOf(toRow)) and
casts event.detail.fromRow which breaks if the event provides different object
instances; instead locate both indices with a stable key (e.g., use
items.findIndex(b => b.name === fromRow.name) and items.findIndex(b => b.name
=== toRow.name)), remove the actual item from items (store it from
items.splice(fromIndex,1)[0]) and insert that stored item at the computed
toIndex (adjusting when above is false) before assigning this.tableData = items.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/row-drag-manager.spec.ts`:
- Around line 32-154: Add a browser/e2e Vitest+Playwright test that performs a
real drag gesture on the rendered limel-table and asserts that the component
emits the 'reorder' event (this covers DOM wiring missing from RowDragManager
unit tests). Create a new spec that mounts limel-table with at least three rows,
simulate pointerdown + pointermove + pointerup (or use Playwright drag and drop)
on the row drag handle element (CSS class 'limel-table-drag-handle'), and listen
for the component's 'reorder' event to verify payload (fromRow/toRow/above)
matches the expected reorder; reference the RowDragManager behavior in
assertions to ensure parity with the existing unit tests. Ensure the test runs
in the existing Vitest Browser Mode configuration and uses the same mock data
shape as the unit tests (e.g., rows with id/name) so the emitted event structure
matches what handleRowMoved produces.

In `@src/components/table/table.tsx`:
- Around line 444-449: The row-drag state is only refreshed in the movableRows
watcher (updateMovableRows) so changes to mode or pageSize can leave
rowDragManager stale; update watchers for mode and pageSize (or centralize in a
method) to perform the same work as updateMovableRows: set this.rowDragManager =
null, call this.initRowDragManager(), and this.init() so drag handles are
re-evaluated whenever mode or pageSize changes; locate updateMovableRows,
initRowDragManager, and init to implement the same refresh logic or call a
shared helper from the mode and pageSize watchers.

---

Duplicate comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 27-40: The handler handleReorder uses reference equality
(items.indexOf(toRow)) and casts event.detail.fromRow which breaks if the event
provides different object instances; instead locate both indices with a stable
key (e.g., use items.findIndex(b => b.name === fromRow.name) and
items.findIndex(b => b.name === toRow.name)), remove the actual item from items
(store it from items.splice(fromIndex,1)[0]) and insert that stored item at the
computed toIndex (adjusting when above is false) before assigning this.tableData
= items.
🪄 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: d33aadcd-10b3-42ef-873d-4564f5c3586d

📥 Commits

Reviewing files that changed from the base of the PR and between a685c48 and 3d1f294.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.tsx
  • src/components/tooltip/tooltip.tsx

Comment on lines +32 to +154
describe('RowDragManager', () => {
let manager: RowDragManager;
let mockPool: ReturnType<typeof createMockPool>;
let mockEmitter: ReturnType<typeof createMockEventEmitter>;

beforeEach(() => {
vi.clearAllMocks();
mockPool = createMockPool();
mockEmitter = createMockEventEmitter();

manager = new RowDragManager(mockPool as any, mockEmitter as any, 'en');
});

describe('getRowHeaderDefinition', () => {
it('returns a column definition with correct properties', () => {
const definition = manager.getRowHeaderDefinition() as any;

expect(definition.headerSort).toBe(false);
expect(definition.resizable).toBe(false);
expect(definition.frozen).toBe(true);
expect(definition.rowHandle).toBe(true);
expect(definition.cssClass).toEqual('limel-table-drag-handle');
});

it('provides a formatter that uses the element pool', () => {
const definition = manager.getRowHeaderDefinition() as any;
const formatter = definition.formatter as () => HTMLElement;

const element = formatter();
expect(mockPool.get).toHaveBeenCalledWith('limel-drag-handle');
expect(element).toBeTruthy();
});

it('sets drag handle properties via setElementProperties', () => {
const definition = manager.getRowHeaderDefinition() as any;
const formatter = definition.formatter as () => HTMLElement;

formatter();
expect(setElementProperties).toHaveBeenCalledWith(
expect.any(HTMLElement),
{ dragDirection: 'vertical', language: 'en' }
);
});

it('provides a cellClick handler that stops propagation', () => {
const definition = manager.getRowHeaderDefinition() as any;
const event = {
stopPropagation: vi.fn(),
preventDefault: vi.fn(),
};

definition.cellClick(event);

expect(event.stopPropagation).toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
});
});

describe('handleRowMoved', () => {
it('emits reorder event with above=false when row has a previous row', () => {
const prevRow = createMockRow({ id: 1, name: 'Alice' });
const movedRow = createMockRow(
{ id: 2, name: 'Bob' },
prevRow,
false
);

manager.handleRowMoved(movedRow as any);

expect(mockEmitter.emit).toHaveBeenCalledWith({
fromRow: { id: 2, name: 'Bob' },
toRow: { id: 1, name: 'Alice' },
above: false,
});
});

it('emits reorder event with above=true when row is first (no previous row)', () => {
const nextRow = createMockRow({ id: 2, name: 'Bob' });
const movedRow = createMockRow(
{ id: 1, name: 'Alice' },
false,
nextRow
);

manager.handleRowMoved(movedRow as any);

expect(mockEmitter.emit).toHaveBeenCalledWith({
fromRow: { id: 1, name: 'Alice' },
toRow: { id: 2, name: 'Bob' },
above: true,
});
});

it('does not emit when row has no neighbors', () => {
const movedRow = createMockRow(
{ id: 1, name: 'Alice' },
false,
false
);

manager.handleRowMoved(movedRow as any);

expect(mockEmitter.emit).not.toHaveBeenCalled();
});

it('prefers previous row over next row for positioning', () => {
const prevRow = createMockRow({ id: 1, name: 'Alice' });
const nextRow = createMockRow({ id: 3, name: 'Charlie' });
const movedRow = createMockRow(
{ id: 2, name: 'Bob' },
prevRow,
nextRow
);

manager.handleRowMoved(movedRow as any);

expect(mockEmitter.emit).toHaveBeenCalledWith({
fromRow: { id: 2, name: 'Bob' },
toRow: { id: 1, name: 'Alice' },
above: false,
});
});
});
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

Add one browser-level reorder test.

These cases validate RowDragManager in isolation, but they never prove that limel-table emits reorder after a real drag gesture. One browser/e2e case around the table component would cover the DOM wiring that this suite cannot.

Based on learnings, the e2e test project already uses Vitest Browser Mode with Playwright, so this interaction can be covered in-browser.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/row-drag-manager.spec.ts` around lines 32 - 154, Add a
browser/e2e Vitest+Playwright test that performs a real drag gesture on the
rendered limel-table and asserts that the component emits the 'reorder' event
(this covers DOM wiring missing from RowDragManager unit tests). Create a new
spec that mounts limel-table with at least three rows, simulate pointerdown +
pointermove + pointerup (or use Playwright drag and drop) on the row drag handle
element (CSS class 'limel-table-drag-handle'), and listen for the component's
'reorder' event to verify payload (fromRow/toRow/above) matches the expected
reorder; reference the RowDragManager behavior in assertions to ensure parity
with the existing unit tests. Ensure the test runs in the existing Vitest
Browser Mode configuration and uses the same mock data shape as the unit tests
(e.g., rows with id/name) so the emitted event structure matches what
handleRowMoved produces.

@albinhallen albinhallen requested a review from Kiarokh April 1, 2026 08:58
Comment thread src/components/table/examples/table-movable-rows.tsx
@Kiarokh Kiarokh force-pushed the feat/table-rows-drag-n-drop branch from 3d1f294 to 62ab3ad Compare April 1, 2026 10:00
import { capitalize } from 'lodash-es';

/**
* Movable rows
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.

Screen.Recording.2026-04-01.at.12.02.46.mov

Something isn't right. Scrolling on the page creates this laggy weird behaviour. I'm gonna just post this for now, until we can identify where the issue comes from.

Copy link
Copy Markdown
Contributor

@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.

♻️ Duplicate comments (1)
src/components/table/table.tsx (1)

444-449: ⚠️ Potential issue | 🟠 Major

Re-evaluate row dragging when pageSize or mode changes.

initRowDragManager() correctly gates the feature on pagination and mode, but the manager is only rebuilt from updateMovableRows(). If movableRows is already true and the parent later enables pagination or switches to remote, the table keeps the stale drag configuration until a full remount.

♻️ Proposed fix
 `@Watch`('pageSize')
 protected pageSizeChanged() {
     this.updateMaxPage();
+    this.refreshRowDragManager();
 }

+@Watch('mode')
+protected modeChanged() {
+    this.refreshRowDragManager();
+}
+
 `@Watch`('movableRows')
 protected updateMovableRows() {
+    this.refreshRowDragManager();
+}
+
+private refreshRowDragManager() {
     this.rowDragManager = null;
     this.initRowDragManager();
     this.init();
 }

Also applies to: 608-623

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 444 - 449, The table's row-drag
manager is only rebuilt in updateMovableRows(), so changes to pageSize or mode
leave a stale configuration; add watchers for the pageSize and mode properties
(or have their existing watchers call the same logic) so that on changes you
reset this.rowDragManager = null and call this.initRowDragManager();
this.init(); — i.e., ensure that the watchers for pageSize and mode invoke the
same update flow as updateMovableRows() (referencing updateMovableRows,
initRowDragManager, init, and rowDragManager).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/table/table.tsx`:
- Around line 444-449: The table's row-drag manager is only rebuilt in
updateMovableRows(), so changes to pageSize or mode leave a stale configuration;
add watchers for the pageSize and mode properties (or have their existing
watchers call the same logic) so that on changes you reset this.rowDragManager =
null and call this.initRowDragManager(); this.init(); — i.e., ensure that the
watchers for pageSize and mode invoke the same update flow as
updateMovableRows() (referencing updateMovableRows, initRowDragManager, init,
and rowDragManager).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4f406521-e32f-4951-a019-0a5de4fab28a

📥 Commits

Reviewing files that changed from the base of the PR and between 3d1f294 and 62ab3ad.

⛔ Files ignored due to path filters (2)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.scss
  • src/components/table/table.tsx
  • src/components/table/table.types.ts
  • src/components/tooltip/tooltip.tsx

value={rowOrder}
/>
</Host>
);
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.

I won't do a review about the technical changes. But this change one does not belong to this commit. It makes it hard for the reviewer to review the commit itself. It should have been rather squashed to the first commit.

I'd leave it up to you whether you want to have one commit first, relying on Tabulator's inbuilt stuff, and a second commit later, for using Lime Element's inbuilt drag and drop mechanism.

Comment thread src/components/table/table.tsx Outdated
Comment on lines +611 to +614
console.warn(
'Row dragging is not supported in remote mode. Please set `mode` to `local` to enable row dragging.'
);
return;
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.

if this approach is valid or inevitable, the consumer facing documentations (example's docs) should explain it too. Console warning is good, but not enough.

I also see that you mentioned it in the movableRows prop's docs. 👍 But the actual example should explain why this is the case (only works only with local and when no pagination).

For a "user" however, it might be strange if they suddenly lose their ability to sort manually. This feature in other words means for them that the table is sorted manually, not based on a column's values. So why should it matter? Can't I just save the table with my own sorting anymore, just because since yesterday, one new row was added and the table got a pagination…

Note that the user can still click the header of a column and resort it, then drag and drop rows. So I'm a bit confused about why we offer this feature, and how consumers or users expect to use it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

For a "user" however, it might be strange if they suddenly lose their ability to sort manually. This feature in other words means for them that the table is sorted manually, not based on a column's values. So why should it matter? Can't I just save the table with my own sorting anymore, just because since yesterday, one new row was added and the table got a pagination…

Note that the user can still click the header of a column and resort it, then drag and drop rows. So I'm a bit confused about why we offer this feature, and how consumers or users expect to use it.

Totally agree about the use case. In forms, we have the sorting disabled so for specifically that use case it makes sense. Since then the order of which items should appear is inferred from the "moving"/sorting.

In that case it's also not necessarily about just displaying data, but also managing/configuring the order of data. It feels like in 99% of cases tables ar used for just displaying data, so that's why it's kind of a weird one :)
We've discussed using a List component instead, but feel we get more structure with a table.

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.

yes, I don't think list is the right choice. But we should at least properly add documentations.
Also if this is an absolute impossibility, the component should by itself force-disable those features, once this prop is set to true. Otherwise we can get conflicting behaviors.

However, I don't know why a user shouldn't be able to sort items manually, inside each page of a table separately. I can imagine legit use cases for that. Can't the table emit the new sorting, and the consumer store that somewhere? If pagination is a problem, then the consumer should decide on their own and disable it.

I don't think it's a good idea that the table does it for them.

@Kiarokh Kiarokh force-pushed the feat/table-rows-drag-n-drop branch from eccee22 to aa3423e Compare April 1, 2026 12:09
Copy link
Copy Markdown
Contributor

@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.

♻️ Duplicate comments (4)
src/components/table/examples/table-movable-rows.tsx (2)

20-27: 🧹 Nitpick | 🔵 Trivial

Mark columns and handleReorder as readonly.

Per static analysis, these members are never reassigned.

♻️ Proposed fix
-    private columns: Array<Column<Bird>> = [
+    private readonly columns: Array<Column<Bird>> = [
         { title: 'Name', field: 'name' },
         { title: 'Binominal name', field: 'binominalName' },
         { title: 'Nest type', field: 'nest', formatter: capitalize },
         { title: 'Eggs per clutch', field: 'eggs', horizontalAlign: 'right' },
     ];

-    private handleReorder = (event: CustomEvent<RowReorderEvent<Bird>>) => {
+    private readonly handleReorder = (event: CustomEvent<RowReorderEvent<Bird>>) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 20 - 27,
Mark the class members that are never reassigned as readonly: add the readonly
modifier to the private property columns (private readonly columns:
Array<Column<Bird>>) and to the handler method declaration handleReorder
(private readonly handleReorder = ...) so the static analyzer knows they are
immutable; keep their existing types and implementation unchanged while only
adding the readonly keyword to the two symbols columns and handleReorder.

31-39: ⚠️ Potential issue | 🟡 Minor

Inconsistent index lookup and unnecessary cast.

Line 31 correctly uses findIndex with name comparison to avoid reference equality issues, but Line 34 still uses indexOf(toRow) which relies on reference equality. If toRow is a clone, indexOf returns -1 and the splice inserts at index 0 (or -1 after the !above increment).

Also, the as Bird cast on Line 39 is unnecessary since fromRow is already typed as Bird.

🛡️ Proposed fix
         const fromIndex = items.findIndex((bird) => bird.name === fromRow.name);
         items.splice(fromIndex, 1);

-        let toIndex = items.indexOf(toRow);
+        let toIndex = items.findIndex((bird) => bird.name === toRow.name);
         if (!above) {
             toIndex += 1;
         }

-        items.splice(toIndex, 0, fromRow as Bird);
+        items.splice(toIndex, 0, fromRow);
         this.tableData = items;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 31 - 39,
The code uses items.indexOf(toRow) which relies on reference equality and can
return -1 for cloned rows; change the to-index lookup to use a stable key match
like items.findIndex(b => b.name === toRow.name) (or id if available) and remove
the unnecessary "as Bird" cast on fromRow; ensure you still adjust toIndex when
!above and then perform items.splice(toIndex, 0, fromRow) so both lookups use
the same equality strategy.
src/components/table/row-drag-manager.ts (1)

63-66: 🧹 Nitpick | 🔵 Trivial

Mark handleCellClick as readonly.

Per static analysis, this arrow function property is never reassigned.

♻️ Proposed fix
-    private handleCellClick = (ev: Event): void => {
+    private readonly handleCellClick = (ev: Event): void => {
         ev.stopPropagation();
         ev.preventDefault();
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/row-drag-manager.ts` around lines 63 - 66, The
handleCellClick arrow property on the RowDragManager class is never reassigned;
mark it as readonly to satisfy static analysis by changing its declaration to a
readonly property (i.e., add the readonly modifier to the existing private
handleCellClick = (ev: Event): void => { ... } declaration) while preserving the
function body and type signature.
src/components/table/table.tsx (1)

608-623: ⚠️ Potential issue | 🟠 Major

Refresh row-drag state when mode or pageSize changes.

The initRowDragManager gates the feature on both mode === 'local' and !this.pageSize, but only the movableRows watcher triggers re-initialization. If pageSize or mode changes after initialization, rowDragManager can become stale—keeping drag handles enabled in unsupported states, or never enabling them when the table becomes eligible.

🛠️ Suggested fix
     `@Watch`('pageSize')
     protected pageSizeChanged() {
         this.updateMaxPage();
+        this.refreshRowDragManager();
     }

+    `@Watch`('mode')
+    protected modeChanged() {
+        this.refreshRowDragManager();
+    }

     `@Watch`('movableRows')
     protected updateMovableRows() {
-        this.rowDragManager = null;
-        this.initRowDragManager();
-        this.init();
+        this.refreshRowDragManager();
     }

+    private refreshRowDragManager() {
+        this.rowDragManager = null;
+        this.initRowDragManager();
+        this.init();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 608 - 623, The
initRowDragManager logic can become stale when mode or pageSize changes; update
the component to re-run that logic whenever mode or pageSize toggles by adding
watchers (or reactive effects) that call initRowDragManager, and inside
initRowDragManager ensure you destroy/clear this.rowDragManager when conditions
fail (e.g., when isRemoteMode() becomes true or pageSize is set) and create a
new RowDragManager when movableRows is true, mode becomes local, and pageSize is
falsy; reference initRowDragManager, movableRows, pageSize, isRemoteMode(), and
rowDragManager to implement these watchers and proper cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 20-27: Mark the class members that are never reassigned as
readonly: add the readonly modifier to the private property columns (private
readonly columns: Array<Column<Bird>>) and to the handler method declaration
handleReorder (private readonly handleReorder = ...) so the static analyzer
knows they are immutable; keep their existing types and implementation unchanged
while only adding the readonly keyword to the two symbols columns and
handleReorder.
- Around line 31-39: The code uses items.indexOf(toRow) which relies on
reference equality and can return -1 for cloned rows; change the to-index lookup
to use a stable key match like items.findIndex(b => b.name === toRow.name) (or
id if available) and remove the unnecessary "as Bird" cast on fromRow; ensure
you still adjust toIndex when !above and then perform items.splice(toIndex, 0,
fromRow) so both lookups use the same equality strategy.

In `@src/components/table/row-drag-manager.ts`:
- Around line 63-66: The handleCellClick arrow property on the RowDragManager
class is never reassigned; mark it as readonly to satisfy static analysis by
changing its declaration to a readonly property (i.e., add the readonly modifier
to the existing private handleCellClick = (ev: Event): void => { ... }
declaration) while preserving the function body and type signature.

In `@src/components/table/table.tsx`:
- Around line 608-623: The initRowDragManager logic can become stale when mode
or pageSize changes; update the component to re-run that logic whenever mode or
pageSize toggles by adding watchers (or reactive effects) that call
initRowDragManager, and inside initRowDragManager ensure you destroy/clear
this.rowDragManager when conditions fail (e.g., when isRemoteMode() becomes true
or pageSize is set) and create a new RowDragManager when movableRows is true,
mode becomes local, and pageSize is falsy; reference initRowDragManager,
movableRows, pageSize, isRemoteMode(), and rowDragManager to implement these
watchers and proper cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fa50e5a1-2fff-427d-a331-b48922bf65d1

📥 Commits

Reviewing files that changed from the base of the PR and between 62ab3ad and aa3423e.

⛔ Files ignored due to path filters (2)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.scss
  • src/components/table/table.tsx
  • src/components/table/table.types.ts
  • src/components/tooltip/tooltip.tsx

this.rowDragManager = null;
}

this.rowDragManager = null;
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.

Screen.Recording.2026-04-01.at.14.43.16.mov

there is a suboptimal interaction here: while dragging a row, if you hover the region where all drag handles are located, nothing works. user should drag over the rest of the rows for the reordering to happen correctly

@@ -1,74 +1,22 @@
// common styles are written in `_row-selection.scss` and imported in `table.scss`
.limel-table-drag-handle {
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.

I've committed this change as a separate commit, to avoid merge conflicts.
If you decide to squash the two commits of enabling drag and drop in the table, into one commit, this can be squashed to that single commit.

@Kiarokh Kiarokh force-pushed the feat/table-rows-drag-n-drop branch from 4b7e2cb to 5466fff Compare April 1, 2026 13:05
Copy link
Copy Markdown
Contributor

@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: 2

♻️ Duplicate comments (2)
src/components/table/table.tsx (1)

316-319: ⚠️ Potential issue | 🟠 Major

Refresh row-drag state when pageSize or mode changes.

rowDragManager is only rebuilt from the movableRows watcher. If pagination is turned on later, or the table switches to or from remote mode, the drag handles and rowMoved listener stay in their old state. That leaves row dragging enabled in unsupported configurations, or never enables it once the table becomes eligible.

♻️ Suggested fix
 `@Watch`('pageSize')
 protected pageSizeChanged() {
     this.updateMaxPage();
+    this.refreshRowDragManager();
+    this.init();
 }
+
+@Watch('mode')
+protected modeChanged() {
+    this.refreshRowDragManager();
+    this.init();
+}
 
 `@Watch`('movableRows')
 protected updateMovableRows() {
-    this.rowDragManager = null;
-    this.initRowDragManager();
+    this.refreshRowDragManager();
     this.init();
 }
+
+private refreshRowDragManager() {
+    this.rowDragManager = null;
+    this.initRowDragManager();
+}

Also applies to: 444-449, 608-623

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 316 - 319, pageSizeChanged
currently only calls updateMaxPage but must also refresh the row-drag state;
invoke the same row-drag rebuild/cleanup logic used by the movableRows watcher
(e.g., call the method that destroys and rebuilds rowDragManager or a helper
like rebuildRowDragManager) so drag handles and listeners are re-evaluated
whenever pageSize or mode changes; also apply the same call inside the mode
watcher so both pageSizeChanged and the mode watcher mirror the movableRows
watcher behavior for rowDragManager.
src/components/table/examples/table-movable-rows.tsx (1)

31-39: ⚠️ Potential issue | 🟡 Minor

toRow lookup still depends on reference identity.

Line 34 still uses indexOf(toRow), and neither index is guarded. If the event carries a cloned or stale row object, findIndex/indexOf returns -1, splice(-1, 1) removes the last item, and the example shows the wrong order.

♻️ Suggested fix
     const fromIndex = items.findIndex((bird) => bird.name === fromRow.name);
-    items.splice(fromIndex, 1);
+    if (fromIndex === -1) {
+        return;
+    }
+
+    const [movedRow] = items.splice(fromIndex, 1);

-    let toIndex = items.indexOf(toRow);
+    let toIndex = items.findIndex((bird) => bird.name === toRow.name);
+    if (toIndex === -1) {
+        return;
+    }
     if (!above) {
         toIndex += 1;
     }

-    items.splice(toIndex, 0, fromRow as Bird);
+    items.splice(toIndex, 0, movedRow);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 31 - 39,
The current move logic uses reference identity for toRow (items.indexOf(toRow))
which breaks when the event carries a cloned/stale object; replace that indexOf
with a findIndex that matches on a unique key (e.g., items.findIndex(b => b.name
=== toRow.name) or an id property) and guard both fromIndex and toIndex (ensure
fromIndex >= 0 before removing, and if toIndex === -1 compute a sensible
fallback — e.g., set toIndex = items.length when !above to append or toIndex = 0
when above — before calling splice) so splicing never removes the wrong element.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/partial-styles/_row-selection.scss`:
- Around line 20-25: The header select-all checkbox stays at left: 0.375rem
while the selector column (.limel-table--row-selector) is shifted when
.has-movable-rows is present, causing the checkbox to overlap the drag-handle;
update the .select-all rule to also shift when nested inside .has-movable-rows
so it aligns with .limel-table--row-selector (use the same
var(--limel-table-drag-handle-width) offset or calc(var(...) + 0.375rem) if you
need to preserve the existing 0.375rem padding) and ensure you reference
.select-all and .limel-table--row-selector in the patch so the header control
moves in sync with the selector column.

In `@src/components/table/row-drag-manager.ts`:
- Around line 68-77: getDragHandleFormatter() currently calls
this.pool.get(LIMEL_DRAG_HANDLE) and never releases the acquired
limel-drag-handle, causing the pool's used map to grow; either ensure the
element is released back to the pool when the row-header cell is torn down (call
this.pool.release(element) from the row-header cell lifecycle/cleanup path where
the formatter's element is detached) or stop using the pool here and
instantiate/configure the drag-handle outside the pool (create a new
limel-drag-handle in getDragHandleFormatter and remove pool.get usage); update
references in getDragHandleFormatter, pool.get, LIMEL_DRAG_HANDLE and the
row-header cell cleanup code to implement the chosen approach.

---

Duplicate comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 31-39: The current move logic uses reference identity for toRow
(items.indexOf(toRow)) which breaks when the event carries a cloned/stale
object; replace that indexOf with a findIndex that matches on a unique key
(e.g., items.findIndex(b => b.name === toRow.name) or an id property) and guard
both fromIndex and toIndex (ensure fromIndex >= 0 before removing, and if
toIndex === -1 compute a sensible fallback — e.g., set toIndex = items.length
when !above to append or toIndex = 0 when above — before calling splice) so
splicing never removes the wrong element.

In `@src/components/table/table.tsx`:
- Around line 316-319: pageSizeChanged currently only calls updateMaxPage but
must also refresh the row-drag state; invoke the same row-drag rebuild/cleanup
logic used by the movableRows watcher (e.g., call the method that destroys and
rebuilds rowDragManager or a helper like rebuildRowDragManager) so drag handles
and listeners are re-evaluated whenever pageSize or mode changes; also apply the
same call inside the mode watcher so both pageSizeChanged and the mode watcher
mirror the movableRows watcher behavior for rowDragManager.
🪄 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: ef1267cb-3a11-469b-8b52-b2b48a7cee97

📥 Commits

Reviewing files that changed from the base of the PR and between aa3423e and 5466fff.

⛔ Files ignored due to path filters (2)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/partial-styles/_row-selection.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.scss
  • src/components/table/table.tsx
  • src/components/table/table.types.ts
  • src/components/tooltip/tooltip.tsx

Comment thread src/components/table/partial-styles/_row-selection.scss
Comment on lines +68 to +77
private getDragHandleFormatter() {
return () => {
const element = this.pool.get(LIMEL_DRAG_HANDLE);
setElementProperties(element, {
dragDirection: 'vertical',
language: this.language,
});

return element;
};
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== RowDragManager pool usage ==="
rg -n -C2 'pool\.(get|release|releaseAll)\(' src/components/table/row-drag-manager.ts

echo
echo "=== ElementPool semantics ==="
rg -n -C2 'private pool|usedElements|public get\(|public release\(|public releaseAll\(' src/components/table/element-pool.ts

echo
echo "=== Table-level reset paths ==="
rg -n -C2 'releaseAll\(|clear\(|replaceData\(|destroy\(' src/components/table/table.tsx

Repository: Lundalogik/lime-elements

Length of output: 2069


Release row-header drag handles from the formatter, or the pool grows indefinitely on redraws.

getDragHandleFormatter() acquires elements via pool.get() but never releases them. Each row render calls the formatter, acquiring a new drag handle from the pool and marking it as "used". When rows are virtualized or removed, these elements are orphaned but remain marked as used in the WeakMap, forcing the pool to create new elements on subsequent redraws. Over time, this accumulates unreleased limel-drag-handle instances until the table is reinitialized or destroyed.

Add a release mechanism tied to the row-header cell lifecycle, or switch to creating formatter elements outside the pool.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/row-drag-manager.ts` around lines 68 - 77,
getDragHandleFormatter() currently calls this.pool.get(LIMEL_DRAG_HANDLE) and
never releases the acquired limel-drag-handle, causing the pool's used map to
grow; either ensure the element is released back to the pool when the row-header
cell is torn down (call this.pool.release(element) from the row-header cell
lifecycle/cleanup path where the formatter's element is detached) or stop using
the pool here and instantiate/configure the drag-handle outside the pool (create
a new limel-drag-handle in getDragHandleFormatter and remove pool.get usage);
update references in getDragHandleFormatter, pool.get, LIMEL_DRAG_HANDLE and the
row-header cell cleanup code to implement the chosen approach.

@albinhallen albinhallen force-pushed the feat/table-rows-drag-n-drop branch from 5466fff to cf97621 Compare April 2, 2026 15:08
Copy link
Copy Markdown
Contributor

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/table/partial-styles/_row-selection.scss (1)

13-18: ⚠️ Potential issue | 🟠 Major

Header select-all checkbox misaligned when movableRows is enabled.

The .select-all positioning at left: 0.375rem is not adjusted for .has-movable-rows, while .limel-table--row-selector is shifted by --limel-table-drag-handle-width on lines 23-25. When both selectable and movableRows are enabled, the header checkbox will overlap the drag-handle column.

🛠️ Proposed fix
 .select-all {
     position: absolute;
     z-index: var(--limel-table-row-selector-z-index);
     left: 0.375rem;
     top: 0.1875rem;
+
+    .has-movable-rows & {
+        left: calc(var(--limel-table-drag-handle-width) + 0.375rem);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/partial-styles/_row-selection.scss` around lines 13 -
18, The header select-all checkbox (.select-all) doesn't account for the
movable-rows offset, so when .has-movable-rows is present it overlaps the
drag-handle; update the CSS to shift .select-all when .has-movable-rows (or when
inside .limel-table--row-selector with .has-movable-rows) by adding a rule that
increases left by var(--limel-table-drag-handle-width) (e.g., left:
calc(0.375rem + var(--limel-table-drag-handle-width))) so the checkbox aligns
with the shifted .limel-table--row-selector column.
♻️ Duplicate comments (3)
src/components/table/row-drag-manager.ts (1)

63-66: 🧹 Nitpick | 🔵 Trivial

Mark handleCellClick as readonly.

This arrow function property is never reassigned.

♻️ Proposed fix
-    private handleCellClick = (ev: Event): void => {
+    private readonly handleCellClick = (ev: Event): void => {
         ev.stopPropagation();
         ev.preventDefault();
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/row-drag-manager.ts` around lines 63 - 66, The property
handleCellClick is defined as an arrow function and never reassigned; mark it
readonly to reflect immutability and prevent accidental reassignment. Update the
declaration of handleCellClick in the RowDragManager (or the class containing
it) to include the readonly modifier so the signature becomes a readonly
property assigned to the arrow function, keeping the existing implementation
(ev.stopPropagation(); ev.preventDefault()) unchanged.
src/components/table/examples/table-movable-rows.tsx (1)

47-52: ⚠️ Potential issue | 🟡 Minor

toIndex lookup still uses reference equality.

Line 47 uses indexOf(toRow) which relies on reference equality. If Tabulator clones the row data, this will return -1 and break the reorder. The fromIndex lookup was fixed to use findIndex, but toIndex was not.

Also, the as Bird cast on line 52 is unnecessary since fromRow is already typed as Bird via the generic RowReorderEvent<Bird>.

🛠️ Proposed fix
-        let toIndex = items.indexOf(toRow);
+        let toIndex = items.findIndex((bird) => bird.name === toRow.name);
         if (!above) {
             toIndex += 1;
         }

-        items.splice(toIndex, 0, fromRow as Bird);
+        items.splice(toIndex, 0, fromRow);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 47 - 52,
The toIndex lookup currently uses items.indexOf(toRow) which relies on reference
equality and can return -1 if Tabulator clones row objects; change it to use the
same predicate approach as the fromIndex fix (use items.findIndex(item =>
item.id === toRow.id or the same unique key used for rows) to locate toRow) and
adjust the insertion accordingly; also remove the unnecessary "as Bird" cast on
fromRow since fromRow is already typed via RowReorderEvent<Bird>.
src/components/table/table.tsx (1)

608-616: ⚠️ Potential issue | 🟠 Major

rowDragManager can become stale when mode or pageSize changes.

The initRowDragManager() method creates the manager unconditionally when movableRows is true, but the documentation states the feature is "Only available in local mode without pagination." However, the watchers for mode (nonexistent) and pageSize (lines 316-319) don't refresh the row drag state. If mode changes from local to remote, or pageSize is set after initialization, rowDragManager remains active in an unsupported state.

🛠️ Proposed fix
+    `@Watch`('mode')
+    protected modeChanged() {
+        this.refreshRowDragManager();
+        this.init();
+    }
+
     `@Watch`('pageSize')
     protected pageSizeChanged() {
         this.updateMaxPage();
+        this.refreshRowDragManager();
+        this.init();
     }

     `@Watch`('movableRows')
     protected updateMovableRows() {
-        this.rowDragManager = null;
-        this.initRowDragManager();
+        this.refreshRowDragManager();
         this.init();
     }

+    private refreshRowDragManager() {
+        this.rowDragManager = null;
+        this.initRowDragManager();
+    }
+
     private initRowDragManager() {
-        if (this.movableRows) {
+        if (this.movableRows && this.mode === 'local' && !this.pageSize) {
             this.rowDragManager = new RowDragManager(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 608 - 616, initRowDragManager
currently instantiates RowDragManager whenever movableRows is true but doesn't
tear it down when table configuration changes (mode or pageSize), causing
rowDragManager to remain active in unsupported states; update initRowDragManager
and add/modify the watchers for mode and pageSize so they call a single
reconcile function (e.g., ensureRowDragManager or refreshRowDragManager) that:
checks this.movableRows, this.mode === 'local' and no pagination (pageSize
unset/0), creates new RowDragManager(this.pool, this.reorder, this.language)
only when allowed, and otherwise disposes/nulls the existing this.rowDragManager
(calling any teardown on RowDragManager). Also ensure any existing creation
first disposes the old instance to avoid leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Line 31: Remove the unused private field sortableCols by deleting the
declaration "private sortableCols: string[] = ['name'];" from the component;
ensure no other code references it (search for sortableCols) and if there are no
usages remove any related type imports or comments, leaving the component
class/functional code unchanged.

In `@src/components/table/row-drag-manager.ts`:
- Around line 68-78: The drag-handle formatter (getDragHandleFormatter) acquires
elements from this.pool via pool.get(LIMEL_DRAG_HANDLE) but never releases them
per-row, causing the pool's "used" set to grow during virtualization; update
getDragHandleFormatter to attach a teardown that calls the pool release for the
returned element (e.g., call this.pool.release(element) or the pool's
equivalent) when the DOM node is removed or the row is destroyed—implement this
by adding a disconnected cleanup on the returned element (or a
MutationObserver/WeakRef cleanup tied to the element) so each acquired element
is released back into the pool when its row is virtualized out.

---

Outside diff comments:
In `@src/components/table/partial-styles/_row-selection.scss`:
- Around line 13-18: The header select-all checkbox (.select-all) doesn't
account for the movable-rows offset, so when .has-movable-rows is present it
overlaps the drag-handle; update the CSS to shift .select-all when
.has-movable-rows (or when inside .limel-table--row-selector with
.has-movable-rows) by adding a rule that increases left by
var(--limel-table-drag-handle-width) (e.g., left: calc(0.375rem +
var(--limel-table-drag-handle-width))) so the checkbox aligns with the shifted
.limel-table--row-selector column.

---

Duplicate comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 47-52: The toIndex lookup currently uses items.indexOf(toRow)
which relies on reference equality and can return -1 if Tabulator clones row
objects; change it to use the same predicate approach as the fromIndex fix (use
items.findIndex(item => item.id === toRow.id or the same unique key used for
rows) to locate toRow) and adjust the insertion accordingly; also remove the
unnecessary "as Bird" cast on fromRow since fromRow is already typed via
RowReorderEvent<Bird>.

In `@src/components/table/row-drag-manager.ts`:
- Around line 63-66: The property handleCellClick is defined as an arrow
function and never reassigned; mark it readonly to reflect immutability and
prevent accidental reassignment. Update the declaration of handleCellClick in
the RowDragManager (or the class containing it) to include the readonly modifier
so the signature becomes a readonly property assigned to the arrow function,
keeping the existing implementation (ev.stopPropagation(); ev.preventDefault())
unchanged.

In `@src/components/table/table.tsx`:
- Around line 608-616: initRowDragManager currently instantiates RowDragManager
whenever movableRows is true but doesn't tear it down when table configuration
changes (mode or pageSize), causing rowDragManager to remain active in
unsupported states; update initRowDragManager and add/modify the watchers for
mode and pageSize so they call a single reconcile function (e.g.,
ensureRowDragManager or refreshRowDragManager) that: checks this.movableRows,
this.mode === 'local' and no pagination (pageSize unset/0), creates new
RowDragManager(this.pool, this.reorder, this.language) only when allowed, and
otherwise disposes/nulls the existing this.rowDragManager (calling any teardown
on RowDragManager). Also ensure any existing creation first disposes the old
instance to avoid leaks.
🪄 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: b463b9d1-ce3c-4313-a4d6-84053ca13b38

📥 Commits

Reviewing files that changed from the base of the PR and between 5466fff and cf97621.

⛔ Files ignored due to path filters (2)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/partial-styles/_row-selection.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.scss
  • src/components/table/table.tsx
  • src/components/table/table.types.ts

Comment thread src/components/table/examples/table-movable-rows.tsx Outdated
@albinhallen albinhallen force-pushed the feat/table-rows-drag-n-drop branch from cf97621 to 21b2291 Compare April 8, 2026 09:20
Copy link
Copy Markdown
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/table/partial-styles/_row-selection.scss (1)

13-18: ⚠️ Potential issue | 🟡 Minor

Add .select-all offset for movable rows.

The .select-all checkbox doesn't adjust its position when has-movable-rows is active. It remains at left: 0.375rem while the row selector column shifts right to accommodate the drag-handle, causing misalignment.

🛠️ Proposed fix
 .select-all {
     position: absolute;
     z-index: var(--limel-table-row-selector-z-index);
     left: 0.375rem;
     top: 0.1875rem;
+
+    .has-movable-rows & {
+        left: calc(var(--limel-table-drag-handle-width) + 0.375rem);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/partial-styles/_row-selection.scss` around lines 13 -
18, The .select-all checkbox is not shifted when movable rows are enabled; add a
sibling/ancestor selector to adjust its left offset when has-movable-rows is
present (e.g., add a rule like ".has-movable-rows .select-all" that increases
left by the drag-handle width using calc or a CSS variable) so the checkbox
aligns with the shifted row-selector column; update _row-selection.scss and
prefer using an existing CSS var for the drag-handle offset if available instead
of a hardcoded value.
♻️ Duplicate comments (5)
src/components/table/row-drag-manager.ts (2)

63-66: 🧹 Nitpick | 🔵 Trivial

Mark handleCellClick as readonly.

Per static analysis, this arrow function property is never reassigned.

♻️ Proposed fix
-    private handleCellClick = (ev: Event): void => {
+    private readonly handleCellClick = (ev: Event): void => {
         ev.stopPropagation();
         ev.preventDefault();
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/row-drag-manager.ts` around lines 63 - 66, The
handleCellClick class property is never reassigned so mark it readonly to
satisfy static analysis: update the property declaration for handleCellClick to
include the readonly modifier (i.e., readonly handleCellClick = (ev: Event):
void => { ... }) so the arrow-function member cannot be reassigned; leave the
implementation (ev.stopPropagation(); ev.preventDefault();) unchanged.

68-78: 🧹 Nitpick | 🔵 Trivial

Pool elements accumulate without release during row virtualization.

The formatter acquires elements via pool.get() but never releases them. When Tabulator virtualizes rows during scrolling, these limel-drag-handle elements remain marked as "used" in the pool's WeakMap. While pool.releaseAll() is called during table reinitialization, extended scrolling through large datasets accumulates unreleased elements.

This matches the existing pattern in table-selection.ts, but consider adding per-row release logic for tables with many rows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/row-drag-manager.ts` around lines 68 - 78, The
drag-handle formatter (getDragHandleFormatter) currently calls
pool.get(LIMEL_DRAG_HANDLE) and never releases those elements, causing
accumulation during row virtualization; fix this by ensuring each element
returned by getDragHandleFormatter is released back to the pool when its row is
removed/unrendered—e.g., attach the element to the row or cell (so you can
identify it later) and call pool.release(element) from the Tabulator row
unrender/unloaded handler or from a corresponding row cleanup/unformatter, or
register a per-element cleanup when calling setElementProperties, ensuring you
reference LIMEL_DRAG_HANDLE, pool.get, and pool.release (or pool.releaseAll if
adapting existing pattern) and add the release call in the row removal/unrender
path.
src/components/table/examples/table-movable-rows.tsx (2)

31-36: 🧹 Nitpick | 🔵 Trivial

Mark columns and handleReorder as readonly.

Per static analysis, these members are never reassigned.

♻️ Proposed fix
-    private columns: Array<Column<Bird>> = [
+    private readonly columns: Array<Column<Bird>> = [
         { title: 'Name', field: 'name' },
         { title: 'Binominal name', field: 'binominalName' },
         { title: 'Nest type', field: 'nest', formatter: capitalize },
         { title: 'Eggs per clutch', field: 'eggs', horizontalAlign: 'right' },
     ];

-    private handleReorder = (event: CustomEvent<RowReorderEvent<Bird>>) => {
+    private readonly handleReorder = (event: CustomEvent<RowReorderEvent<Bird>>) => {

Also applies to: 38-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 31 - 36,
Mark the class members that are never reassigned as readonly: change the
property declaration for columns (Array<Column<Bird>>) to readonly and add
readonly to the handleReorder method signature (or mark it as a readonly arrow
function property if it’s defined that way) so static analysis stops flagging
reassignments; update the declarations for columns and handleReorder in the same
class where they currently appear to include the readonly modifier.

45-50: ⚠️ Potential issue | 🟡 Minor

indexOf(toRow) relies on reference equality.

Line 45 uses indexOf(toRow) which relies on reference equality. If Tabulator or the event mechanism clones row data objects, this returns -1 and causes incorrect insertion. Line 42 was fixed to use findIndex, but line 45 was not.

Additionally, the as Bird cast on line 50 is unnecessary since fromRow is already typed as Bird.

🛠️ Proposed fix
         const fromIndex = items.findIndex((bird) => bird.name === fromRow.name);
         items.splice(fromIndex, 1);

-        let toIndex = items.indexOf(toRow);
+        let toIndex = items.findIndex((bird) => bird.name === toRow.name);
         if (!above) {
             toIndex += 1;
         }

-        items.splice(toIndex, 0, fromRow as Bird);
+        items.splice(toIndex, 0, fromRow);
         this.tableData = items;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 45 - 50,
Replace the reference-equality lookup using items.indexOf(toRow) with a
predicate-based search like items.findIndex(item => item.id === toRow.id) (or
another unique key on Bird) so cloned row objects are matched correctly, and
remove the unnecessary "as Bird" cast when inserting fromRow into items (use
fromRow directly); target the variables/items and functions: items, toRow,
fromRow, and the splice call.
src/components/table/table.tsx (1)

608-616: ⚠️ Potential issue | 🟠 Major

initRowDragManager doesn't enforce documented restrictions.

The movableRows prop documentation states it's "Only available in local mode without pagination," but initRowDragManager() creates the manager regardless of mode or pageSize. This allows drag handles to appear in unsupported configurations.

🛠️ Proposed fix
     private initRowDragManager() {
-        if (this.movableRows) {
+        const isLocalMode = this.mode === 'local';
+        const hasPagination = !!this.pageSize;
+
+        if (this.movableRows && isLocalMode && !hasPagination) {
             this.rowDragManager = new RowDragManager(
                 this.pool,
                 this.reorder,
                 this.language
             );
+        } else if (this.movableRows) {
+            console.warn(
+                'limel-table: movableRows is only supported in local mode without pagination'
+            );
         }
     }

Additionally, per a past review comment, the mode and pageSize watchers should also call refreshRowDragManager() to handle dynamic changes to these props.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 608 - 616, initRowDragManager
currently only checks movableRows; change it to only create RowDragManager when
this.movableRows AND this.mode === 'local' AND pagination is off (e.g.
!this.pageSize or pageSize === 0 per your pagination semantics), and ensure it
cleans up/destroys any existing this.rowDragManager when the conditions are not
met; also update the watchers for mode and pageSize to call
refreshRowDragManager() so dynamic prop changes create or tear down the manager
via the existing refreshRowDragManager() helper (reference initRowDragManager,
refreshRowDragManager, RowDragManager, movableRows, mode, pageSize).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/table.tsx`:
- Around line 444-449: When movableRows toggles, explicitly release the pooled
drag-handle elements before discarding the old RowDragManager: in
updateMovableRows(), if a rowDragManager or its pool exists call its
pool.releaseAll() (or this.pool.releaseAll() if pool is component-level) first,
then destroy/null the rowDragManager and call init(); this ensures the
formatter-acquired elements are released rather than orphaned when you later
recreate the manager in init().

---

Outside diff comments:
In `@src/components/table/partial-styles/_row-selection.scss`:
- Around line 13-18: The .select-all checkbox is not shifted when movable rows
are enabled; add a sibling/ancestor selector to adjust its left offset when
has-movable-rows is present (e.g., add a rule like ".has-movable-rows
.select-all" that increases left by the drag-handle width using calc or a CSS
variable) so the checkbox aligns with the shifted row-selector column; update
_row-selection.scss and prefer using an existing CSS var for the drag-handle
offset if available instead of a hardcoded value.

---

Duplicate comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 31-36: Mark the class members that are never reassigned as
readonly: change the property declaration for columns (Array<Column<Bird>>) to
readonly and add readonly to the handleReorder method signature (or mark it as a
readonly arrow function property if it’s defined that way) so static analysis
stops flagging reassignments; update the declarations for columns and
handleReorder in the same class where they currently appear to include the
readonly modifier.
- Around line 45-50: Replace the reference-equality lookup using
items.indexOf(toRow) with a predicate-based search like items.findIndex(item =>
item.id === toRow.id) (or another unique key on Bird) so cloned row objects are
matched correctly, and remove the unnecessary "as Bird" cast when inserting
fromRow into items (use fromRow directly); target the variables/items and
functions: items, toRow, fromRow, and the splice call.

In `@src/components/table/row-drag-manager.ts`:
- Around line 63-66: The handleCellClick class property is never reassigned so
mark it readonly to satisfy static analysis: update the property declaration for
handleCellClick to include the readonly modifier (i.e., readonly handleCellClick
= (ev: Event): void => { ... }) so the arrow-function member cannot be
reassigned; leave the implementation (ev.stopPropagation();
ev.preventDefault();) unchanged.
- Around line 68-78: The drag-handle formatter (getDragHandleFormatter)
currently calls pool.get(LIMEL_DRAG_HANDLE) and never releases those elements,
causing accumulation during row virtualization; fix this by ensuring each
element returned by getDragHandleFormatter is released back to the pool when its
row is removed/unrendered—e.g., attach the element to the row or cell (so you
can identify it later) and call pool.release(element) from the Tabulator row
unrender/unloaded handler or from a corresponding row cleanup/unformatter, or
register a per-element cleanup when calling setElementProperties, ensuring you
reference LIMEL_DRAG_HANDLE, pool.get, and pool.release (or pool.releaseAll if
adapting existing pattern) and add the release call in the row removal/unrender
path.

In `@src/components/table/table.tsx`:
- Around line 608-616: initRowDragManager currently only checks movableRows;
change it to only create RowDragManager when this.movableRows AND this.mode ===
'local' AND pagination is off (e.g. !this.pageSize or pageSize === 0 per your
pagination semantics), and ensure it cleans up/destroys any existing
this.rowDragManager when the conditions are not met; also update the watchers
for mode and pageSize to call refreshRowDragManager() so dynamic prop changes
create or tear down the manager via the existing refreshRowDragManager() helper
(reference initRowDragManager, refreshRowDragManager, RowDragManager,
movableRows, mode, pageSize).
🪄 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: 6e174135-c334-425c-8e73-30159a6da658

📥 Commits

Reviewing files that changed from the base of the PR and between cf97621 and 21b2291.

⛔ Files ignored due to path filters (2)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/partial-styles/_row-selection.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.scss
  • src/components/table/table.tsx
  • src/components/table/table.types.ts

Comment on lines +444 to +449
@Watch('movableRows')
protected updateMovableRows() {
this.rowDragManager = null;
this.initRowDragManager();
this.init();
}
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

Pool elements not released when movableRows toggles.

When movableRows changes, the watcher nulls rowDragManager and calls init(). While init() calls pool.releaseAll() before destroying Tabulator (line 530), the drag-handle elements acquired by the old RowDragManager formatter remain orphaned if the watcher fires before init() releases them.

Consider explicitly releasing pool elements before nulling the manager:

🛠️ Suggested fix
     `@Watch`('movableRows')
     protected updateMovableRows() {
+        this.pool.releaseAll();
         this.rowDragManager = null;
         this.initRowDragManager();
         this.init();
     }
📝 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
@Watch('movableRows')
protected updateMovableRows() {
this.rowDragManager = null;
this.initRowDragManager();
this.init();
}
`@Watch`('movableRows')
protected updateMovableRows() {
this.pool.releaseAll();
this.rowDragManager = null;
this.initRowDragManager();
this.init();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/table.tsx` around lines 444 - 449, When movableRows
toggles, explicitly release the pooled drag-handle elements before discarding
the old RowDragManager: in updateMovableRows(), if a rowDragManager or its pool
exists call its pool.releaseAll() (or this.pool.releaseAll() if pool is
component-level) first, then destroy/null the rowDragManager and call init();
this ensures the formatter-acquired elements are released rather than orphaned
when you later recreate the manager in init().

@albinhallen albinhallen force-pushed the feat/table-rows-drag-n-drop branch from 21b2291 to cd7b28a Compare April 9, 2026 09:40
@albinhallen
Copy link
Copy Markdown
Author

@Kiarokh the issue with not being able to hover/move in the limel-drag-handle column is fixed now. The only potential issue I see now is that it alternates between cursor/pointer, just as in the tabulator example: https://tabulator.info/docs/6.4/move

The scroll issue on the docs page seems to have been resolved, I don't see it anymore

Copy link
Copy Markdown
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/table/partial-styles/_row-selection.scss (1)

13-18: ⚠️ Potential issue | 🟡 Minor

Missing offset for .select-all when movable rows are enabled.

The .limel-table--row-selector correctly shifts right by --limel-table-drag-handle-width when nested under .has-movable-rows (lines 23-25), but .select-all retains its fixed left: 0.375rem position. This will cause the header "select all" checkbox to overlap the drag-handle column when both selectable and movableRows are enabled.

🛡️ Proposed fix
 .select-all {
     position: absolute;
     z-index: var(--limel-table-row-selector-z-index);
     left: 0.375rem;
     top: 0.1875rem;
+
+    .has-movable-rows & {
+        left: calc(var(--limel-table-drag-handle-width) + 0.375rem);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/partial-styles/_row-selection.scss` around lines 13 -
18, The .select-all rule currently uses a fixed left offset causing overlap when
movable rows are enabled; update the stylesheet so that when .select-all is
inside .has-movable-rows (or sibling to .limel-table--row-selector under
.has-movable-rows) its left position is shifted by the drag handle width
variable (--limel-table-drag-handle-width) e.g. compute left using
calc(--limel-table-drag-handle-width + 0.375rem) to match the existing
.limel-table--row-selector adjustment and prevent overlap with the drag-handle
column.
♻️ Duplicate comments (3)
src/components/table/row-drag-manager.ts (1)

63-66: 🧹 Nitpick | 🔵 Trivial

Mark handleCellClick as readonly.

Per static analysis feedback, this arrow function property is never reassigned.

♻️ Proposed fix
-    private handleCellClick = (ev: Event): void => {
+    private readonly handleCellClick = (ev: Event): void => {
         ev.stopPropagation();
         ev.preventDefault();
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/row-drag-manager.ts` around lines 63 - 66, The arrow
function property handleCellClick should be marked readonly because it is never
reassigned; update its declaration in the RowDragManager class (the
handleCellClick property) to include the readonly modifier so it becomes
readonly handleCellClick = (ev: Event): void => { ev.stopPropagation();
ev.preventDefault(); } ensuring the signature and behavior remain unchanged.
src/components/table/examples/table-movable-rows.tsx (2)

31-36: 🧹 Nitpick | 🔵 Trivial

Mark columns as readonly.

Per static analysis feedback, columns is never reassigned and should be marked as readonly.

♻️ Proposed fix
-    private columns: Array<Column<Bird>> = [
+    private readonly columns: Array<Column<Bird>> = [
         { title: 'Name', field: 'name' },
         { title: 'Binominal name', field: 'binominalName' },
         { title: 'Nest type', field: 'nest', formatter: capitalize },
         { title: 'Eggs per clutch', field: 'eggs', horizontalAlign: 'right' },
     ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 31 - 36,
The private property columns (type Array<Column<Bird>>) is never reassigned—mark
it readonly by changing its declaration to include the readonly modifier (e.g.,
private readonly columns: Array<Column<Bird>> = [...] or private readonly
columns: ReadonlyArray<Column<Bird>> = [...]) so the property and its type
reflect immutability; update the declaration where columns is defined in the
component.

42-50: ⚠️ Potential issue | 🟡 Minor

Inconsistent lookup strategy: toRow still uses reference equality.

Line 42 correctly uses findIndex with a property comparison for fromRow, but line 45 uses indexOf(toRow) which relies on reference equality. If Tabulator clones row data objects, this will return -1 and cause incorrect reordering. Apply the same findIndex pattern for consistency.

Additionally, the as Bird cast on line 50 is unnecessary since fromRow is already typed as Bird.

🛡️ Proposed fix
         const fromIndex = items.findIndex((bird) => bird.name === fromRow.name);
         items.splice(fromIndex, 1);

-        let toIndex = items.indexOf(toRow);
+        let toIndex = items.findIndex((bird) => bird.name === toRow.name);
         if (!above) {
             toIndex += 1;
         }

-        items.splice(toIndex, 0, fromRow as Bird);
+        items.splice(toIndex, 0, fromRow);
         this.tableData = items;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/examples/table-movable-rows.tsx` around lines 42 - 50,
The move logic mixes reference and property-based lookups: replace
items.indexOf(toRow) with a property-based find (e.g., items.findIndex((b) =>
b.name === toRow.name)) so toIndex is computed reliably even if row objects are
cloned, and then use items.splice(fromIndex,1) / items.splice(toIndex,0,fromRow)
as before; also remove the unnecessary "as Bird" cast on fromRow since fromRow
is already typed as Bird.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/table/row-drag-manager.ts`:
- Around line 68-78: The formatter getDragHandleFormatter currently acquires
elements from this.pool via pool.get(LIMEL_DRAG_HANDLE) but never returns them
to the pool, which can leak "used" pool entries for heavily virtualized tables;
modify getDragHandleFormatter (and the element returned) to register a
teardown/cleanup that calls this.pool.release(element) when the row is
unrendered/detached (follow the same per-row release pattern used in
table-selection.ts), so each acquired LIMEL_DRAG_HANDLE is released individually
instead of only relying on pool.releaseAll() during reinitialization.

---

Outside diff comments:
In `@src/components/table/partial-styles/_row-selection.scss`:
- Around line 13-18: The .select-all rule currently uses a fixed left offset
causing overlap when movable rows are enabled; update the stylesheet so that
when .select-all is inside .has-movable-rows (or sibling to
.limel-table--row-selector under .has-movable-rows) its left position is shifted
by the drag handle width variable (--limel-table-drag-handle-width) e.g. compute
left using calc(--limel-table-drag-handle-width + 0.375rem) to match the
existing .limel-table--row-selector adjustment and prevent overlap with the
drag-handle column.

---

Duplicate comments:
In `@src/components/table/examples/table-movable-rows.tsx`:
- Around line 31-36: The private property columns (type Array<Column<Bird>>) is
never reassigned—mark it readonly by changing its declaration to include the
readonly modifier (e.g., private readonly columns: Array<Column<Bird>> = [...]
or private readonly columns: ReadonlyArray<Column<Bird>> = [...]) so the
property and its type reflect immutability; update the declaration where columns
is defined in the component.
- Around line 42-50: The move logic mixes reference and property-based lookups:
replace items.indexOf(toRow) with a property-based find (e.g.,
items.findIndex((b) => b.name === toRow.name)) so toIndex is computed reliably
even if row objects are cloned, and then use items.splice(fromIndex,1) /
items.splice(toIndex,0,fromRow) as before; also remove the unnecessary "as Bird"
cast on fromRow since fromRow is already typed as Bird.

In `@src/components/table/row-drag-manager.ts`:
- Around line 63-66: The arrow function property handleCellClick should be
marked readonly because it is never reassigned; update its declaration in the
RowDragManager class (the handleCellClick property) to include the readonly
modifier so it becomes readonly handleCellClick = (ev: Event): void => {
ev.stopPropagation(); ev.preventDefault(); } ensuring the signature and behavior
remain unchanged.
🪄 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: ca492c8c-6b52-46ec-b9e5-990363d1be18

📥 Commits

Reviewing files that changed from the base of the PR and between 21b2291 and cd7b28a.

⛔ Files ignored due to path filters (2)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • src/components/table/examples/table-movable-rows.tsx
  • src/components/table/partial-styles/_row-drag-handle.scss
  • src/components/table/partial-styles/_row-selection.scss
  • src/components/table/row-drag-manager.spec.ts
  • src/components/table/row-drag-manager.ts
  • src/components/table/table.scss
  • src/components/table/table.tsx
  • src/components/table/table.types.ts

Comment on lines +68 to +78
private getDragHandleFormatter() {
return () => {
const element = this.pool.get(LIMEL_DRAG_HANDLE);
setElementProperties(element, {
dragDirection: 'vertical',
language: this.language,
});

return element;
};
}
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

Consider element release for heavily virtualized tables.

The formatter acquires elements via pool.get() but doesn't release them when rows are virtualized out of view. While pool.releaseAll() is called during table reinitialization, extended scrolling through tables with many rows will accumulate limel-drag-handle elements in the pool's "used" set.

This matches the existing pattern used by other formatters (e.g., table-selection.ts), so it's consistent with the codebase. For large tables with heavy virtualization, you may want to consider adding per-row release logic in the future.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/table/row-drag-manager.ts` around lines 68 - 78, The formatter
getDragHandleFormatter currently acquires elements from this.pool via
pool.get(LIMEL_DRAG_HANDLE) but never returns them to the pool, which can leak
"used" pool entries for heavily virtualized tables; modify
getDragHandleFormatter (and the element returned) to register a teardown/cleanup
that calls this.pool.release(element) when the row is unrendered/detached
(follow the same per-row release pattern used in table-selection.ts), so each
acquired LIMEL_DRAG_HANDLE is released individually instead of only relying on
pool.releaseAll() during reinitialization.

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