From 7f580e7b97cc8e116874bf7e175d986da4e37499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Re=C3=A9?= Date: Wed, 29 Jul 2026 15:24:03 +0200 Subject: [PATCH] Fix Segment children prop-types warning for the Toolbar portal (#97) Client-side navigation to the search page warned 'Invalid prop children supplied to Segment, expected a ReactNode': createPortal returns a ReactPortal, which renders fine but is not recognized by the prop-types node checker. Render the portal as a sibling of the Segment (a portal renders into its DOM target regardless of tree position; Segment provides no context or handlers the subtree would lose). The indentation-only changes are prettier reindent after wrapping in a fragment - review with 'git diff -w' (7 insertions, 1 deletion). --- frontend/packages/volto-solr/news/97.bugfix | 1 + .../theme/SolrSearch/SolrSearch.jsx | 352 +++++++++--------- 2 files changed, 180 insertions(+), 173 deletions(-) create mode 100644 frontend/packages/volto-solr/news/97.bugfix diff --git a/frontend/packages/volto-solr/news/97.bugfix b/frontend/packages/volto-solr/news/97.bugfix new file mode 100644 index 00000000..9f25d1e2 --- /dev/null +++ b/frontend/packages/volto-solr/news/97.bugfix @@ -0,0 +1 @@ +Fix React prop-types warning on client-side navigation to the search page: the Toolbar portal is rendered as a sibling of the Segment instead of a child (prop-types does not recognize ReactPortal as a node). @reebalazs diff --git a/frontend/packages/volto-solr/src/components/theme/SolrSearch/SolrSearch.jsx b/frontend/packages/volto-solr/src/components/theme/SolrSearch/SolrSearch.jsx index cf10c36c..8623b54c 100644 --- a/frontend/packages/volto-solr/src/components/theme/SolrSearch/SolrSearch.jsx +++ b/frontend/packages/volto-solr/src/components/theme/SolrSearch/SolrSearch.jsx @@ -355,186 +355,192 @@ class SolrSearch extends Component { this.props.contentTypeSearchResultViews[contentType] || this.props.contentTypeSearchResultDefaultView; return ( - - - - {this.props.showSearchInput ? ( -
-
-
- - this.setState({ searchword: e.target.value }) - } - onSubmit={this.onSubmit} - /> - -
-
-
- ) : null} - {/* RAG: TESTING - debug toggle + answer area (real UX comes + <> + + + + {this.props.showSearchInput ? ( +
+
+
+ + this.setState({ searchword: e.target.value }) + } + onSubmit={this.onSubmit} + /> + +
+
+
+ ) : null} + {/* RAG: TESTING - debug toggle + answer area (real UX comes from the search modal ticket, styling intentionally raw). Rendered only when the backend reports the feature available - an unconfigured site shows the classic search, untouched. */} - {this.props.ragAvailable ? ( -
- this.setUseAI(checked)} + {this.props.ragAvailable ? ( +
+ this.setUseAI(checked)} + /> + {this.state.useAI && + (this.props.rag.loading || this.props.rag.loaded) ? ( +
+ {this.props.rag.loading ?

Thinking…

: null} + {this.props.rag.error ? ( +

{this.props.rag.error}

+ ) : null} + {this.props.rag.answer ? ( +

+ {this.props.rag.answer} +

+ ) : null} + {this.props.rag.loaded && + !this.props.rag.answer && + !this.props.rag.error ? ( +

No answer — no matching documents found.

+ ) : null} + {(this.props.rag.sources || []).length > 0 ? ( + + ) : null} +
+ ) : null} +
+ ) : null} + {this.state.allowLocal && + getPathPrefix(this.props.history.location) !== undefined ? ( + this.setLocal(checked)} + checked={this.state.local} /> - {this.state.useAI && - (this.props.rag.loading || this.props.rag.loaded) ? ( -
- {this.props.rag.loading ?

Thinking…

: null} - {this.props.rag.error ? ( -

{this.props.rag.error}

- ) : null} - {this.props.rag.answer ? ( -

- {this.props.rag.answer} -

- ) : null} - {this.props.rag.loaded && - !this.props.rag.answer && - !this.props.rag.error ? ( -

No answer — no matching documents found.

+ ) : null} + + this.setGroupSelect(groupSelect)} + facetGroups={this.props.facetGroups} + /> + +
+
+ {this.props.total > 0 ? ( +
+ { + this.setState({ layout: value }); + }} + /> + { + this.onSortChange(selectedOption, order); + }} + /> +
) : null} - {(this.props.rag.sources || []).length > 0 ? ( -
+
+ + + + + + +
+
+ {this.props.items?.map((item, index) => ( +
+ {createElement(resultTypeMapper(item['@type']), { + key: item['@id'], + item, + layout: this.state.layout, + })} +
))} - - ) : null} +
+ {this.props.batching && + this.props.total / settings.defaultPageSize > 1 && ( +
+ + ), + icon: true, + 'aria-disabled': !this.props.batching.prev, + className: !this.props.batching.prev + ? 'disabled' + : null, + }} + nextItem={{ + content: ( + + ), + icon: true, + 'aria-disabled': !this.props.batching.next, + className: !this.props.batching.next + ? 'disabled' + : null, + }} + /> +
+ )} +
- ) : null} -
- ) : null} - {this.state.allowLocal && - getPathPrefix(this.props.history.location) !== undefined ? ( - this.setLocal(checked)} - checked={this.state.local} - /> - ) : null} - - this.setGroupSelect(groupSelect)} - facetGroups={this.props.facetGroups} - /> - -
-
- {this.props.total > 0 ? ( -
- { - this.setState({ layout: value }); - }} - /> - { - this.onSortChange(selectedOption, order); - }} - /> -
- ) : null} -
-
- - - - - - -
-
- {this.props.items?.map((item, index) => ( -
- {createElement(resultTypeMapper(item['@type']), { - key: item['@id'], - item, - layout: this.state.layout, - })} -
- ))} -
- {this.props.batching && - this.props.total / settings.defaultPageSize > 1 && ( -
- - ), - icon: true, - 'aria-disabled': !this.props.batching.prev, - className: !this.props.batching.prev - ? 'disabled' - : null, - }} - nextItem={{ - content: ( - - ), - icon: true, - 'aria-disabled': !this.props.batching.next, - className: !this.props.batching.next - ? 'disabled' - : null, - }} - /> -
- )} -
-
-
-
- + + + + + {/* The Toolbar portal is a sibling of the Segment, not a child: + createPortal returns a ReactPortal, which renders fine but is + not recognized by the prop-types `node` checker, so Segment's + propTypes would warn about invalid children (#97). */} {this.state.isClient && createPortal( , document.getElementById('toolbar'), )} - + ); } }