fix(ui-core): unblock antd migration — optional Alert title, dashed Divider, disabled-trigger Tooltip - #33163
fix(ui-core): unblock antd migration — optional Alert title, dashed Divider, disabled-trigger Tooltip#33163chirag-madlani wants to merge 3 commits into
Conversation
`title` was a required `string`. antd's equivalent, `message`, is an optional ReactNode, and its `description`-only alerts have no heading at all - so migrating those meant either inventing user-facing copy or leaving them on antd. Across Collate's 11 alerts only 2 had a plain string message: 6 passed markup and 3 had no message whatsoever. `title` is now optional and accepts a ReactNode. When it is absent the heading element is not rendered, and the alert falls back to the single-block layout it already used for a title with no body, so the icon stays centred rather than top-aligned against a lone paragraph. Existing call sites - which all pass a title, since it was required - render exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps that together blocked migrating antd's Divider: - No dashed variant. A solid rule here is a filled 1px box, and a background cannot be dashed, so the dashed case switches to a border and collapses the box in that axis. The flanking rules either side of a label follow the same variant. - The vertical rule expressed its height solely through `self-stretch`, which produces nothing outside a flex or grid parent and is overridden whenever a consumer aligns the divider itself - `self-center` on an unlayered LESS class is enough to collapse it to zero and make it vanish. antd's vertical divider had an intrinsic height for this reason; `min-h-[1em]` restores that without capping it when stretching does apply. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A disabled control fires no pointer or focus events, so react-aria never opened the tooltip on one. That is backwards for the common case: the tooltip is usually what explains *why* the control is disabled, and it disappeared exactly when the user needed it. antd wrapped disabled children in its own listener element to avoid this. A disabled child now takes the same auto-wrapping path as a non-focusable one, but through `Focusable` and a span rather than the `AriaButton` wrapper - the child is already a button, and nesting one inside another is invalid HTML and breaks clicks. The child is also made inert so it stops swallowing the pointer events the wrapper needs. Two Collate call sites are on antd purely because of this, and QueryToolbar has the same latent bug today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| return axis === 'y' | ||
| ? 'tw:w-0 tw:border-l tw:border-dashed tw:border-secondary' | ||
| : 'tw:h-0 tw:border-t tw:border-dashed tw:border-secondary'; |
There was a problem hiding this comment.
⚠️ Bug: Dashed divider uses non-existent color token, renders wrong color
The dashed branch of ruleClasses sets the border color with tw:border-secondary, but no --color-secondary token exists — the token is border-secondary, so the color utility must be tw:border-border-secondary (mirroring the solid branch's tw:bg-border-secondary). As written, the border-color utility isn't generated and the dashed rule falls back to currentColor (the inherited text color), so every dashed divider — the whole point of this change — draws in the wrong, usually darker color. The tests only assert border-dashed/border-l, so they don't catch it.
Use the border-secondary color token via border-border-secondary.:
return axis === 'y'
? 'tw:w-0 tw:border-l tw:border-dashed tw:border-border-secondary'
: 'tw:h-0 tw:border-t tw:border-dashed tw:border-border-secondary';
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| <AriaFocusable> | ||
| <span | ||
| className={cx( | ||
| 'tw:inline-flex tw:w-max tw:cursor-not-allowed tw:*:pointer-events-none', | ||
| triggerClassName | ||
| )} | ||
| tabIndex={0}> | ||
| {children} | ||
| </span> | ||
| </AriaFocusable> |
There was a problem hiding this comment.
💡 Edge Case: Disabled-trigger wrap ignores excludeTriggerFromTabOrder
The disabled-child branch hardcodes tabIndex={0} on the wrapper span, so a disabled trigger that a consumer explicitly asked to exclude from the tab order (excludeTriggerFromTabOrder) is still keyboard-focusable, unlike the non-disabled wrap path which honors that flag. Consider deriving tabIndex from excludeTriggerFromTabOrder (e.g. tabIndex={excludeTriggerFromTabOrder ? -1 : 0}) for consistency.
Was this helpful? React with 👍 / 👎
Code Review
|
| Compact |
|
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source
Three gaps in
ui-core-componentsthat are each blocking antd call sites from migrating. Separate commits, independently reviewable.Found while sweeping Collate — every one of these turned a would-be mechanical migration into "leave it on antd".
1.
Alert— a body without a titletitlewas a requiredstring. antd'smessageis an optional ReactNode, and itsdescription-only alerts have no heading at all. Of Collate's 11 alerts, only 2 had a plain string message — 6 passed markup, and 3 had no message whatsoever. Migrating those meant inventing user-facing copy.titleis now optional and takes aReactNode. When absent, the heading element isn't rendered and the alert uses the single-block layout it already had for a title with no body, so the icon stays centred instead of top-aligned against a lone paragraph. Every existing call site passes a title (it was required), so nothing changes for them.2.
Divider— dashed, and a vertical rule with a heightself-stretch, which produces nothing outside a flex/grid parent and is overridden the moment a consumer aligns the divider itself — an unlayered LESS.self-centeris enough to collapse it to zero and make it vanish silently. antd's vertical divider had an intrinsic height;min-h-[1em]restores that without capping the stretch case.Between them these unblock 21 Collate elements.
3.
Tooltip— open on a disabled triggerA disabled control fires no pointer or focus events, so react-aria never opened its tooltip. That's backwards for the usual case: the tooltip is what explains why the control is disabled, and it vanished exactly when the user needed it. antd wrapped disabled children in a listener element for this reason.
A disabled child now takes the same auto-wrapping path as a non-focusable one, but via
Focusable+ a span rather than theAriaButtonwrapper — the child is already a button, and nesting one inside another is invalid HTML and breaks clicks. The child is made inert so it stops swallowing the events the wrapper needs.Two Collate sites sit on antd purely because of this, and
QueryToolbarhas the same latent bug on main today.Verification
Table › can omit the selection cell for a full-width synthetic row) is pre-existing on clean main — verified by stashing this branch and re-running.tsc, eslint, prettier clean.Note for reviewers
A fourth change was planned — making
Buttonforward injected mouse handlers — and dropped after measuring. I'd assumed react-aria'sfilterDOMPropsstripped them. It doesn't:onBlur,onMouseOver,onMouseDownandonPointerEnterall reach the DOM. OnlyonFocusis swallowed, which means an antd-wrapped core Button gets hover tooltips but not keyboard-focus ones. Real, but narrow, and not worth speculative surgery on Button's prop plumbing — raising separately if it's worth fixing.Draft: happy to split further or adjust the Alert layout fallback if you'd rather it stayed top-aligned.
🤖 Generated with Claude Code