Skip to content

feat(data-table): add declarative cell-editing errors - #635

Merged
IzumiSy merged 16 commits into
mainfrom
feat/data-table-edit-errors
Oct 28, 2026
Merged

IzumiSy merged 16 commits into
mainfrom
feat/data-table-edit-errors

Conversation

@IzumiSy

@IzumiSy IzumiSy commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

DataTable's built-in editors already handle draft input, parsing, and keyboard navigation, but screens must build their own feedback for failures in editing callbacks. A screen can reject a proposed value by leaving data unchanged, yet that does not provide a consistent, accessible way to explain the failure beside the affected cell.

The goal here is declarative error presentation, not a new validation API. Existing callbacks should remain responsible for application rules and persistence, while DataTable supplies a standard error popover and lets consumers customize its content.

An initial prototype exposed an important UX inconsistency: Enter discarded the rejected draft and restored the adopted value while leaving the cell marked invalid. The displayed value no longer matched the error, and correcting or dismissing that error was unclear. This change treats the draft and its failure as one editing session so they cannot become detached.

Keyboard navigation must not make an unchanged failed value appear accepted or trigger another save merely because the user moves to a different cell. Dismissing error details must remain distinct from resolving the error.

Screen shot

An error in cell

スクリーンショット 2026-10-22 14 55 27

Error detail

スクリーンショット 2026-10-22 14 55 47

Design Decision

Callback-driven errors with safe user-facing messages

By default, synchronous throws and returned Promise rejections from onValueChange and onValueCommitted use the cell's error popover. The standard body displays a non-empty message only from the exported CellEditError, marking it as safe to show to users. Consumers translate these messages before throwing. Ordinary errors, other thrown values, and empty messages use the localized fallback “Could not confirm this change” / 「変更を確定できませんでした」 in both the popover and its accessible description; internal messages and stack traces are not exposed.

The fallback describes an unconfirmed change rather than assuming the input is invalid or claiming that DataTable performed a save. Application rules and persistence remain consumer-owned. Capturing a callback failure does not automatically log or report it; consumers remain responsible for diagnostics.

Consumers only configure exceptions to the default:

error?: false | {
  render: (ctx: { row: TRow; error: unknown }) => ReactNode;
};

error: false preserves eager edit completion and leaves callback failures to the consumer: synchronous exceptions are not intercepted, and returned Promises are not observed. error.render receives the current row and original thrown value and replaces the popover body, including support for interactive React content. Custom content owns its message-safety policy. DataTable owns the indicator, positioning, keyboard access, and close button; it adds no reset or rollback action.

There is no presentation discriminator because only popovers are supported. Inline presentation was left out because it introduces separate questions about replacing cell content, editor layout, and display while typing. There are also no new validate, schema, or business-constraint options.

Keep the failed draft until success or cancellation

Each cell has one internal session: idle, editing, confirming, or failed. A failed state always contains its target draft.

  • A failed change check prevents confirmation; a pending change check must succeed before onValueCommitted runs.
  • onValueCommitted is the confirmation attempt. A normal return or fulfilled Promise completes the edit; a throw or rejection retains the original draft for correction or explicit retry.
  • Esc first closes open error details without discarding the draft. A subsequent Esc in the editor or error trigger discards the draft and error together and restores the latest consumer-provided data.
  • Tab and Shift+Tab move between editable cells or leave the table without discarding pending or failed callback drafts. For direct inputs, neither Tab nor blur retries an already failed commit: Enter explicitly retries the same draft, while changing the input restores the normal confirmation flow. Repeated confirmation and subsequent blur do not duplicate an in-flight attempt. Existing malformed-number behavior remains separate.
  • New input, cancellation, and unmount invalidate stale UI completions. Repeating an unchanged date action cannot bypass a failed check or restart a pending confirmation.

The public callback signatures remain => void to preserve compatibility with existing handlers and successful return values. Only returned Promises are observed for the editing lifecycle; successful return values are ignored, not adopted as row data. Adoption, persistence, request cancellation, and rollback remain consumer-owned; suppressing stale UI results does not undo callback side effects or prevent stale consumer writes.

Error inspection is part of the cell's focus group

