Skip to content

feat(Board): corner widget chrome, custom widget mods, layout-change reason, dragCancel default - #1350

Merged
tenphi merged 2 commits into
mainfrom
feat/board-widget-chrome-and-intent
Aug 24, 2026
Merged

feat(Board): corner widget chrome, custom widget mods, layout-change reason, dragCancel default#1350
tenphi merged 2 commits into
mainfrom
feat/board-widget-chrome-and-intent

Conversation

@tenphi

@tenphi tenphi commented Aug 24, 2026

Copy link
Copy Markdown
Member

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.

dragCancel gets a real default

It 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 as div[role="button"] still started a drag.

It worked out on a selectable board only by accident: selectionCancel incidentally 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. dragCancel now defaults to BOARD_SELECTION_CANCEL; pass '' for the old behaviour.

cornerChrome

A 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 dragCancel entry.

Pairs with resizeGripPlacement="corner" on the opposite corner.

mods on Board.Widget

App-defined modifiers merged into the ones the board sets, so a styles map can match on app state instead of the app swapping whole style objects per state:

<Board.Widget mods={{ editing: true }} styles={{ shadow: { editing: '0 0 0 1bw #primary' } }} />

Board's own modifiers are applied last, so a custom one can never shadow selected, drag and the rest — which the board's styling and its accessibility wiring both read. There's a test for exactly that.

onLayoutChange reports why

A board commits for two different kinds of reason: a gesture ('drag', 'resize', 'transfer') and a normalization ('normalize' — a reflow after the column count changed, an isAutoHeight widget 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 '': false to switch a single state off is the easy mistake: it opts into replace mode and silently takes selected, pre-selected and 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

  • 2113 unit tests + 36 browser tests pass. New coverage: chrome renders outside the widget host, stays reachable by assistive tech, and cannot start a drag; a custom mod reaches a style map; a custom mod cannot shadow a board state; a resize reports reason: 'resize'.
  • Docs, a CornerChrome story, and a changeset are included; the defaults registry is regenerated.
  • One pre-existing lint finding fixed along the way: the new corner transform map now carries a default, so chrome is still centred if no corner modifier matches.

🤖 Generated with Claude Code


Note

Medium Risk
Default dragCancel changes pointer-drag behavior for all boards, and TypeScript onLayoutChange signatures 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.

dragCancel now defaults to BOARD_SELECTION_CANCEL (same as selectionCancel), 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 / cornerChromePlacement on Board.Widget (and via widgetProps) render corner controls in the grip layer outside the widget clip and outside the drag gesture—no dragCancel needed. mods merges app-defined tasty modifiers under the board’s own (selected, drag, etc. always win), including on the floating drag clone.

onLayoutChange and Board.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.

…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-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 59cfccb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cube-dev/ui-kit Minor

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

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cube-ui-kit Ready Ready Preview Aug 24, 2026 5:46pm

Request Review

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-f4fa715.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Passed?
All 497.35 KB (+0.07% 🔺) Yes 🎉
Tree shaking (just a Button) 118.91 KB (0% 🟰) Yes 🎉

Compared against main at 8fdee38run 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.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread src/components/layout/Board/Board.tsx Outdated
Comment thread src/components/layout/Board/WidgetHost.tsx
… 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>
@tenphi
tenphi merged commit ee97f36 into main Aug 24, 2026
16 checks passed
@tenphi
tenphi deleted the feat/board-widget-chrome-and-intent branch August 24, 2026 18:08
@tenphi tenphi mentioned this pull request Aug 24, 2026
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.

1 participant