feat(Board): corner widget chrome, custom widget mods, layout-change reason, dragCancel default - #1350
Conversation
…reason, dragCancel default Four gaps that each forced an app to reach around the component. `dragCancel` defaulted to no cancel at all, so a pointer-down on a button inside a widget dragged the widget instead of pressing the button. That only worked out on a *selectable* board, where `selectionCancel` incidentally doubles as the drag guard; every other board had to hand-write a selector, and a hand-written one is a copy of ours that drifts -- typically missing the ARIA roles, so a react-aria control rendering as `div[role="button"]` still dragged. It now defaults to `BOARD_SELECTION_CANCEL`. `cornerChrome` puts a control on a widget's corner. A widget clips its own content, so an app hanging a settings button off the corner gets it cropped in half -- or clipped by an ancestor's scroll container when the widget is in the first row. Board already draws corner resize grips in a layer that escapes that clip; chrome now shares it. Being outside the widget host, it is outside the drag gesture too, so it needs no `dragCancel` entry. `mods` lets an app put its own state on a widget and style against it, instead of swapping whole style objects per state. Board's own modifiers are applied last so a custom one cannot shadow `selected` or `drag`, which the styling and the a11y wiring both read. `onLayoutChange` now says why it fired. A board commits both for gestures and for normalizations -- a reflow after the column count changed, an auto-height widget growing -- and an app persisting every commit had no way to tell them apart, so it wrote normalizations back as edits and dirtied documents nobody touched. The argument is additive; existing handlers keep working. The docs also now explain that widget style maps MERGE: omit `''` to extend the defaults, set it to replace them and use `@inherit` to keep what matters. Reaching for `'': false` to switch one state off is the easy mistake -- it silently takes `selected`, `pre-selected` and the drag lift with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 59cfccb The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📦 NPM canary releaseDeployed canary version 0.0.0-canary-f4fa715. |
🏋️ Size limit report
Compared against main at 8fdee38 — run 32753929263, 2026-08-24T16:58:31Z.To see which modules changed, download the size-limit-statoscope-report artifact from this run and open report.html. |
🧪 Storybook is successfully deployed!
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 65acbaa. Configure here.
… mods on the drag clone Both from review of #1350. `cornerChrome`, `cornerChromePlacement` and `mods` are part of the widget defaults type, so `widgetProps` advertises them -- but the host only read them from the per-widget registration, so a board-level value was silently dropped. Being typed as a default and then ignored is worse than not accepting it. `mods` merges board-level under per-widget, mirroring how `widgetProps.styles` merges under a widget's own styles. The floating clone spread the board's own mods but not the app's, so a custom state blinked off for the duration of a pointer drag -- the one moment it is most visible, since the clone IS the widget while the in-grid host is hidden. The regression test fails against the previous code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Follow-up to #1348. Four more places where Cube Cloud's Grid container (cubedevinc/cubejs-enterprise#13919) had to reach around
Board— each one fixed here rather than worked around there.dragCancelgets a real defaultIt defaulted to no cancel at all, so a pointer-down on a button inside a widget dragged the widget instead of pressing the button. Every board hosting interactive content had to hand-write a selector — and a hand-written one is a copy of ours that drifts. Cloud's was missing
[role="button"],[role="menuitem"],[role="checkbox"],[role="switch"]and[role="tab"], so a react-aria control rendering asdiv[role="button"]still started a drag.It worked out on a selectable board only by accident:
selectionCancelincidentally doubles as the drag guard there, which is an odd coupling — whether a button keeps its own press shouldn't depend on whether the board supports selection.dragCancelnow defaults toBOARD_SELECTION_CANCEL; pass''for the old behaviour.cornerChromeA control anchored to a widget's corner and centred on it — a settings button, a badge, a remove affordance.
A widget clips its own content, so an app hanging such a control off the corner itself gets it cropped in half, or clipped by an ancestor's scroll container when the widget sits in the first row. Cloud shipped both defects as documented-and-accepted. Board already draws corner resize grips in a layer that escapes that clip; chrome now shares it. Being outside the widget host it is outside the drag gesture too, so pressing it can never start a drag and it needs no
dragCancelentry.Pairs with
resizeGripPlacement="corner"on the opposite corner.modsonBoard.WidgetApp-defined modifiers merged into the ones the board sets, so a
stylesmap can match on app state instead of the app swapping whole style objects per state:Board's own modifiers are applied last, so a custom one can never shadow
selected,dragand the rest — which the board's styling and its accessibility wiring both read. There's a test for exactly that.onLayoutChangereports whyA board commits for two different kinds of reason: a gesture (
'drag','resize','transfer') and a normalization ('normalize'— a reflow after the column count changed, anisAutoHeightwidget growing). An app persisting every commit had no way to tell them apart, so it wrote normalizations back as edits, marking documents dirty nobody touched.Cloud's workaround was a 50-line diff of the committed layout against the rendered one to guess whether a commit was authoring. The argument is additive, so existing one-parameter handlers keep working.
A docs fix, not a code fix
While investigating a fifth request — "let a widget add a style state without losing the defaults" — it turned out tasty already supports this and the Board docs never said so. A style map with no
''entry extends the defaults; one that sets''replaces them, and'@inherit'cherry-picks what to keep.Reaching for
'': falseto switch a single state off is the easy mistake: it opts into replace mode and silently takesselected,pre-selectedand the drag lift with it. That is exactly what Cloud hit, and why it was re-authoring three shadow maps by hand. Documented now, with both forms shown.Verification
reason: 'resize'.CornerChromestory, and a changeset are included; the defaults registry is regenerated.transformmap now carries a default, so chrome is still centred if no corner modifier matches.🤖 Generated with Claude Code
Note
Medium Risk
Default
dragCancelchanges pointer-drag behavior for all boards, and TypeScriptonLayoutChangesignatures now include the info object—worth verifying consumers and persistence logic.Overview
Board gains four API improvements aimed at dashboards with interactive widgets and layout persistence.
dragCancelnow defaults toBOARD_SELECTION_CANCEL(same asselectionCancel), so buttons and other controls inside widgets no longer start a drag on non-selectable boards unless you opt out with''or a custom selector.cornerChrome/cornerChromePlacementonBoard.Widget(and viawidgetProps) render corner controls in the grip layer outside the widget clip and outside the drag gesture—nodragCancelneeded.modsmerges app-defined tasty modifiers under the board’s own (selected,drag, etc. always win), including on the floating drag clone.onLayoutChangeandBoard.Responsive’s handler now receive a second argument{ reason }('drag','resize','transfer','normalize') so apps can skip persisting automatic reflows. Docs add a Corner chrome story and explain widget style map merging (''replace vs extend).Exports:
BoardCornerPlacement,BoardLayoutChangeInfo,BoardLayoutChangeReason; eslint defaults registry updated.Reviewed by Cursor Bugbot for commit 59cfccb. Bugbot is set up for automated code reviews on this repo. Configure here.