Repository navigation
fix(styled): styling props no longer reach the DOM — 0.77.7 - #697
Merged
Merged
Conversation
React warned about Reqore's own styling props on DOM elements ("React
does not recognize the `hasIcon` prop", "Received `true` for a
non-boolean attribute `fill`"), and some were written out as attributes
(`theme="[object Object]"`, `color="#454545"` on buttons).
Two gaps let them through. A styled DOM tag filters props with
is-prop-valid, which keeps every name that is an attribute somewhere:
`fill`, `wrap`, `color`, `width`, `opacity`, `direction`, `offset`, ...
And a styled component rendered `as` another component (react-aria's
DateInput, re-resizable, animated.div, radix, a router link) forwards
everything, and that component writes it onto its own element. The
bare DatePicker hit the second: ReqoreInput handed its styled input's
flags to DateInput, whose Group put them on its div.
The styled components now name the styling props they keep back, with
the existing omitStyleProps helper. StyledEffect keeps back `color`,
`effect`, `intent`, `isText` and `theme`, so every component built on
it inherits that. The props ReqoreControlGroup hands its children are
one list, REQORE_CONTROL_GROUP_CHILD_PROPS, checked against what the
group actually hands out. ReqoreTooltipComponent no longer passes
`tooltip` to a DOM element, and ReqoreTree drops its unused `mode`.
Every story was rendered and the props each element received were
checked; the test renders the components that leaked, reading what
React handed each element rather than relying on React's warnings,
which come once per prop name per page.
…ePopover The popover clones `closePopover` onto every element of its content so a menu or a panel inside it can close it. A DOM element cannot use it, and React warned about it; a `ReqoreMessage` in the content forwarded it through `animated.div` onto its `<div>`. Found by the browser run of the Popover stories, whose plays open the popovers.
|
📦 Prerelease published for this PR: Install it in a consumer (updates yarn add @qoretechnologies/reqore@0.77.7-pr.697.g7a79650Published under the |
qlip visual regression✅ No visual changes. |
`as={rest.as || resizable ? Resizable : 'div'}` binds as
`(rest.as || resizable) ? Resizable : 'div'`, so a panel given `as`
rendered re-resizable's `Resizable` and the caller's element never
appeared. It was `as={rest.as || Resizable}` until 9ad9ed4 added the
`div` fallback without parentheses; the caller's `as` was meant to win.
`as` now wins, and re-resizable's props are only spread when the panel
really renders a `Resizable`, so they do not land on the caller's
element either. No other `a || b ? c : d` in src has a fallback value
on the left of the `||`.
ReqoreTier rendered `<ButtonBadge content={badge} />` with no size, and
the badge sized the gap in front of it from `PADDING_FROM_SIZE[size]`:
`NaN`, so the spacer was neither horizontal nor vertical and drew
nothing. The badge sits on a line of its own, so the tier now asks for
no gap (`margin='none'`, like the callout, entity row and feature card
badges) and passes the panel's size. ButtonBadge itself defaults `size`
to `normal`, the size its tag already fell back to, so a badge without
one can no longer compute `NaN`.
… props object DeepScan REACT_USELESS_DEPENDENCY_OF_HOOK: renderTag listed the per-render props object as its dependency, so it was rebuilt on every render; it only reads its arguments and `active`. The badge list was also a fresh array whenever the content was not one, which defeated the three badge-position memos; it is now memoised on the content itself.
Foxhoundn
added a commit
that referenced
this pull request
Oct 7, 2026
…nobs Brings in #697 (styling props no longer reach the DOM), released as 0.77.7 while this PR was open. The only conflict was the version: this PR moves from 0.77.6 to 0.77.8, the next one above develop's 0.77.7. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Foxhoundn
added a commit
that referenced
this pull request
Oct 7, 2026
develop released 0.77.7 (#697) while this PR was open, so these knobs and the button / input font fix ship in 0.77.8, not 0.77.6. The "new in" and "changed in" notes say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Reqore's own styling props reached DOM elements. React warned in consumers' story runs and tests, and some props were written out as attributes:
<DatePicker value={new Date()} onChange={…} />on its own warned aboutclearable("Receivedtruefor a non-boolean attribute") andhasIcon,hasRightIcon,hasShortcutHint,hasPasswordToggle("React does not recognize the prop"). Withminimalorpillset, it warned about those too, and wrotetheme="[object Object]",effect,_sizeandvalueonto the segment group's<div>.<ReqoreTabs fill …/>,<ReqoreCollection fill …/>, and aReqoreCheckboxgivenfill(a control group handsfillto every child) warned aboutfill.colorattribute. Spacers and table cells renderedwidthandheight. Panel titles renderedopacity, and timelines rendereddirection.Root cause
There are two separate gaps in styled-components v5:
is-prop-valid. That filter keeps any name that is an attribute on some element, sofill,wrap,color,width,height,opacity,direction,offsetandtypeall reach adivorspan.asanother component forwards every prop. That component then writes the props onto its own element. This applies to react-aria'sDateInput, re-resizable,animated.div, radix's slider root and a consumer's router link. The bare DatePicker hit this case:ReqoreInputrenders its styled inputas={DateInput}, and react-aria'sGroupspreads everything it is given onto its<div>.Fix
The fix uses the repo's existing convention,
omitStylePropswith.withConfig({ shouldForwardProp }). Each styled component now names the styling props it keeps back. Prop names are unchanged and styles still read every prop, so only the rendered element changes.StyledEffectkeeps backcolor,effect,intent,isTextandtheme. Every component built on it inherits this, because styled-components combines a base'sshouldForwardPropwith the derived one.REQORE_CONTROL_GROUP_CHILD_PROPS(inhelpers/styled.ts) is one list of every propReqoreControlGroupclones onto its children: the layout flags, plus the stack-position props in astackgroup.ReqoreControlGroupItemand the slider root use it, and a test checks the list against what the group actually hands out.Input(styled input, wrapper, icon/shortcut wrappers)DatePicker(DateInputno longer gets the field'svalue)Button(+ content, label spans)Tabs(tabs, list, item)Collection(wrapper, item content)Checkbox(row, switch toggle)Menu(resizableResizabletarget, divider)Panel(resizable target, title bars)Notification/Message(animated.divtarget)Tag(+ action wrapper)Span/Link(router-link target)ParagraphIconSpacerTimelineNavbar(type)Table(cell, column group)SliderrootReqoreTooltipComponentno longer passestooltipto a DOM element (Component='div').closePopoveronto content that is a DOM element. AReqoreMessagein popover content also no longer forwards it.ReqoreTreetakes its unusedmodeprop out of what it spreads onto the panel.Sweep
I rendered every story (73 files, 1092 stories) in jsdom. For every element, I read the props React actually received (React 18 keeps them on the node). I did not rely on React's warnings, which come only once per prop name per page. I also ran the whole unit suite with the same check after each test, and ran every story file in the browser (
yarn test:stories, one file at a time) with the plays opening popovers, menus and dropdowns.Leaks found and fixed: all of the components listed above. On develop, the browser story run logged 26 DOM-prop warnings. On this branch, the only one left is a story's own misuse:
TagGroup.storiespasses tag props (onRemoveClick,rightIcon,actions) to the group inmeta.args.Found and not changed:
disabledandreadOnlyon non-form elements (.reqore-control-wrapper,.reqore-checkbox, tags, table cells, panels, …). These reflect state, and code queries them; reqore's ownIconPickertest assertsdisabledon.reqore-icon-picker. Changing them would change behaviour, so it is left for a separate decision.zindex="-1"on the rich-text editor comes from slate-react itself.placeholderon theDropdownbutton.Two bugs the sweep found, fixed in follow-up commits
ReqorePanelgivenasrendered aResizable(2f373f3)as={rest.as || cond ? Resizable : 'div'}binds as(rest.as || cond) ? Resizable : 'div'. It wasas={rest.as || Resizable}until 9ad9ed4 added thedivfallback without parentheses, so the caller'saswas meant to win.asnow wins. re-resizable's props are spread only when the panel really renders aResizable, so they no longer land on the caller's element.a || b ? c : dinsrc(127 of them) found no other one with a fallback value on the left of the||.ReqoreTierrendered its badge without asize(c9df9db)ButtonBadgesizes the gap in front of it fromPADDING_FROM_SIZE[size], which wasNaN. The spacer then ended up neither horizontal nor vertical and drew nothing.margin='none', like the Callout, EntityRow and FeatureCard badges) and passes the panel'ssize.ButtonBadgenow defaultssizetonormal, which is the size its tag already fell back to, so a badge without a size can no longer produceNaN.ButtonBadgecaller already passes a size.Tests:
__tests__/panelAs.test.tsxchecks that an element or component given asasrenders, thataswins overresizable, that a resizable panel still renders aResizable, and that a plain panel renders adiv.__tests__/buttonBadgeSize.test.tsxchecks that a badge without a size gets a 4px gap and that a Tier draws no unsized spacer. All of these fail on develop except the two "still renders" checks. The full suite now passes 99 files and 1230 tests.Visual: I captured every story again on this branch. Tier and Panel are byte-identical to develop: the tier's old spacer drew nothing, and no story gives a panel
as. The remaining differences against develop are the same run-to-run flakes as before. The ones that did not match an earlier capture (Icon/Basic, Testimonial/With Avatar Image) were byte-identical to develop on a re-run, and Paging/Infinite differs between runs of the same tree.Tests
__tests__/domPropLeaks.test.tsxrenders every component that leaked, using the props that leaked, and asserts two things: no non-DOM prop reached any element, and React logged no DOM-prop warning. It covers 20 cases, plus the control-group list check and two self-checks of the detector. On develop, all 19 leak cases and the list check fail. On this branch, all pass.build:test:prodare clean (the pre-push hook ran them).Visual
I captured every story with Qlip locally on develop and on this branch. 1049 of 1077 screenshots are byte-identical. I re-ran the 28 that differed on both trees: each one also changes between runs of the same tree (caret, hover/transition and load-timing states), and the Error Boundary story shows the local checkout path. No pixel change comes from this PR.
Version: 0.77.7 (one bump for the PR) (0.77.6 is taken by #696).
🤖 Generated with Claude Code