Skip to content

fix: no reqore styling prop reaches a component rendered as; descriptions take block content — 0.77.10 - #709

Closed
davidnich wants to merge 4 commits into
developfrom
bugfix/dom-leaks-and-block-descriptions
Closed

davidnich wants to merge 4 commits into
developfrom
bugfix/dom-leaks-and-block-descriptions

Conversation

@davidnich

@davidnich davidnich commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #697 (0.77.7): the remaining styling-prop leaks into a component rendered as another, and descriptions that could not hold block content.

Problems

  1. ReqoreButton as={Link} handed the router link reqore's styling props. <ReqoreControlGroup vertical stack><ReqoreButton as={Link} to='/x' fluid compact /></ReqoreControlGroup> gave the link fluid, compact, flat, animate, maxWidth, alignSelf, active, readOnly, tooltip, description, the group's stack-position flags (isChild, isFirst, childId, …) and more — 36 props, which the link wrote onto its <a> (React warnings, size="small" attributes). Breadcrumbs and menu items rendered as a link inherit it.
  2. A resizable + transparent ReqorePanel handed re-resizable transparent, which it spreads onto its wrapper div.
  3. description was drawn in a <p> in ReqoreEntityRow, ReqoreSeverityRow, ReqoreCallout, ReqoreFeatureCard and ReqoreTier (and a ReqoreTestimonial's quote). Consumers pass ReqoreP / ReqoreControlGroup there (qorus-ide: ~14 call sites), which a <p> cannot hold: React warns (validateDOMNesting: <div> cannot appear as a descendant of <p>), and the browser's parser closes the paragraph at the first block, moving the content out of it and out of its styling.

Root causes

  • src/components/Button/index.tsx:341 (develop): shouldForwardProp: omitStyleProps('fill', 'color', 'wrap'). A DOM tag still filters through isPropValid, but a component target receives every prop that is not named, and the button names three.
  • src/components/Panel/index.tsx:587-596: the panel's hand list of eight names lacks transparent (and disabled, minimal, …). It went stale as props were added — the same failure mode as the button's.
  • src/components/{SeverityRow:365, Callout:423, EntityRow:434, FeatureCard:376}/index.tsx, Tier/index.tsx:154, Testimonial/index.tsx:350: the description is a ReqoreP, i.e. a <p>. The root of each of these components is the effect's span, which may not hold blocks either.

Fixes

  • Derived omit lists, checked by the compiler (src/helpers/styled.ts). TReqoreStylePropKeys<TProps, TTargetProps> is every key of a component's prop interface that is not an attribute/prop of what it renders; listReqoreStyleProps<…>({ … }) takes a Record of exactly those keys. A prop added to the interface and not to the record fails the build instead of leaking, so the lists cannot go stale again.
    • REQORE_BUTTON_STYLE_PROPS — against IReqoreButtonStyle minus <button> + <a> attributes (disabled, type, href, … still reach the element). The button also keeps back REQORE_CONTROL_GROUP_CHILD_PROPS.
    • REQORE_PANEL_STYLE_PROPS — against IStyledPanel minus <div> attributes and re-resizable's ResizableProps, so a Resizable panel still receives size, enable, minWidth, handles.
    • REQORE_TAG_STYLE_PROPS, REQORE_TEXTAREA_STYLE_PROPS (the latter keeps every <textarea> attribute and Slate's own renderElement / renderLeaf / decorate forwarded).
  • Descriptions are a ReqoreP rendered as='div'. styled-components names classes after the CSS they generate, and the div gets the same classes as the <p>, so a plain-text description is drawn identically. The root of each of these six components is rendered as a div (it is display: flex/grid in every case, so that changes nothing visible). Each prop's doc now says that it takes block content.

Sweep of every as-capable component (and every description)

Each was rendered as a component that records what it receives:

Component Leaked to its as target Fix
ReqoreTag asBadge, labelKey, fixed, minimal, rounded, size, interactive, removable, tooltip REQORE_TAG_STYLE_PROPS + control-group flags
ReqoreP _size, block, tooltip omits _size + control-group flags
StyledTextEffect (P, Span, Link, Callout content) block, inline omits both
ReqoreTabsListItem theme, active, activeColor, activeTabMarker, disabled, fixed, intent, padded, size, vertical omits them
ReqoreTextarea as Slate's editable _size, flat, minimal, rounded REQORE_TEXTAREA_STYLE_PROPS
ReqoreIcon wrapperElement={animated.span} size, customTheme + control-group flags omits control-group flags
ReqoreBreadcrumbsItem interactive (not a button prop) dropped
ReqoreTooltipComponent handed tooltip to every non-DOM component never forwards it

Found on the way: the Timeline item wrote disabled onto its <li> (now omitted; the item sets aria-disabled), and the Textarea wrapper width/height onto its <div>.

Checked and fine: ReqoreLink (fixed in #697), ReqoreInput, ReqoreEffect / ReqoreTextEffect, the Menu and Drawer Resizable, Message / Notification animated.div, Popover's wrapperTag. A panel action's as element is still handed fixed and customTheme — that is the action API (the element is expected to be a reqore control), not a leak. ReqoreButton's and ReqoreCheckbox's description stay phrasing content: they render inside a <button> / <label>, which may only hold phrasing content. ReqoreEmptyState already draws a non-string description in a div.

Not in this PR: many styled divs across the library still receive the Reqore size prop as an HTML size attribute (<div size="normal">). React does not warn about it (it is a valid attribute name) and it changes nothing visible; it is a separate, library-wide sweep.

Tests

  • __tests__/domPropLeaks.test.tsx: 11 new cases — the button as a router link in a vertical stack group (with every styling prop), breadcrumbs and menu items as links, a tag and a paragraph as links in control groups, a transparent panel as a link, a resizable transparent panel, resizable panels in a stack group, a tabs item as a link, a textarea as an editor component and animated icons in a control group; the existing timeline case now also catches disabled on its <li>. The stub components mark their element, and the detector also flags a Reqore size on it (a valid HTML attribute name, which isPropValid lets through). Plus negative tests on each style list (it never holds disabled / href / type / value / readOnly / re-resizable's size / enable / minWidth …) and a detector self-test. All 20 new tests fail on develop.
  • __tests__/blockDescriptions.test.tsx: every component given block content draws it in a div, keeps it inside the description, has no block inside a <p> or <span>, and logs no validateDOMNesting warning; a plain-text description is the container's only content; a ReqoreP as='div' gets exactly the classes a <p> does; a detector self-test. The block-content cases fail on develop.
  • Full unit suite: 100 files, 1266 tests pass; tsc (test + prod), eslint clean. DeepScan: 0 new issues (the Tier description's inline style, re-reported when its element moved lines, is now a module constant).

Visual

Captured locally (headless, one story file at a time) for the 20 affected story files — Button, Panel, Tag, TagGroup, Paragraph, Callout, EntityRow, SeverityRow, FeatureCard, Tier, Testimonial, Timeline, Tabs, TextArea, Icon, Breadcrumbs, Menu, Effect, Span, Link — on develop and on this branch: 354 snapshots, 353 byte-identical. The one that differs, Form Textarea / Form Attributes Mobile, differs in 7 anti-aliased pixels of the focus outline's corner by 1–2 colour levels. Running the branch twice gives both the develop hash and the branch hash, so it is capture noise, not this change. Plain-text descriptions are pixel-identical, as intended.

Version: 0.77.10 (0.77.8 is #696's, 0.77.9 is #708's).

🤖 Generated with Claude Code

…e's styling props

ReqoreButton rendered `as={Link}` handed the router link every styling
prop it had: `fluid`, `compact`, `flat`, `animate`, `maxWidth`,
`alignSelf`, `tooltip`, the stack-position flags a ControlGroup clones
onto its children, ... and the link wrote them onto its `<a>`. A
resizable, transparent ReqorePanel handed re-resizable `transparent`,
which it spread onto its wrapper div.

Both come from the same gap: a styled component's `shouldForwardProp`
named a few props by hand (Button: fill/color/wrap; Panel: eight), and
a component target receives every prop not named. The hand lists went
stale as props were added.

The styling props of Button, Panel, Tag and Textarea are now listed by
listReqoreStyleProps from a record typed against the component's prop
interface minus what its element takes (TReqoreStylePropKeys), so a new
prop that is not added fails the build instead of leaking. Panel keeps
re-resizable's own props (`size`, `enable`, ...) forwarded.

The sweep of every `as`-capable component found the same leak in Tag
(asBadge, labelKey, size, ...), Paragraph (`_size`, `block`), the
TextEffect base (`block`, `inline`), the Tabs list item (`theme`,
`active`, `size`, ...), the Textarea given Slate's editable (`flat`,
`minimal`, `rounded`, `_size`), the Icon's animated wrapper (`size`,
control-group flags) and the Breadcrumbs item (`interactive`). The
tooltip component no longer hands `tooltip` to any component; the
Timeline item no longer writes `disabled` onto its `li` (it sets
`aria-disabled`), nor the Textarea wrapper `width`/`height` onto its
div.
ReqoreEntityRow, ReqoreSeverityRow, ReqoreCallout, ReqoreFeatureCard and
ReqoreTier drew `description` (and ReqoreTestimonial its `quote`) in a
ReqoreP, a `<p>`. Consumers pass paragraphs and control groups there,
which a `<p>` cannot hold: React warned (validateDOMNesting) and the
browser's parser closes the paragraph at the first block, moving the
content out of it and its styling.

The description is now a ReqoreP rendered as a `div`: the same styled
classes, so a plain-text description looks exactly as before. The root
of each of these components, the effect's `span`, is rendered as a
`div` too, since a span may not hold blocks either. The prop docs say
that a description takes block content.
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

📦 Prerelease published for this PR:

@qoretechnologies/reqore@0.77.10-pr.709.g1ab099e

Install it in a consumer (updates package.json + lockfile):

yarn add @qoretechnologies/reqore@0.77.10-pr.709.g1ab099e

Published under the pr dist-tag — latest/beta are untouched. Repoint consumers to the released version once this PR merges.

@qlip-qore-technologies

qlip-qore-technologies Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

qlip visual regression

✅ No visual changes.

Review build 20261007-101620 in the dashboard →

…r render

DeepScan REACT_INEFFICIENT_PURE_COMPONENT_PROP: the inline style handed to
the memoized ReqoreP defeated its memoization. It was there before; the
description's element moving to its own lines made DeepScan report it as new.
@Foxhoundn

Copy link
Copy Markdown
Member

Review: the core fix is right; a few things to do before merging

The compiler-checked omit lists, the six descriptions drawn as divs and the tooltip change look correct. CI is green, and a simulated merge with #696 passes the full unit suite (108 files, 1,400 tests). What needs to be done:

After #696 lands (it goes in first)

  1. Version: feat: redesign knobs — fonts (buttons/inputs), transparent layout, Statistic valueAs/countUp, Tier price detail + modern appearance, ReqoreTierGroup (3D stack on phones), Panel media, FeatureCard actions, Timeline states, SegmentedControl item props — 0.78.0 #696 is 0.78.0 and fix: focus lists, rich-text sync, table header scroll + dragToScroll, panel floating actions, no styling prop reaches the DOM, descriptions take block content — 0.78.5 #708 becomes 0.78.1, so this becomes 0.78.2. The package.json version line conflicts with feat: redesign knobs — fonts (buttons/inputs), transparent layout, Statistic valueAs/countUp, Tier price detail + modern appearance, ReqoreTierGroup (3D stack on phones), Panel media, FeatureCard actions, Timeline states, SegmentedControl item props — 0.78.0 #696. The description's "0.77.8 is feat: redesign knobs — fonts (buttons/inputs), transparent layout, Statistic valueAs/countUp, Tier price detail + modern appearance, ReqoreTierGroup (3D stack on phones), Panel media, FeatureCard actions, Timeline states, SegmentedControl item props — 0.78.0 #696's" is stale too.
  2. The type check fails on the merged tree. feat: redesign knobs — fonts (buttons/inputs), transparent layout, Statistic valueAs/countUp, Tier price detail + modern appearance, ReqoreTierGroup (3D stack on phones), Panel media, FeatureCard actions, Timeline states, SegmentedControl item props — 0.78.0 #696 adds media, mediaAlt, mediaPosition, mediaAspectRatio and mediaProps to the panel. REQORE_PANEL_STYLE_PROPS (Panel/index.tsx L593) needs those five keys, or tsc fails with TS2345. That is the only error on the merged tree. ReqorePanel already destructures them, so nothing else changes.
  3. The modern tier's description. feat: redesign knobs — fonts (buttons/inputs), transparent layout, Statistic valueAs/countUp, Tier price detail + modern appearance, ReqoreTierGroup (3D stack on phones), Panel media, FeatureCard actions, Timeline states, SegmentedControl item props — 0.78.0 #696 adds appearance='modern' to ReqoreTier, which draws description in a ReqoreP (Tier/index.tsx L659-L665 on #696). It needs the same change as the classic description here, plus a modern case in blockDescriptions.test.tsx. Otherwise block content in a modern tier is a div inside a p, and React warns validateDOMNesting.

Gaps in the sweep (they existed before, but are cheap to fix here)

  1. Textarea: StyledTextarea (Textarea/index.tsx L164-L165) doesn't omit REQORE_CONTROL_GROUP_CHILD_PROPS, unlike Button, Tag, P, Icon, Slider and Panel.
    • In a fill group, fill reaches the <textarea>, and React warns about a non-boolean attribute.
    • A textarea rendered as a component in a stack group hands that component fill, stack, spaceBetween, isChild, childId, childrenCount and the other stack flags.
    • Fix: omitStyleProps(...REQORE_TEXTAREA_STYLE_PROPS, ...REQORE_CONTROL_GROUP_CHILD_PROPS), plus a leak test.
  2. The five roots this PR turns into divs still get fill and size inside a fill group: Callout L375, EntityRow L382, SeverityRow L324, FeatureCard L323 and Testimonial L305. The same filter fixes them.
  3. Callout children with blocks still sit in a span. StyledCalloutContent (Callout L297, used at L445 and L458) renders a span, and the old prop doc told consumers to pass blocks as children. Give both uses as='div', and add a test with block children.

Worth deciding

  1. Plain-text descriptions also change from p to div (Callout L427, EntityRow L443, SeverityRow L374, FeatureCard L385, Testimonial L358, Tier L163).
    • Inside Reqore this is pixel-identical, but consumer CSS that targets p stops matching. The landing page's "How it works" section, for example, styles li p.
    • Keeping p for string or number descriptions and using a div only for elements keeps plain text exactly as before. EmptyState, DescriptionList and Timeline already do this.
  2. Document the behaviour change in COMPONENTS.md. The omit lists are built against each component's default element, so a real attribute is now dropped when as is another DOM tag that supports it.
    • For example, <ReqorePanel as='fieldset' disabled> no longer disables its fields, and <ReqoreTag as='button' disabled> loses disabled.
    • Some attributes also leave the DOM: disabled on the panel div, the tab wrapper and the timeline li; readonly on read-only or loading buttons; size on tag spans.
    • Nothing looks different, and nothing in qorus-ide or the landing page relies on them.
  3. COMPONENTS.md isn't updated for the new block descriptions; the shared frontend rules ask for it.

Tests still missing: a textarea in a control group, the description components in a fill group, callout block children, the modern tier, and a panel or tag rendered as another DOM tag.

@davidnich

Copy link
Copy Markdown
Member Author

These commits moved to #708 (one open PR for this work): 3d5c4a3, e0d3bb2 and 0669d75 are cherry-picks of b0eab1d, da78b7a and 1ab099e. 9180338 adapts them to #696 (the panel's media props, the modern Tier's description). This PR's version bump (0.77.10) was dropped; #708 ships as 0.78.1. #708 also carries the library-wide sweep of styling props written as HTML attributes (7f6bd6c). Closing without merging.

@davidnich davidnich closed this Oct 7, 2026
davidnich added a commit that referenced this pull request Oct 7, 2026
…he card and row surfaces; a state attribute reaches the element a component is rendered as

From Foxhoundn's review of #709:

- A description that is text (a string or a number) is a `<p>` again,
  exactly as before, so consumer CSS aimed at `p` still matches it; only
  other content is drawn in a `div` (getReqoreTextElement). Callout,
  EntityRow, SeverityRow, FeatureCard, Tier (both appearances) and the
  Testimonial's quote.
- A callout's children are drawn in a `div`: they may be blocks, which
  the effect's `span` could not hold.
- The textarea, and the callout, entity-row, severity-row, feature-card
  and testimonial surfaces, keep back the flags a ReqoreControlGroup
  hands its children, and the surfaces their own `disabled`,
  `radiusSize`, `inheritCustomTheme` (and the callout's
  `accentPosition`, the callout's and card's `interactive`), which a
  component they are rendered `as` received.
- A component's styling-prop list is built against its default
  element. Rendered `as` an element that has the state attribute it
  names (`disabled`, `readOnly`, `checked`, `selected`), the attribute
  is that element's: `<ReqorePanel as='fieldset' disabled>` disables its
  fields again, `<ReqoreTag as='button' disabled>` is a disabled button.
- COMPONENTS.md: block descriptions, and what reaches the DOM now.

Tests: plain text is a `<p>`; callout block children; a textarea in a
filling stack group; the five surfaces in a filling stack group and
rendered as a router link with every surface prop set; a panel as a
fieldset and a tag as a button.
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.

2 participants