New callback failures normally open their details without moving focus, including an asynchronous failure that arrives after the user has moved to another cell. Tab/Shift+Tab navigation dismisses the departing cell's details instead, including a failure thrown synchronously by that confirmation. The failed draft, error outline, icon, and accessible description remain intact. Returning to the cell does not reopen the same dismissed failure; the error icon or Alt+ArrowDown reopens it, and Alt+ArrowDown also focuses details that are already open.

Opening or interacting with the error popover must not accidentally confirm or discard a draft. The cell and its portaled inspector form one logical focus group, and Tab within interactive error content still moves between its controls. Closing details during navigation must not pull focus back into the failed cell, including at the first and last editable cells; ordinary outside-click dismissal also respects the chosen focus destination. Dismissing details and cancelling an edit remain separate operations: dismissal retains the draft and error, whereas cancellation clears them together.

Summary

  • Add standard and consumer-defined callback-error popovers across typing, choice, date, and date-time editors, with automatic failure details that preserve focus and dismissal on Tab navigation.
  • Export CellEditError for safe, translated user-facing messages; use localized confirmation-failure feedback for other thrown values.
  • Introduce a shared editing-session lifecycle that retains pending and failed drafts, gates confirmation on change checks, and separates explicit retry and cancellation from Tab navigation and error-detail dismissal.
  • Add state-machine and interaction regression coverage, an interactive DataTable Lab showcase, updated documentation, and a minor changeset with usage examples.

@IzumiSy
IzumiSy marked this pull request as ready for review October 22, 2026 06:00
@IzumiSy
IzumiSy requested a review from a team as a code owner October 22, 2026 06:00
@wemra3

wemra3 commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Since #583, components use the danger roles instead of the destructive utilities, and destructive is deprecated. This PR adds text-destructive and ring-destructive to the error icon, and the cell still uses ring-destructive from #611. Could you change them?

  • ring-destructive → ring-danger-solid (same colour)
  • text-destructive → text-danger-text, which other error icons already use (Field, CsvImporter, Alert)

@interacsean interacsean left a comment

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 I enter 13, then blur, it takes the stated 600ms to show the error, if I re-enter the field and blur again, the error clears (even though I have left the 13 untouched). If the field hasn't changed since the last error, it can probably keep the error without revalidating... I guess there's some case where the validation may clear, based on updated data elsewhere though.
  • The error information is not accessible via keyboard navigation (i.e. tab)
  • I'd like to see the error popover show up when it first occurs — I think for both, because otherwise the user has to make a mouse/touch interaction to understand what is wrong. This would help with accessibility. It could have a 'x' to dismiss it (and then user can open again via "(!)")
  • The style looks okay, although I think it can be improved: the alignment to the issue cell could be tighter to make it even more clear which cell the error is referring to.
  • Showing the thrownError.message means that an error which is unexpected ends up showing text in the UI. Wdyt about creating an error class that extends error, and consumer code must throw one of these errors to show text. If any other error is thrown, UI just shows "An error occurred". I haven't thought through i18n: either you throw CellValidationError(t("my error")) or perhaps throw CellValidationError("my error") could try to pass "my error" to the i18n functions. Examples in the docs would be good for this too
  • inline-editing-demo.tsx still says a rejected value “does not block Tab navigation or create a cell error”

