diff --git a/.changeset/search-dull-planes-prove.md b/.changeset/search-dull-planes-prove.md
new file mode 100644
index 0000000000..f185207ede
--- /dev/null
+++ b/.changeset/search-dull-planes-prove.md
@@ -0,0 +1,30 @@
+---
+'@backstage/plugin-search-react': minor
+---
+
+The `` component now accepts an optional property `disableRenderingWithNoResults` to disable rendering when no results are returned.
+Possibility to provide a custom no results component if needed through the `noResultsComponent` property.
+
+Examples:
+
+_Rendering a custom no results component_
+
+```jsx
+}
+ title="Documentation"
+ noResultsComponent={}
+/>
+```
+
+_Disable rendering when there are no results_
+
+```jsx
+}
+ title="Documentation"
+ disableRenderingWithNoResults
+/>
+```
diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md
index 5ec4124fcc..194b285e49 100644
--- a/plugins/search-react/api-report.md
+++ b/plugins/search-react/api-report.md
@@ -213,7 +213,9 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & {
export const SearchResult: (props: SearchResultProps) => JSX.Element;
// @public
-export const SearchResultApi: (props: SearchResultApiProps) => JSX.Element;
+export const SearchResultApi: (
+ props: SearchResultApiProps,
+) => JSX.Element | null;
// @public
export type SearchResultApiProps = SearchResultContextProps & {
@@ -226,11 +228,11 @@ export const SearchResultComponent: (props: SearchResultProps) => JSX.Element;
// @public
export const SearchResultContext: (
props: SearchResultContextProps,
-) => JSX.Element;
+) => JSX.Element | null;
// @public
export type SearchResultContextProps = {
- children: (state: AsyncState) => JSX.Element;
+ children: (state: AsyncState) => JSX.Element | null;
};
// @public
@@ -269,13 +271,22 @@ export type SearchResultGroupLayoutProps = ListProps & {
link?: ReactNode;
linkProps?: Partial;
filterOptions?: FilterOption[];
- renderFilterOption?: (filterOption: FilterOption) => JSX.Element;
+ renderFilterOption?: (
+ value: FilterOption,
+ index: number,
+ array: FilterOption[],
+ ) => JSX.Element | null;
filterFields?: string[];
renderFilterField?: (key: string) => JSX.Element | null;
resultItems?: SearchResult_2[];
- renderResultItem?: (resultItem: SearchResult_2) => JSX.Element;
+ renderResultItem?: (
+ value: SearchResult_2,
+ index: number,
+ array: SearchResult_2[],
+ ) => JSX.Element | null;
error?: Error;
loading?: boolean;
+ noResultsComponent?: ReactNode;
};
// @public
@@ -284,6 +295,7 @@ export type SearchResultGroupProps = Omit<
'loading' | 'error' | 'resultItems' | 'filterFields'
> & {
query: Partial;
+ disableRenderingWithNoResults?: boolean;
};
// @public
diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx
index a3e4c0e1f0..40c60c3263 100644
--- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx
+++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx
@@ -36,7 +36,7 @@ export type SearchResultContextProps = {
/**
* A child function that receives an asynchronous result set and returns a react element.
*/
- children: (state: AsyncState) => JSX.Element;
+ children: (state: AsyncState) => JSX.Element | null;
};
/**
diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx
index 3a20dec36d..fab807dd84 100644
--- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx
+++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx
@@ -269,7 +269,7 @@ export const WithFilters = () => {
);
};
-export const WithNoResults = () => {
+export const WithDefaultNoResultsComponent = () => {
const [query] = useState>({
types: ['techdocs'],
});
@@ -285,6 +285,23 @@ export const WithNoResults = () => {
);
};
+export const WithCustomNoResultsComponent = () => {
+ const [query] = useState>({
+ types: ['techdocs'],
+ });
+
+ return (
+
+ }
+ title="Documentation"
+ noResultsComponent={}
+ />
+
+ );
+};
+
const CustomResultListItem = (props: any) => {
const { icon, result } = props;
diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx
index c1b45cc433..7943dba3f7 100644
--- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx
+++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx
@@ -291,6 +291,46 @@ describe('SearchResultGroup', () => {
});
});
+ it('Does not render result group if no results returned and disableRenderingWithNoResults prop is provided', async () => {
+ query.mockResolvedValueOnce({ results: [] });
+ await renderWithEffects(
+ wrapInTestApp(
+
+ }
+ title="Documentation"
+ disableRenderingWithNoResults
+ />
+ ,
+ ),
+ );
+
+ await waitFor(() => {
+ expect(screen.queryByText('Documentation')).not.toBeInTheDocument();
+ });
+ });
+
+ it('Should render custom component when no results returned', async () => {
+ query.mockResolvedValueOnce({ results: [] });
+ await renderWithEffects(
+ wrapInTestApp(
+
+ }
+ title="Documentation"
+ noResultsComponent="No results were found"
+ />
+ ,
+ ),
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('No results were found')).toBeInTheDocument();
+ });
+ });
+
it('Shows an error panel when results rendering fails', async () => {
query.mockRejectedValueOnce(new Error());
await renderWithEffects(
diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx
index 9c55996ca7..6040dbe560 100644
--- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx
+++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx
@@ -306,7 +306,11 @@ export type SearchResultGroupLayoutProps = ListProps & {
* Function to customize how filter options are rendered.
* @remarks Defaults to a menu item where its value and label bounds to the option string.
*/
- renderFilterOption?: (filterOption: FilterOption) => JSX.Element;
+ renderFilterOption?: (
+ value: FilterOption,
+ index: number,
+ array: FilterOption[],
+ ) => JSX.Element | null;
/**
* A list of search filter keys, also known as filter field names.
*/
@@ -322,7 +326,11 @@ export type SearchResultGroupLayoutProps = ListProps & {
/**
* Function to customize how result items are rendered.
*/
- renderResultItem?: (resultItem: SearchResult) => JSX.Element;
+ renderResultItem?: (
+ value: SearchResult,
+ index: number,
+ array: SearchResult[],
+ ) => JSX.Element | null;
/**
* If defined, will render a default error panel.
*/
@@ -331,6 +339,10 @@ export type SearchResultGroupLayoutProps = ListProps & {
* If defined, will render a default loading progress.
*/
loading?: boolean;
+ /**
+ * Optional component to render when no results. Default to component.
+ */
+ noResultsComponent?: ReactNode;
};
/**
@@ -350,14 +362,31 @@ export function SearchResultGroupLayout(
icon,
title,
titleProps = {},
- link,
+ link = (
+ <>
+ See all
+
+ >
+ ),
linkProps = {},
filterOptions,
- renderFilterOption,
+ renderFilterOption = filterOption => (
+
+ ),
filterFields,
renderFilterField,
resultItems,
- renderResultItem,
+ renderResultItem = resultItem => (
+
+ ),
+ noResultsComponent = (
+
+ ),
...rest
} = props;
@@ -401,30 +430,14 @@ export function SearchResultGroupLayout(
onClick={handleClose}
keepMounted
>
- {filterOptions.map(filterOption =>
- renderFilterOption ? (
- renderFilterOption(filterOption)
- ) : (
-
- ),
- )}
+ {filterOptions.map(renderFilterOption)}
) : null}
{filterFields?.map(
filterField => renderFilterField?.(filterField) ?? null,
)}
- {link ?? (
- <>
- See all
-
- >
- )}
+ {link}
{loading ? : null}
@@ -435,12 +448,10 @@ export function SearchResultGroupLayout(
/>
) : null}
{!loading && !error && resultItems?.length
- ? resultItems.map(resultItem => renderResultItem?.(resultItem) ?? null)
+ ? resultItems.map(renderResultItem)
: null}
{!loading && !error && !resultItems?.length ? (
-
-
-
+ {noResultsComponent}
) : null}
);
@@ -458,6 +469,10 @@ export type SearchResultGroupProps = Omit<
* A search query used for requesting the results to be grouped.
*/
query: Partial;
+ /**
+ * Optional property to provide if component should not render the group when no results are found.
+ */
+ disableRenderingWithNoResults?: boolean;
};
/**
@@ -471,9 +486,7 @@ export function SearchResultGroup(
const {
query,
linkProps = {},
- renderResultItem = ({ document }) => (
-
- ),
+ disableRenderingWithNoResults,
...rest
} = props;
@@ -495,17 +508,22 @@ export function SearchResultGroup(
}}
>
- {({ loading, error, value }) => (
-
- )}
+ {({ loading, error, value }) => {
+ if (!value?.results?.length && disableRenderingWithNoResults) {
+ return null;
+ }
+
+ return (
+
+ );
+ }}
);