Skip to content

feat: draggable column#6047

Merged
connoratrug merged 6 commits intomasterfrom
feat/draggable-column
Mar 13, 2026
Merged

feat: draggable column#6047
connoratrug merged 6 commits intomasterfrom
feat/draggable-column

Conversation

@connoratrug
Copy link
Contributor

What are the main changes you did

  • add option to drag to resize table column

How to test

  • use feature

Checklist

  • updated docs in case of new feature
  • added/updated tests
  • added/updated testplan to include a test for this fix, including ref to bug using # notation

connoratrug and others added 5 commits March 12, 2026 14:13
Hover over the column edge and drag to resize column

- add composable to do size calculations
- use request animation frame to deduplicate drag events and avoid direct repaints ( performance )
- pass the container to allow calculating drag indicator line for table with dynamic content and horizontal scroll
- move column sort down to label to avoid confusion with drag
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds draggable column resizing to the EMX2 table component, allowing users to drag column borders to adjust widths. It introduces a new useColumnResize composable that manages resize state and a visual guide line, and integrates it into the table component.

Changes:

  • New useColumnResize composable that handles mouse-based column resizing with requestAnimationFrame throttling
  • Updated TableEMX2.vue to use pixel-based column widths with drag handles and a visual resize guide
  • Moved sort click handler in TableHeaderAction.vue to avoid conflict with the new resize handle

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
apps/tailwind-components/app/composables/useColumnResize.ts New composable managing column resize state, mouse events, and guide position
apps/tailwind-components/app/components/table/TableEMX2.vue Integrates column resize: adds drag handles, guide overlay, and pixel-based widths
apps/tailwind-components/app/components/table/TableHeaderAction.vue Moves sort click from outer div to inner span to avoid interfering with resize
apps/tailwind-components/tests/vitest/composables/useColumnResize.spec.ts Unit tests for the new composable
Comments suppressed due to low confidence (1)

apps/tailwind-components/app/components/table/TableEMX2.vue:101

  • The inline :style width now conflicts with the CSS classes w-60 and w-60 lg:w-full still applied via :class. Since table-fixed is used and column widths are now managed by columnWidths, these Tailwind width classes should be removed to avoid the style conflict (inline styles and CSS classes both setting width).
              :style="{ width: columnWidths[column.id] + 'px' }"
              class="text-table-row group-hover:bg-hover"
              :class="{
                'w-60 lg:w-full': columns.length <= 5,
                'w-60': columns.length > 5,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +29 to +42
function startResize(event: MouseEvent, columnId: string) {
isResizing.value = true;
resizingColumn.value = columnId;

startX.value = event.clientX;
startWidth.value = columnWidths.value[columnId] ?? 240;

guideX.value = getRelativeX(event.clientX);

document.body.style.cursor = "col-resize";

window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", stopResize);
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

pendingX is initialized to 0 and is only updated on mousemove. If the user does a mousedown then mouseup without moving the mouse, pendingX is 0, causing diff = 0 - startX which produces a large negative value. While the MIN_WIDTH guard prevents the width from going negative, the column width silently stays unchanged with no feedback, and pendingX retains stale state across resize operations. pendingX should be initialized to event.clientX in startResize to ensure correct behavior when no mousemove occurs.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +68
if (newWidth > MIN_WIDTH) {
columnWidths.value[resizingColumn.value] = newWidth;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

When newWidth <= MIN_WIDTH, the column width is left unchanged at its previous value rather than being clamped to MIN_WIDTH. This means dragging far to the left has no effect instead of shrinking the column to its minimum. Consider clamping: columnWidths.value[resizingColumn.value] = Math.max(newWidth, MIN_WIDTH).

Suggested change
if (newWidth > MIN_WIDTH) {
columnWidths.value[resizingColumn.value] = newWidth;
}
columnWidths.value[resizingColumn.value] = Math.max(newWidth, MIN_WIDTH);

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +46
<span
@click="emit('sort-requested', column.id)"
class="hover:cursor-pointer"
>{{ column.label }}</span
>
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The sort click handler was moved from the wrapping div to a span inside the button, but the button itself still has @click.prevent which only prevents default — it doesn't trigger sorting. This means clicking the button area outside the span text (e.g., padding) no longer triggers sorting, reducing the clickable area. The @click for sort should be on the button element instead, replacing the @click.prevent.

Copilot uses AI. Check for mistakes.

function getRelativeX(clientX: number) {
const rect = container.value?.getBoundingClientRect();
return rect ? clientX - rect.left : clientX;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The guideX position is calculated relative to the container's left edge but doesn't account for scrollLeft. If the table is scrolled horizontally, the guide line will appear at the wrong position. getRelativeX should add container.value.scrollLeft to the result.

Suggested change
return rect ? clientX - rect.left : clientX;
const scrollLeft = container.value?.scrollLeft ?? 0;
return rect ? clientX - rect.left + scrollLeft : clientX;

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@connoratrug connoratrug merged commit 8ea6cdb into master Mar 13, 2026
5 of 6 checks passed
@connoratrug connoratrug deleted the feat/draggable-column branch March 13, 2026 12:26
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.

3 participants