typeOptions: { maxDecimals: 0 }, // Display precision, not an input rule.
edit: {
canEdit: (_row, { selected }) => selected,
onValueCommitted: (row, value) => {

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.

We should update this example of error handling in here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in 6399348. This example now throws CellEditError instead of showing a toast and returning, so invalid quantities retain their draft and display cell-local feedback. Removed the unused useToast reference and regenerated the documentation.

itsprade and others added 2 commits October 26, 2026 13:23
* feat(data-table): give the default cell-error popover an error look and undo hint

The standard popover body showed only the message, so it read like a
neutral tooltip. It now leads with a destructive alert icon, shows the
message in medium weight, and adds the existing "Press Esc to undo" hint.
Custom error.render content is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(data-table): tighten the cell-error popover

Drop the undo hint and the popup border, reduce padding and cap the width
at max-w-xs so long messages wrap. The lab's Draft note column now throws
a longer message on "?" to show wrapping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(data-table): refine cell-error popover placement and tint

- 1px error ring on the cell, including while focused
- popup opens directly under the cell, aligned to its left edge
- light red tint (Alert's danger-surface) layered over the opaque popover
  colour so table lines don't show through; softer shadow-sm
- icon centred on the first text line, at 70% of the text colour
- lab: "!!" in Draft note throws a long message to show wrapping

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(data-table): 1.5px error outline, heavier error icons

- cell error ring is 1.5px (resting and focused)
- error icons in the cell and popover use strokeWidth 2.5
- drop the focus ring on the in-cell error trigger; it drew a second
  box around the icon when focus returned to it on popover close

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@IzumiSy

IzumiSy commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

In reply to the color-token feedback:

Updated in 6399348. The cell error outline now uses ring-danger-solid, and the error icon uses text-danger-text. The latest popover UI refinements are preserved.

@IzumiSy

IzumiSy commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

In reply to this review:

Thanks for the detailed feedback! Updated in 6399348:

  • An unchanged failed draft now retains its error through refocus and blur without revalidating. Enter can explicitly retry it; regression tests cover this sequence.
  • Error details are accessible with Alt+ArrowDown, with an associated accessible description explaining the shortcut. Tab remains dedicated to moving between editable cells rather than adding another stop for the error icon.
  • New failures automatically open the details without stealing focus, including asynchronous failures after moving to another cell. The close button dismisses only the details; the draft and error remain available for correction or reopening.
  • The latest cell-anchored popover styling is preserved, with the gap reduced to 2px.
  • Added CellEditError. Only its non-empty message is shown by the standard UI; unexpected errors use the localized generic message. Consumers translate messages before throwing, e.g. new CellEditError(t("quantityRequired")). Custom renderers still receive the original thrown value.
  • Updated the inline-editing demo and documentation to use CellEditError. Consumer-owned optimistic saving and rollback remain unchanged.

@IzumiSy

IzumiSy commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Screen shot for the current UI

スクリーンショット 2026-10-26 15 26 02

@IzumiSy

IzumiSy commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

…errors

# Conflicts:
#	docs-manifest.json

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>
Copilot stopped work on behalf of IzumiSy due to an error October 26, 2026 06:47
@IzumiSy

IzumiSy commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

…errors

# Conflicts:
#	docs-manifest.json
#	examples/vite-app/src/pages/showcase/data-table-lab/inline-editing-demo.tsx

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

Copilot AI commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved and pushed in b1a55bc. Merged the latest main, preserved both CellEditError and aggregation demo imports, and regenerated documentation metadata. All 791 targeted tests, type checks, lint, formatting, docs checks, and security scans passed. check-dts was blocked by npm React dependency resolution in its temporary consumer install.

@interacsean

Copy link
Copy Markdown
Contributor

I picked up two more issues that fell out of the last round of edits

cell-remaining.mp4
  1. The 13 async example does not seem to have actually been fixed. I.e. error clears even though value has not changed
  2. Edge-ish case for showing the error by default. In the example I type 'bad':
    a. If I click outside, the error notification shows
    b. If I 'tab', it flickers on then disappears. I assume this is because it is the last editable cell — either last in the row, or last in the table altogether (I didn't check and the table only has one row)

@IzumiSy

IzumiSy commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

@interacsean Thanks for catching these! Updated in d77c7e1:

  1. For the async 13 case, Tab/Shift+Tab no longer retries an unchanged failed commit. The failed value and error indicator stay intact; Enter explicitly retries it.
  2. Tab now intentionally closes the error details while retaining the failed draft and error indicator. A synchronous failure from Tab no longer briefly opens the popover, and leaving the last editable cell moves focus out of the table normally rather than back into the cell.

Refocusing does not reopen dismissed details; use the error icon or Alt+ArrowDown. Tab within interactive popover content still works normally.

@IzumiSy
IzumiSy merged commit bb54644 into main Oct 28, 2026
8 checks passed
@IzumiSy
IzumiSy deleted the feat/data-table-edit-errors branch October 28, 2026 03:35
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.

5 participants