Repository: equinor/design-system
Pull request #3782 (2025-04-25 10:51:02)

fix(eds-core-react, eds-data-grid-react): :bug: prevent triggering sorting on resize
update resizer component to prevent mouse events from triggering sorting


Patch:
diff --git a/packages/eds-core-react/src/components/Table/HeaderCell/HeaderCell.tsx b/packages/eds-core-react/src/components/Table/HeaderCell/HeaderCell.tsx
index bb9fb31a..e6c3804a 100644
--- a/packages/eds-core-react/src/components/Table/HeaderCell/HeaderCell.tsx
+++ b/packages/eds-core-react/src/components/Table/HeaderCell/HeaderCell.tsx
@@ -69,6 +69,7 @@ const StyledTableCell = styled.th((props: BaseProps) => {
 const CellInner = styled.div`
   display: flex;
   align-items: center;
+  height: 100%;
 `
 
 type CellProps = {
diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx b/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx
index e3e459ca..acc74a0c 100644
--- a/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx
+++ b/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx
@@ -440,13 +440,15 @@ ColumnGrouping.args = {
   scrollbarHorizontal: true,
 }
 
-export const Sortable: StoryFn<EdsDataGridProps<Photo>> = (args) => {
+export const AllTheThings: StoryFn<EdsDataGridProps<Photo>> = (args) => {
   return <EdsDataGrid {...args} />
 }
 
-Sortable.args = {
+AllTheThings.args = {
   enableSorting: true,
+  columnResizeMode: 'onChange',
   columns: groupedColumns,
+  enableColumnFiltering: true,
   sortingState: [
     {
       desc: true,
diff --git a/packages/eds-data-grid-react/src/components/FilterWrapper.tsx b/packages/eds-data-grid-react/src/components/FilterWrapper.tsx
index b4aa561b..1661ba5d 100644
--- a/packages/eds-data-grid-react/src/components/FilterWrapper.tsx
+++ b/packages/eds-data-grid-react/src/components/FilterWrapper.tsx
@@ -50,7 +50,7 @@ export function FilterWrapper<T = unknown>({
   )
 
   return (
-    <>
+    <span>
       <Button
         aria-haspopup
         aria-expanded={open}
@@ -83,6 +83,6 @@ export function FilterWrapper<T = unknown>({
           )}
         </Popover.Content>
       </Popover>
-    </>
+    </span>
   )
 }
diff --git a/packages/eds-data-grid-react/src/components/Resizer.tsx b/packages/eds-data-grid-react/src/components/Resizer.tsx
index 706b866e..f7fcbcb7 100644
--- a/packages/eds-data-grid-react/src/components/Resizer.tsx
+++ b/packages/eds-data-grid-react/src/components/Resizer.tsx
@@ -30,4 +30,7 @@ export const Resizer = styled.div<ResizeProps>`
   touch-action: none;
   display: flex;
   justify-content: flex-end;
+
+  /* Make resizer handle have higher specificity */
+  z-index: 1;
 `
diff --git a/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx b/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx
index aa3b1a4e..fcabcf05 100644
--- a/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx
+++ b/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx
@@ -11,6 +11,30 @@ import { FilterWrapper } from './FilterWrapper'
 import { SortIndicator } from './SortIndicator'
 import { ResizeInner, Resizer } from './Resizer'
 import { TableCell } from './TableCell'
+import styled from 'styled-components'
+
+const SortButton = styled.button`
+  cursor: pointer;
+  height: 100%;
+  width: calc(100% - 5px); /* Leave space for resizer */
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: transparent;
+  border: none;
+  padding-left: var(--eds_table__cell__padding_x, 16px);
+  padding-right: var(--eds_table__cell__padding_x, 16px);
+  margin: 0;
+  outline: none;
+  color: inherit;
+  text-align: left;
+  font: inherit;
+`
+
+const TableHeaderCellLabel = styled.div`
+  display: flex;
+  flex-direction: column;
+`
 
 type Props<T> = {
   header: Header<T, unknown>
@@ -34,6 +58,9 @@ export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
   const ctx = useTableContext()
   const table = ctx.table
   const pinned = header.column.getIsPinned()
+  const canSort = header.column.getCanSort()
+  const canFilter = header.column.getCanFilter()
+
   const offset = useMemo<number>(() => {
     if (!pinned) {
       return null
@@ -42,6 +69,17 @@ export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
       ? header.getStart()
       : table.getTotalSize() - header.getStart() - header.getSize()
   }, [pinned, header, table])
+
+  const tableCellPadding = useMemo(() => {
+    if (canSort && canFilter) {
+      return '0 var(--eds_table__cell__padding_x, 16px) 0 0'
+    }
+    if (canSort) {
+      return '0'
+    }
+    return '0 var(--eds_table__cell__padding_x, 16px) 0 var(--eds_table__cell__padding_x, 16px)'
+  }, [canSort, canFilter])
+
   return header.isPlaceholder ? (
     <TableCell
       $sticky={ctx.stickyHeader}
@@ -61,36 +99,39 @@ export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
       className={ctx.headerClass ? ctx.headerClass(header.column) : ''}
       aria-sort={getSortLabel(header.column.getIsSorted())}
       key={header.id}
-      onClick={header.column.getToggleSortingHandler()}
       colSpan={header.colSpan}
       style={{
         width: header.getSize(),
         verticalAlign: ctx.enableColumnFiltering ? 'top' : 'middle',
         ...(ctx.headerStyle ? ctx.headerStyle(header.column) : {}),
+        padding: tableCellPadding,
       }}
     >
-      <>
-        <div style={{ display: 'flex', flexDirection: 'column' }}>
-          <span className="table-header-cell-label">
+      {canSort ? (
+        <SortButton
+          tabIndex={-1}
+          onClick={header.column.getToggleSortingHandler()}
+          data-testid={`sort-button-${header.id}`}
+        >
+          <TableHeaderCellLabel className="table-header-cell-label">
             {flexRender(header.column.columnDef.header, header.getContext())}
-          </span>
-        </div>
-        {!header.column.columnDef.meta?.customFilterInput && (
-          <SortIndicator column={header.column} />
-        )}
+          </TableHeaderCellLabel>
+          {!header.column.columnDef.meta?.customFilterInput && (
+            <SortIndicator column={header.column} />
+          )}
+        </SortButton>
+      ) : (
+        <TableHeaderCellLabel className="table-header-cell-label">
+          {flexRender(header.column.columnDef.header, header.getContext())}
+        </TableHeaderCellLabel>
+      )}
+
+      {canFilter && !header.column.columnDef.meta?.customFilterInput ? (
+        <FilterWrapper column={header.column} />
+      ) : null}
 
-        {header.column.getCanFilter() &&
-        !header.column.columnDef.meta?.customFilterInput ? (
-          // Supressing this warning - div is not interactive, but prevents propagation of events to avoid unintended sorting
-          // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
-          <div onClick={(e) => e.stopPropagation()}>
-            <FilterWrapper column={header.column} />
-          </div>
-        ) : null}
-      </>
       {columnResizeMode && (
         <Resizer
-          onClick={(e) => e.stopPropagation()}
           onMouseDown={header.getResizeHandler()}
           onTouchStart={header.getResizeHandler()}
           $isResizing={header.column.getIsResizing()}


Test patch:
diff --git a/packages/eds-core-react/src/components/Table/__snapshots__/Table.test.tsx.snap b/packages/eds-core-react/src/components/Table/__snapshots__/Table.test.tsx.snap
index ccf9b5c9..dcdb9c7c 100644
--- a/packages/eds-core-react/src/components/Table/__snapshots__/Table.test.tsx.snap
+++ b/packages/eds-core-react/src/components/Table/__snapshots__/Table.test.tsx.snap
@@ -29,6 +29,7 @@ exports[`Table Matches snapshot 1`] = `
 .c4 {
   display: flex;
   align-items: center;
+  height: 100%;
 }
 
 .c1 {
diff --git a/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx b/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx
index 41a17b68..d2c2982b 100644
--- a/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx
+++ b/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx
@@ -171,18 +171,25 @@ describe('EdsDataGrid', () => {
 
   describe('Sorting', () => {
     it('should have built-in sorting if enabled', async () => {
+      // Setup
       render(<EdsDataGrid enableSorting={true} columns={columns} rows={data} />)
-      expect(
-        screen.getAllByRole('columnheader')[0].getAttribute('aria-sort'),
-      ).toBe('none')
-      await userEvent.click(screen.getAllByRole('columnheader')[0])
-      expect(
-        screen.getAllByRole('columnheader')[0].getAttribute('aria-sort'),
-      ).toBe('ascending')
-      await userEvent.click(screen.getAllByRole('columnheader')[0])
-      expect(
-        screen.getAllByRole('columnheader')[0].getAttribute('aria-sort'),
-      ).toBe('descending')
+      const header = screen.getAllByRole('columnheader')[0]
+      const sortButton = within(header).getByTestId('sort-button-cargoId')
+
+      // Initial state verification
+      expect(header.getAttribute('aria-sort')).toBe('none')
+
+      // Test ascending sort
+      await userEvent.click(sortButton)
+      expect(header.getAttribute('aria-sort')).toBe('ascending')
+
+      // Test descending sort
+      await userEvent.click(sortButton)
+      expect(header.getAttribute('aria-sort')).toBe('descending')
+
+      // Test reset to default (optional additional verification)
+      await userEvent.click(sortButton)
+      expect(header.getAttribute('aria-sort')).toBe('none')
     })
 
     it('should not have sorting if not set', async () => {