Skip to content

FilterPanel nests a <button> inside its own trigger <button> when a facet has active selections #199

Description

@einari

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

  1. Render a FilterPanel with any FilterDefinition (string or number type).
  2. Select at least one option (or set a range/custom value) for that facet.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions