What happened
Whenever a FilterPanel facet (FilterDefinition) has at least one active selection, the panel's per-facet trigger renders an invalid DOM nesting: a <button> (the "clear filter" × control) inside another <button> (the facet's expand/collapse trigger). React logs this at runtime:
In HTML, <button> cannot be a descendant of <button>.
This will cause a hydration error.
<button> cannot contain a nested <button>.
Where it comes from
Source/JavaScript/Filter/FilterPanel.tsx (compiled output inspected at dist/esm/Filter/FilterPanel.js), inside the per-filter render:
<button type="button" className="pv-filter-trigger" onClick={() => onExpandedFilterChange(...)}>
<span className="pv-filter-label">{filter.label}</span>
<span className="pv-filter-trigger-meta">
{!isNumeric && !isCustom && selections.size > 0 && (
<>
<span className="pv-filter-count">{selections.size}</span>
<button
type="button"
className="pv-filter-clear-header"
title="Clear filter"
onClick={(e) => { e.stopPropagation(); onFilterClear(filter.key); }}
aria-label={clearFilterAriaLabel}
>×</button>
</>
)}
...
</span>
</button>
The same pattern repeats for the numeric-range and custom-value "clear range"/"clear filter" buttons a few lines below.
Repro
- Render a
FilterPanel with any FilterDefinition (string or number type).
- Select at least one option (or set a range/custom value) for that facet.
- Open the browser console.
Expected: no console errors, valid DOM.
Observed: a React DOM-nesting error/warning for every facet that currently has an active selection, every time the panel (re)renders.
Why it matters
This is not cosmetic - <button> inside <button> is invalid HTML. Browsers auto-close/hoist the inner button out of the outer one during parsing, which can silently change hit-testing/focus order from what the JSX intends, and it will produce a real hydration mismatch for any consumer that server-renders this component. It also means every app currently using FilterPanel with an active filter has this warning permanently in its console, masking real issues.
Suggested direction
The clear-header control does not need to be a <button> nested inside the trigger <button> - a <span role="button" tabIndex={0} onClick=... onKeyDown=...> (or moving the clear control outside the trigger button entirely, e.g. as a sibling positioned via CSS) would avoid the nested interactive-element violation while keeping the same visual/interaction model. Leaving the actual fix to whoever owns the component.
What happened
Whenever a
FilterPanelfacet (FilterDefinition) has at least one active selection, the panel's per-facet trigger renders an invalid DOM nesting: a<button>(the "clear filter" × control) inside another<button>(the facet's expand/collapse trigger). React logs this at runtime:Where it comes from
Source/JavaScript/Filter/FilterPanel.tsx(compiled output inspected atdist/esm/Filter/FilterPanel.js), inside the per-filter render:The same pattern repeats for the numeric-range and custom-value "clear range"/"clear filter" buttons a few lines below.
Repro
FilterPanelwith anyFilterDefinition(string or number type).Expected: no console errors, valid DOM.
Observed: a React DOM-nesting error/warning for every facet that currently has an active selection, every time the panel (re)renders.
Why it matters
This is not cosmetic -
<button>inside<button>is invalid HTML. Browsers auto-close/hoist the inner button out of the outer one during parsing, which can silently change hit-testing/focus order from what the JSX intends, and it will produce a real hydration mismatch for any consumer that server-renders this component. It also means every app currently usingFilterPanelwith an active filter has this warning permanently in its console, masking real issues.Suggested direction
The clear-header control does not need to be a
<button>nested inside the trigger<button>- a<span role="button" tabIndex={0} onClick=... onKeyDown=...>(or moving the clear control outside the trigger button entirely, e.g. as a sibling positioned via CSS) would avoid the nested interactive-element violation while keeping the same visual/interaction model. Leaving the actual fix to whoever owns the component.