Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

A summary of notable changes per release. For the full commit history see the [repository on GitHub](https://github.com/jbetancur/react-data-table-component/commits/master).

## 8.12.0

### New features

- **`textarea` cell editor** — multi-line text editor with newline insertion, modifier-key/blur commit, and row height expansion. → [Inline editing](/docs/inline-editing) ([#1354](https://github.com/jbetancur/react-data-table-component/issues/1354))

---

## 8.11.0

### Behavior changes
Expand Down
64 changes: 58 additions & 6 deletions apps/docs/src/components/demos/InlineEditingDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,55 @@ interface Employee {
status: Status;
salary: number;
remote: boolean;
notes: string;
}

const initialData: Employee[] = [
{ id: 1, name: 'Aria Chen', department: 'Engineering', status: 'Active', salary: 155000, remote: true },
{ id: 2, name: 'Marcus Webb', department: 'Product', status: 'Active', salary: 132000, remote: false },
{ id: 3, name: 'Priya Kapoor', department: 'Design', status: 'On Leave', salary: 118000, remote: true },
{ id: 4, name: 'Jordan Ellis', department: 'Analytics', status: 'Active', salary: 143000, remote: false },
{ id: 5, name: 'Sam Rivera', department: 'Engineering', status: 'Terminated', salary: 128000, remote: false },
{
id: 1,
name: 'Aria Chen',
department: 'Engineering',
status: 'Active',
salary: 155000,
remote: true,
notes: 'Tech lead on the platform team.\nMentoring two juniors.',
},
{
id: 2,
name: 'Marcus Webb',
department: 'Product',
status: 'Active',
salary: 132000,
remote: false,
notes: 'Focusing on Q3 roadmap and feature delivery.',
},
{
id: 3,
name: 'Priya Kapoor',
department: 'Design',
status: 'On Leave',
salary: 118000,
remote: true,
notes: 'Returning next month.\nDesign system lead.',
},
{
id: 4,
name: 'Jordan Ellis',
department: 'Analytics',
status: 'Active',
salary: 143000,
remote: false,
notes: 'Leading the ETL pipeline migration project.',
},
{
id: 5,
name: 'Sam Rivera',
department: 'Engineering',
status: 'Terminated',
salary: 128000,
remote: false,
notes: 'Completed handover of all open tickets.',
},
];

const statusColors: Record<Status, string> = {
Expand Down Expand Up @@ -106,6 +147,15 @@ export default function InlineEditingDemo() {
},
onCellEdit: handleCellEdit,
},
{
id: 'notes',
name: 'Notes',
selector: r => r.notes,
grow: 2,
maxWidth: '150px',
editor: { type: 'textarea', placeholder: 'Add notes…', rows: 2 },
onCellEdit: handleCellEdit,
},
{
id: 'remote',
name: 'Remote',
Expand All @@ -121,7 +171,9 @@ export default function InlineEditingDemo() {
<div className="space-y-2">
<p className="text-xs text-gray-400">
Click any cell to edit. <strong>Name</strong> and <strong>Salary</strong> are text inputs;{' '}
<strong>Department</strong> and <strong>Status</strong> are dropdowns; <strong>Remote</strong> is a checkbox.{' '}
<strong>Department</strong> and <strong>Status</strong> are dropdowns; <strong>Notes</strong> is a
multi-line textarea (<kbd>Enter</kbd> for newline, <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> or blur
to commit); <strong>Remote</strong> is a checkbox.{' '}
<kbd>Enter</kbd> commits, <kbd>Esc</kbd> cancels. <strong>Name</strong> and <strong>Salary</strong> are
validated: try committing an empty name or a negative salary to see the inline error. Keyboard navigation is
enabled too: click or Tab into the table, move between cells and headers with the arrow keys, and press{' '}
Expand Down
8 changes: 6 additions & 2 deletions apps/docs/src/pages/docs/api.astro
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const columns: TableColumn<MyRow>[] = [
type CellEditor<T = unknown> =
| { type: 'text'; placeholder?: string }
| { type: 'textarea'; placeholder?: string; rows?: number }
| { type: 'number'; placeholder?: string; min?: number; max?: number; step?: number }
| { type: 'date'; min?: string; max?: string }
| { type: 'checkbox' }
Expand Down Expand Up @@ -202,8 +203,9 @@ interface CustomCellEditorContext<T> {
<DocsTable
headers={['Variant', 'Field', 'Type', 'Description']}
rows={[
['All', '<code>type</code>', '<code>"text" | "number" | "date" | "checkbox" | "select" | "custom"</code>', 'Editor widget to render.'],
['<code>text</code> / <code>number</code> / <code>select</code>', '<code>placeholder</code>', '<code>string</code>', 'Placeholder text. For <code>select</code>, shown as a disabled hidden option when the current value is empty.'],
['All', '<code>type</code>', '<code>"text" | "textarea" | "number" | "date" | "checkbox" | "select" | "custom"</code>', 'Editor widget to render.'],
['<code>text</code> / <code>textarea</code> / <code>number</code> / <code>select</code>', '<code>placeholder</code>', '<code>string</code>', 'Placeholder text. For <code>select</code>, shown as a disabled hidden option when the current value is empty.'],
['<code>textarea</code>', '<code>rows</code>', '<code>number</code>', 'Visible text lines for multi-line editing (default: 3). Forwarded to native <code>&lt;textarea&gt;</code>.'],
['<code>number</code>', '<code>min</code> / <code>max</code> / <code>step</code>', '<code>number</code>', 'Forwarded to the native <code>&lt;input type="number"&gt;</code>.'],
['<code>date</code>', '<code>min</code> / <code>max</code>', '<code>string</code>', 'Forwarded to the native <code>&lt;input type="date"&gt;</code> as ISO dates.'],
['<code>select</code>', '<code>options</code>', '<code>{ value: string; label: ReactNode }[]</code>', 'Dropdown options. The <code>value</code> is what gets committed; <code>label</code> is displayed (must be string or number inside native <code>&lt;option&gt;</code>).'],
Expand All @@ -219,6 +221,7 @@ interface CustomCellEditorContext<T> {
['<code>rdt_cellEditable</code>', 'Cell container', 'Added on every cell that has an editor defined (idle state).'],
['<code>rdt_cellEditing</code>', 'Cell container', 'Added while the editor is open. Removes padding and paints the focus ring.'],
['<code>rdt_editInput</code>', '<code>&lt;input&gt;</code>', 'Text editor control inside an editing cell.'],
['<code>rdt_editTextarea</code>', '<code>&lt;textarea&gt;</code>', 'Multi-line textarea editor control inside an editing cell.'],
['<code>rdt_editSelect</code>', '<code>&lt;select&gt;</code>', 'Dropdown editor control inside an editing cell.'],
]}
/>
Expand All @@ -231,6 +234,7 @@ interface CustomCellEditorContext<T> {
['<code>--rdt-color-cell-edit-bg</code>', '8% primary on bg', 'Background of the cell while editing.'],
['<code>--rdt-color-cell-edit-hover</code>', '6% primary', 'Background of an editable cell on hover.'],
['<code>--rdt-color-cell-edit-hover-border</code>', '40% primary', 'Dashed underline color on editable cell hover.'],
['<code>--rdt-cell-edit-textarea-max-height</code>', '<code>240px</code>', 'Maximum height of the inline textarea editor before scrolling.'],
]}
/>

Expand Down
37 changes: 27 additions & 10 deletions apps/docs/src/pages/docs/inline-editing.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import DocsTable from '../../components/DocsTable.astro';

<p>
Click an editable cell to swap its content for an inline editor. Commit with <kbd>Enter</kbd>
or by clicking away; <kbd>Esc</kbd> discards the change. The library doesn't own your data
. You handle the update in <code>onCellEdit</code>.
(or <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> for textareas) or by clicking away;
<kbd>Esc</kbd> discards the change. The library doesn't own your data. You handle the update
in <code>onCellEdit</code>.
</p>

<p>
Six editor types are built in: <code>text</code>, <code>number</code>, <code>date</code>,
<code>checkbox</code>, <code>select</code>, and <code>custom</code>. The cell itself becomes
Seven editor types are built in: <code>text</code>, <code>textarea</code>, <code>number</code>,
<code>date</code>, <code>checkbox</code>, <code>select</code>, and <code>custom</code>. The cell itself becomes
the editor: full-bleed input, primary-color focus ring, no nested borders. Add a
<code>validate</code> function on any column to reject invalid input or surface an inline
error message.
Expand Down Expand Up @@ -147,6 +148,18 @@ export default function App() {
editor: { type: 'text', placeholder: 'Enter a name…' },
onCellEdit }`} />

<h3>Multi-line text (textarea)</h3>
<p>
For multi-line notes, descriptions, or addresses. <kbd>Enter</kbd> inserts a newline, while
<kbd>Ctrl</kbd>+<kbd>Enter</kbd>, <kbd>Cmd</kbd>+<kbd>Enter</kbd>, or clicking away (blur) commits the edit:
</p>

<CodeBlock code={`{
id: 'notes', name: 'Notes', selector: r => r.notes,
editor: { type: 'textarea', placeholder: 'Add notes…', rows: 3 },
onCellEdit,
}`} />

<h3>Number input</h3>
<p>
A native <code>type="number"</code> field with optional <code>min</code>, <code>max</code>,
Expand Down Expand Up @@ -271,6 +284,7 @@ export default function App() {
['<code>--rdt-color-cell-edit-bg</code>', '8% primary on bg', 'Background of the cell while editing.'],
['<code>--rdt-color-cell-edit-hover</code>', '6% primary', 'Background of an editable cell on hover.'],
['<code>--rdt-color-cell-edit-hover-border</code>', '40% primary', 'Color of the dashed underline that hints "this cell is editable" on hover.'],
['<code>--rdt-cell-edit-textarea-max-height</code>', '<code>240px</code>', 'Maximum height of the inline textarea editor before scrolling.'],
]}
/>

Expand Down Expand Up @@ -456,14 +470,16 @@ const columns: TableColumn<Employee>[] = [

<h2>Styling the input</h2>
<p>
Editor controls have class <code>rdt_editInput</code> (text) and <code>rdt_editSelect</code>
(dropdown). Both inherit font and color from the cell and stretch to fill its full
footprint. The cell itself uses <code>rdt_cellEditable</code> (idle hover state) and
<code>rdt_cellEditing</code> (active edit state). Override any of them in your stylesheet:
Editor controls have class <code>rdt_editInput</code> (text), <code>rdt_editTextarea</code>
(multi-line text), and <code>rdt_editSelect</code> (dropdown). All inherit font and color
from the cell and stretch to fill its full footprint. The cell itself uses
<code>rdt_cellEditable</code> (idle hover state) and <code>rdt_cellEditing</code> (active edit state).
Override any of them in your stylesheet:
</p>

<CodeBlock code={`/* Make the input bigger and chunkier */
.rdt_editInput,
.rdt_editTextarea,
.rdt_editSelect {
font-size: 14px;
font-weight: 500;
Expand All @@ -488,7 +504,7 @@ const columns: TableColumn<Employee>[] = [

<h2>Limitations</h2>
<ul>
<li>Built-in editors cover text, number, date, checkbox, and select. For autocomplete, multi-line text, rich pickers, etc. use the <code>custom</code> editor.</li>
<li>Built-in editors cover text, textarea, number, date, checkbox, and select. For autocomplete, rich pickers, etc. use the <code>custom</code> editor.</li>
<li>The <code>value</code> passed to <code>onCellEdit</code> and <code>validate</code> is always a string. Parse it yourself.</li>
<li>Dropdown <code>label</code>s must be strings or numbers. They render inside native <code>&lt;option&gt;</code> elements which can't contain arbitrary React content.</li>
<li>There is no built-in "dirty" indicator or undo stack. Track that in your application state if you need it.</li>
Expand All @@ -499,6 +515,7 @@ const columns: TableColumn<Employee>[] = [

<CodeBlock code={`type CellEditor<T = unknown> =
| { type: 'text'; placeholder?: string }
| { type: 'textarea'; placeholder?: string; rows?: number }
| { type: 'number'; placeholder?: string; min?: number; max?: number; step?: number }
| { type: 'date'; min?: string; max?: string }
| { type: 'checkbox' }
Expand Down Expand Up @@ -533,7 +550,7 @@ interface CustomCellEditorContext<T> {
['<code>editable</code>', '<code>boolean</code>', 'Shorthand for a text editor. Equivalent to <code>editor: &#123; type: \'text\' &#125;</code>. Ignored when <code>editor</code> is set.'],
['<code>editor</code>', '<code>CellEditor</code>', 'Editor configuration. See <a href="#celleditor">CellEditor</a> above for the full union.'],
['<code>validate</code>', '<code>(value, row, column) =&gt; true | false | string</code>', 'Runs before <code>onCellEdit</code>. Return <code>true</code> to accept, <code>false</code> to reject silently, or a string to keep the editor open with an inline error.'],
['<code>onCellEdit</code>', '<code>(row: T, value: string, column: TableColumn&lt;T&gt;) =&gt; void</code>', 'Called when the user commits an edit (<kbd>Enter</kbd>, blur, or selection change for dropdowns). Receives the original row, the new string value, and the column definition.'],
['<code>onCellEdit</code>', '<code>(row: T, value: string, column: TableColumn&lt;T&gt;) =&gt; void</code>', 'Called when the user commits an edit (<kbd>Enter</kbd>, <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> for textareas, blur, or selection change for dropdowns). Receives the original row, the new string value, and the column definition.'],
]}
/>

Expand Down
19 changes: 16 additions & 3 deletions src/DataTable.css
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,8 @@
the cell keeps its horizontal padding (and thus its original width — no
jitter). It stretches vertically because the cell uses align-items:stretch. */
.rdt_editInput,
.rdt_editSelect {
.rdt_editSelect,
.rdt_editTextarea {
display: block;
box-sizing: border-box;
width: calc(100% + 2 * var(--rdt-cell-padding-x, 16px));
Expand All @@ -938,7 +939,18 @@
-webkit-appearance: none;
}

.rdt_editInput::placeholder {
.rdt_editTextarea {
padding-top: 12px;
padding-bottom: 12px;
resize: none;
max-height: var(--rdt-cell-edit-textarea-max-height, 240px);
overflow-y: auto;
font-family: inherit;
line-height: inherit;
}

.rdt_editInput::placeholder,
.rdt_editTextarea::placeholder {
color: var(--rdt-color-text-secondary, rgba(0, 0, 0, 0.54));
opacity: 0.7;
}
Expand Down Expand Up @@ -970,7 +982,8 @@

/* Dense rows shrink the editor height to match */
.rdt_rowDense .rdt_editInput,
.rdt_rowDense .rdt_editSelect {
.rdt_rowDense .rdt_editSelect,
.rdt_rowDense .rdt_editTextarea {
min-height: 32px;
}

Expand Down
30 changes: 30 additions & 0 deletions src/__tests__/accessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ describe('axe', () => {
expect(container.querySelector('.rdt_cellEditing input')).not.toBeNull();
await expectNoViolations();
});

test('cell editor open (textarea)', async () => {
const { container } = render(
<DataTable columns={[{ ...columns[0], editor: { type: 'textarea' } }, columns[1]]} data={rows} />,
);
fireEvent.click(container.querySelector('.rdt_cellEditable') as HTMLElement);
expect(container.querySelector('.rdt_cellEditing textarea')).not.toBeNull();
await expectNoViolations();
});
});

describe('header cell semantics', () => {
Expand Down Expand Up @@ -161,6 +170,27 @@ describe('header cell semantics', () => {
expect(label?.closest('[role="columnheader"]')).not.toBeNull();
});

test('the inline textarea editor is labelled by its column header', () => {
const jsxColumns: TableColumn<Row>[] = [
{
id: 'name',
name: (
<span>
Full <em>notes</em>
</span>
),
selector: r => r.name,
editor: { type: 'textarea' },
},
];
const { container } = render(<DataTable columns={jsxColumns} data={rows} />);
fireEvent.click(container.querySelector('.rdt_cellEditable') as HTMLElement);
const textarea = container.querySelector('.rdt_cellEditing textarea') as HTMLElement;
const label = document.getElementById(textarea.getAttribute('aria-labelledby') as string);
expect(label?.textContent).toBe('Full notes');
expect(label?.closest('[role="columnheader"]')).not.toBeNull();
});

test('the sort control is queryable as a button named after its column', () => {
const { container, getByRole } = render(<DataTable columns={columns} data={rows} />);
fireEvent.click(getByRole('button', { name: 'Name' }));
Expand Down
Loading
Loading