From b5aedf18d6e2cadcbf74d7be3b3229d16b06561e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 03:21:03 +0000 Subject: [PATCH 1/2] fix(components): the row's leading cell is the selection's hit area, so a near miss on the checkbox no longer opens the record (objectui#13016) On a tall row the 16px checkbox is a small target. A click beside it bubbled to the row's onRowClick and navigated to the record detail. The leading cell now toggles the row's selection on any click that is not on the checkbox itself, and the row's click filter skips it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XV4S9ReSgn5hj61n1VbimV --- .changeset/leading-cell-hit-area.md | 5 ++ ...ta-table-leadingCellHitArea-13016.test.tsx | 82 +++++++++++++++++++ .../src/renderers/complex/data-table.tsx | 15 +++- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 .changeset/leading-cell-hit-area.md create mode 100644 packages/components/src/renderers/complex/__tests__/data-table-leadingCellHitArea-13016.test.tsx diff --git a/.changeset/leading-cell-hit-area.md b/.changeset/leading-cell-hit-area.md new file mode 100644 index 0000000000..5471c604e7 --- /dev/null +++ b/.changeset/leading-cell-hit-area.md @@ -0,0 +1,5 @@ +--- +"@object-ui/components": patch +--- + +data-table: the whole leading cell of a selectable row is now the selection's hit area. A click beside the small checkbox toggles the row instead of opening the record (objectui#13016). diff --git a/packages/components/src/renderers/complex/__tests__/data-table-leadingCellHitArea-13016.test.tsx b/packages/components/src/renderers/complex/__tests__/data-table-leadingCellHitArea-13016.test.tsx new file mode 100644 index 0000000000..34cf2db5b0 --- /dev/null +++ b/packages/components/src/renderers/complex/__tests__/data-table-leadingCellHitArea-13016.test.tsx @@ -0,0 +1,82 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#13016 — the row's leading cell is the selection's hit area. On a tall row the 16px + * checkbox is a small target, and a click that missed it bubbled to the row + * and opened the record. A click anywhere in the leading cell now toggles the + * row's selection and never reaches `onRowClick`. + */ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import '../data-table'; + +const ROWS = [ + { id: 'a', name: 'Ada' }, + { id: 'b', name: 'Grace' }, +]; + +function renderTable(extra: Record = {}) { + const DataTable = ComponentRegistry.get('data-table') as any; + const onRowClick = vi.fn(); + const onSelectionChange = vi.fn(); + render( + , + ); + return { onRowClick, onSelectionChange }; +} + +const leadingCellOf = (name: string) => + screen.getByText(name).closest('tr')!.querySelector('td[data-slot="row-leading-cell"]') as HTMLElement; +const checkboxOf = (name: string) => + leadingCellOf(name).querySelector('[role="checkbox"]') as HTMLElement; + +describe.each([ + ['with row numbers', { showRowNumbers: true }], + ['without row numbers', { showRowNumbers: false }], +])('data-table leading cell hit area (%s)', (_label, extra) => { + it('a click on the cell beside the checkbox selects the row and does not open it', () => { + const { onRowClick, onSelectionChange } = renderTable(extra); + const cell = leadingCellOf('Ada'); + fireEvent.click(cell.querySelector('[data-slot="row-select"]') ?? cell); + expect(onRowClick).not.toHaveBeenCalled(); + expect(checkboxOf('Ada')).toHaveAttribute('aria-checked', 'true'); + expect(onSelectionChange).toHaveBeenLastCalledWith([ROWS[0]]); + + fireEvent.click(cell); + expect(onRowClick).not.toHaveBeenCalled(); + expect(checkboxOf('Ada')).toHaveAttribute('aria-checked', 'false'); + }); + + it('a click on the checkbox itself toggles once', () => { + const { onRowClick } = renderTable(extra); + fireEvent.click(checkboxOf('Grace')); + expect(checkboxOf('Grace')).toHaveAttribute('aria-checked', 'true'); + expect(onRowClick).not.toHaveBeenCalled(); + }); + + it('a click on a data cell still opens the record', () => { + const { onRowClick } = renderTable(extra); + fireEvent.click(screen.getByText('Ada')); + expect(onRowClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/components/src/renderers/complex/data-table.tsx b/packages/components/src/renderers/complex/data-table.tsx index 733befd9ac..4184852f1e 100644 --- a/packages/components/src/renderers/complex/data-table.tsx +++ b/packages/components/src/renderers/complex/data-table.tsx @@ -3437,6 +3437,11 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { // dropdown "Edit" item would navigate to the record detail. const target = e.target as HTMLElement; if ( + // objectui#13016 — the leading cell is the selection's hit area + // (see its own handler below): a click that + // misses the small checkbox there selects the + // row, never opens the record. + target.closest('[data-slot="row-leading-cell"]') || target.closest('button') || target.closest('a') || target.closest('input, select, textarea, label') || @@ -3469,7 +3474,15 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { {selectable && ( 0 && ["sticky left-0 z-10 bg-background", STICKY_ROW_TINT])} + // objectui#13016 — the whole cell toggles the row, not the 16px box + // alone: on a tall row the box is a small target + // and a near miss used to open the record. The + // checkbox itself handles its own clicks. + onClick={(e) => { + if ((e.target as HTMLElement).closest('[role="checkbox"]')) return; + handleSelectRow(rowId, !isSelected); + }} + className={cn(cellClassName, "w-10 px-3 relative cursor-pointer", showRowNumbers && "text-center", frozenColumns > 0 && ["sticky left-0 z-10 bg-background", STICKY_ROW_TINT])} > {/* objectui#12423 — the leading cell (see `leadingCellCount`): the row number by default, From 2f8a5768ea35efd7fb79a55b1c64a0a970e60acb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 03:29:51 +0000 Subject: [PATCH 2/2] test(components): pin the leading-cell hit area on a row other than the first (objectui#13016) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XV4S9ReSgn5hj61n1VbimV --- .../data-table-leadingCellHitArea-13016.test.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/components/src/renderers/complex/__tests__/data-table-leadingCellHitArea-13016.test.tsx b/packages/components/src/renderers/complex/__tests__/data-table-leadingCellHitArea-13016.test.tsx index 34cf2db5b0..fded7f913e 100644 --- a/packages/components/src/renderers/complex/__tests__/data-table-leadingCellHitArea-13016.test.tsx +++ b/packages/components/src/renderers/complex/__tests__/data-table-leadingCellHitArea-13016.test.tsx @@ -67,6 +67,16 @@ describe.each([ expect(checkboxOf('Ada')).toHaveAttribute('aria-checked', 'false'); }); + it('every row\'s leading cell is a hit area, not only the first', () => { + const { onRowClick, onSelectionChange } = renderTable(extra); + const cell = leadingCellOf('Grace'); + fireEvent.click(cell.querySelector('[data-slot="row-select"]') ?? cell); + expect(onRowClick).not.toHaveBeenCalled(); + expect(checkboxOf('Grace')).toHaveAttribute('aria-checked', 'true'); + expect(checkboxOf('Ada')).toHaveAttribute('aria-checked', 'false'); + expect(onSelectionChange).toHaveBeenLastCalledWith([ROWS[1]]); + }); + it('a click on the checkbox itself toggles once', () => { const { onRowClick } = renderTable(extra); fireEvent.click(checkboxOf('Grace'));