diff --git a/.changeset/search-en-jul-hemma.md b/.changeset/search-en-jul-hemma.md
new file mode 100644
index 0000000000..9fae9fcaa5
--- /dev/null
+++ b/.changeset/search-en-jul-hemma.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-search': patch
+---
+
+Introduces a `` variant, which operates on the same part of a search query as the existing ``, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs).
+
+Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtype--accordion) to see how it can be used.
diff --git a/.changeset/search-over-tysta-skogor.md b/.changeset/search-over-tysta-skogor.md
new file mode 100644
index 0000000000..5398ae7f5f
--- /dev/null
+++ b/.changeset/search-over-tysta-skogor.md
@@ -0,0 +1,58 @@
+---
+'@backstage/create-app': patch
+---
+
+The `` filter in the composed `SearchPage.tsx` was replaced with the `` variant.
+
+This is an entirely optional change; if you wish to display a control surface for search `types` as a single-select accordion (as opposed to the current multi-select of checkboxes), you can make the following (or similar) changes to your search page layout:
+
+```diff
+--- a/packages/app/src/components/search/SearchPage.tsx
++++ b/packages/app/src/components/search/SearchPage.tsx
+@@ -11,7 +11,7 @@ import {
+ SearchType,
+ DefaultResultListItem,
+ } from '@backstage/plugin-search';
+-import { Content, Header, Page } from '@backstage/core-components';
++import { CatalogIcon, Content, DocsIcon, Header, Page } from '@backstage/core-components';
+
+ const useStyles = makeStyles((theme: Theme) => ({
+ bar: {
+@@ -19,6 +19,7 @@ const useStyles = makeStyles((theme: Theme) => ({
+ },
+ filters: {
+ padding: theme.spacing(2),
++ marginTop: theme.spacing(2),
+ },
+ filter: {
+ '& + &': {
+@@ -41,12 +42,23 @@ const SearchPage = () => {
+
+
+
++ ,
++ },
++ {
++ value: 'techdocs',
++ name: 'Documentation',
++ icon: ,
++ },
++ ]}
++ />
+
+-
+ ({
},
filters: {
padding: theme.spacing(2),
+ marginTop: theme.spacing(2),
},
}));
@@ -55,12 +63,23 @@ const SearchPage = () => {
+ ,
+ },
+ {
+ value: 'techdocs',
+ name: 'Documentation',
+ icon: ,
+ },
+ ]}
+ />
-
({
bar: {
@@ -19,6 +25,7 @@ const useStyles = makeStyles((theme: Theme) => ({
},
filters: {
padding: theme.spacing(2),
+ marginTop: theme.spacing(2),
},
filter: {
'& + &': {
@@ -41,12 +48,23 @@ const SearchPage = () => {
+ ,
+ },
+ {
+ value: 'techdocs',
+ name: 'Documentation',
+ icon: ,
+ },
+ ]}
+ />
-
JSX.Element;
-// Warning: (ae-forgotten-export) The symbol "SearchTypeProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "SearchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export const SearchType: ({
- values,
- className,
- name,
- defaultValue,
-}: SearchTypeProps) => JSX.Element;
+export const SearchType: {
+ (props: SearchTypeProps): JSX.Element;
+ Accordion(props: SearchTypeAccordionProps): JSX.Element;
+};
+
+// @public (undocumented)
+export type SearchTypeAccordionProps = {
+ name: string;
+ types: Array<{
+ value: string;
+ name: string;
+ icon: JSX.Element;
+ }>;
+ defaultValue?: string;
+};
+
+// @public (undocumented)
+export type SearchTypeProps = {
+ className?: string;
+ name: string;
+ values?: string[];
+ defaultValue?: string[] | string | null;
+};
// Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx
index baf00767db..f5fa6689ef 100644
--- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx
+++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx
@@ -25,6 +25,17 @@ import { DefaultResultListItem } from './DefaultResultListItem';
export default {
title: 'Plugins/Search/DefaultResultListItem',
component: DefaultResultListItem,
+ decorators: [
+ (Story: () => JSX.Element) => (
+
+
+
+
+
+
+
+ ),
+ ],
};
const mockSearchResult = {
@@ -35,54 +46,34 @@ const mockSearchResult = {
};
export const Default = () => {
- return (
-
-
-
-
-
-
-
- );
+ return ;
};
export const WithIcon = () => {
return (
-
-
-
- }
- />
-
-
-
+ }
+ />
);
};
export const WithSecondaryAction = () => {
return (
-
-
-
- }
- style={{ textTransform: 'lowercase' }}
- >
- {mockSearchResult.owner}
-
- }
- />
-
-
-
+ }
+ style={{ textTransform: 'lowercase' }}
+ >
+ {mockSearchResult.owner}
+
+ }
+ />
);
};
diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx
index 8d81e15e56..3887df814f 100644
--- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx
+++ b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx
@@ -15,88 +15,57 @@
*/
import { Grid, makeStyles, Paper } from '@material-ui/core';
-import React from 'react';
-import { MemoryRouter } from 'react-router';
-import { SearchContext } from '../SearchContext';
+import React, { ComponentType } from 'react';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchBar } from './SearchBar';
export default {
title: 'Plugins/Search/SearchBar',
component: SearchBar,
-};
-
-const defaultValue = {
- term: '',
- setTerm: () => {},
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+
+
+ ),
+ ],
};
export const Default = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
export const CustomPlaceholder = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
export const Focused = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
- {/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */}
- {/* eslint-disable-next-line jsx-a11y/no-autofocus */}
-
-
-
-
-
-
+
+ {/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */}
+ {/* eslint-disable-next-line jsx-a11y/no-autofocus */}
+
+
);
};
export const WithoutClearButton = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
@@ -113,17 +82,8 @@ const useStyles = makeStyles({
export const CustomStyles = () => {
const classes = useStyles();
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx
new file mode 100644
index 0000000000..e969e0d8cc
--- /dev/null
+++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ApiProvider } from '@backstage/core-app-api';
+import { SearchResultSet } from '@backstage/search-common';
+import { TestApiRegistry } from '@backstage/test-utils';
+import React, { ComponentProps } from 'react';
+import { searchApiRef } from '../../apis';
+import { SearchContextProvider as RealSearchContextProvider } from './SearchContext';
+
+type QueryResultProps = {
+ mockedResults?: SearchResultSet;
+};
+
+/**
+ * Utility context provider only for use in Storybook stories. You should use
+ * the real `` exported by `@backstage/plugin-search` in
+ * your app instead of this! In some cases (like the search page) it may
+ * already be provided on your behalf.
+ */
+export const SearchContextProvider = (
+ props: ComponentProps & QueryResultProps,
+) => {
+ const { mockedResults, ...contextProps } = props;
+ const query: any = () => Promise.resolve(mockedResults || {});
+ const apiRegistry = TestApiRegistry.from([searchApiRef, { query }]);
+
+ return (
+
+
+
+ );
+};
diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx
index c98c1106e6..0ea1a66bf9 100644
--- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx
+++ b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx
@@ -15,56 +15,44 @@
*/
import { Grid, Paper } from '@material-ui/core';
-import React from 'react';
-import { MemoryRouter } from 'react-router';
-import { SearchContext } from '../SearchContext';
+import React, { ComponentType } from 'react';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchFilter } from './SearchFilter';
export default {
title: 'Plugins/Search/SearchFilter',
component: SearchFilter,
-};
-
-const defaultValue = {
- filters: {},
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+
+
+ ),
+ ],
};
export const CheckBoxFilter = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
export const SelectFilter = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx
index c0a774ab46..bfea548134 100644
--- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx
+++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx
@@ -14,62 +14,52 @@
* limitations under the License.
*/
-import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { wrapInTestApp } from '@backstage/test-utils';
import { Button } from '@material-ui/core';
import React, { ComponentType } from 'react';
-import { searchApiRef } from '../../apis';
import { rootRouteRef } from '../../plugin';
-import { SearchContextProvider, useSearch } from '../SearchContext';
+import { useSearch } from '../SearchContext';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchModal } from './SearchModal';
-const mockSearchApi = {
- query: () =>
- Promise.resolve({
- results: [
- {
- type: 'custom-result-item',
- document: {
- location: 'search/search-result-1',
- title: 'Search Result 1',
- text: 'some text from the search result',
- },
- },
- {
- type: 'no-custom-result-item',
- document: {
- location: 'search/search-result-2',
- title: 'Search Result 2',
- text: 'some text from the search result',
- },
- },
- {
- type: 'no-custom-result-item',
- document: {
- location: 'search/search-result-3',
- title: 'Search Result 3',
- text: 'some text from the search result',
- },
- },
- ],
- }),
+const mockResults = {
+ results: [
+ {
+ type: 'custom-result-item',
+ document: {
+ location: 'search/search-result-1',
+ title: 'Search Result 1',
+ text: 'some text from the search result',
+ },
+ },
+ {
+ type: 'no-custom-result-item',
+ document: {
+ location: 'search/search-result-2',
+ title: 'Search Result 2',
+ text: 'some text from the search result',
+ },
+ },
+ {
+ type: 'no-custom-result-item',
+ document: {
+ location: 'search/search-result-3',
+ title: 'Search Result 3',
+ text: 'some text from the search result',
+ },
+ },
+ ],
};
-const apiRegistry = () => ApiRegistry.from([[searchApiRef, mockSearchApi]]);
-
export default {
title: 'Plugins/Search/SearchModal',
component: SearchModal,
decorators: [
(Story: ComponentType<{}>) =>
wrapInTestApp(
- <>
-
-
-
-
-
- >,
+
+
+ ,
{ mountedRoutes: { '/search': rootRouteRef } },
),
],
diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx
index 82e4fc2916..5efbecbe2b 100644
--- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx
+++ b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx
@@ -16,83 +16,81 @@
import { Link } from '@backstage/core-components';
import { List, ListItem } from '@material-ui/core';
-import React from 'react';
+import React, { ComponentType } from 'react';
import { MemoryRouter } from 'react-router';
import { DefaultResultListItem } from '../DefaultResultListItem';
-import { SearchContext } from '../SearchContext';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchResult } from './SearchResult';
+const mockResults = {
+ results: [
+ {
+ type: 'custom-result-item',
+ document: {
+ location: 'search/search-result-1',
+ title: 'Search Result 1',
+ text: 'some text from the search result',
+ },
+ },
+ {
+ type: 'no-custom-result-item',
+ document: {
+ location: 'search/search-result-2',
+ title: 'Search Result 2',
+ text: 'some text from the search result',
+ },
+ },
+ {
+ type: 'no-custom-result-item',
+ document: {
+ location: 'search/search-result-3',
+ title: 'Search Result 3',
+ text: 'some text from the search result',
+ },
+ },
+ ],
+};
+
export default {
title: 'Plugins/Search/SearchResult',
component: SearchResult,
-};
-
-const defaultValue = {
- result: {
- loading: false,
- error: '',
- value: {
- results: [
- {
- type: 'custom-result-item',
- document: {
- location: 'search/search-result-1',
- title: 'Search Result 1',
- text: 'some text from the search result',
- },
- },
- {
- type: 'no-custom-result-item',
- document: {
- location: 'search/search-result-2',
- title: 'Search Result 2',
- text: 'some text from the search result',
- },
- },
- {
- type: 'no-custom-result-item',
- document: {
- location: 'search/search-result-3',
- title: 'Search Result 3',
- text: 'some text from the search result',
- },
- },
- ],
- },
- },
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+ ),
+ ],
};
export const Default = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
- {({ results }) => (
-
- {results.map(({ type, document }) => {
- switch (type) {
- case 'custom-result-item':
- return (
-
- );
- default:
- return (
-
-
- {document.title} - {document.text}
-
-
- );
- }
- })}
-
- )}
-
-
-
+
+ {({ results }) => (
+
+ {results.map(({ type, document }) => {
+ switch (type) {
+ case 'custom-result-item':
+ return (
+
+ );
+ default:
+ return (
+
+
+ {document.title} - {document.text}
+
+
+ );
+ }
+ })}
+
+ )}
+
);
};
diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx
new file mode 100644
index 0000000000..6b4171a024
--- /dev/null
+++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { ApiProvider } from '@backstage/core-app-api';
+import { TestApiRegistry } from '@backstage/test-utils';
+import { act, render } from '@testing-library/react';
+import user from '@testing-library/user-event';
+
+import { searchApiRef } from '../../apis';
+import { SearchContext, SearchContextProvider } from '../SearchContext';
+import { SearchType } from './SearchType';
+
+describe('SearchType.Accordion', () => {
+ const query = jest.fn();
+ const mockApis = TestApiRegistry.from([searchApiRef, { query }]);
+
+ const contextSpy = {
+ result: { loading: false, value: { results: [] } },
+ term: '',
+ types: [],
+ filters: {},
+ toggleModal: jest.fn(),
+ setTerm: jest.fn(),
+ setTypes: jest.fn(),
+ setFilters: jest.fn(),
+ setPageCursor: jest.fn(),
+ };
+
+ const expectedLabel = 'Expected Label';
+ const expectedType = {
+ value: 'expected-type',
+ name: 'Expected Type',
+ icon: <>>,
+ };
+
+ beforeEach(() => {
+ query.mockResolvedValue({ results: [] });
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should render as expected', async () => {
+ const { getByText } = render(
+
+
+
+
+ ,
+ );
+
+ // The given label should be rendered.
+ expect(getByText(expectedLabel)).toBeInTheDocument();
+
+ // "Collapse" is visible by default (element is not collapsed)
+ expect(getByText('Collapse')).toBeInTheDocument();
+
+ // The default "all" type should be rendered.
+ expect(getByText('All')).toBeInTheDocument();
+
+ // The given type is also visible
+ expect(getByText(expectedType.name)).toBeInTheDocument();
+
+ await act(() => Promise.resolve());
+ });
+
+ it('should set entire types array when a type is selected', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ user.click(getByText(expectedType.name));
+
+ expect(contextSpy.setTypes).toHaveBeenCalledWith([expectedType.value]);
+ });
+
+ it('should reset types array when all is selected', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ user.click(getByText('All'));
+
+ expect(contextSpy.setTypes).toHaveBeenCalledWith([]);
+ });
+
+ it('should reset page cursor when a new type is selected', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ user.click(getByText(expectedType.name));
+
+ expect(contextSpy.setPageCursor).toHaveBeenCalledWith(undefined);
+ });
+
+ it('should collapse when a new type is selected', () => {
+ const { getByText, queryByText } = render(
+
+
+ ,
+ );
+
+ user.click(getByText(expectedType.name));
+
+ expect(queryByText('Collapse')).not.toBeInTheDocument();
+ });
+});
diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx
new file mode 100644
index 0000000000..57e6609bab
--- /dev/null
+++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { cloneElement, Fragment, useEffect, useState } from 'react';
+import { useSearch } from '../SearchContext';
+import {
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ Card,
+ CardContent,
+ CardHeader,
+ Divider,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ makeStyles,
+} from '@material-ui/core';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import AllIcon from '@material-ui/icons/FontDownload';
+
+const useStyles = makeStyles(theme => ({
+ card: {
+ backgroundColor: 'rgba(0, 0, 0, .11)',
+ },
+ cardContent: {
+ paddingTop: theme.spacing(1),
+ },
+ icon: {
+ color: theme.palette.common.black,
+ },
+ list: {
+ width: '100%',
+ },
+ listItemIcon: {
+ width: '24px',
+ height: '24px',
+ },
+ accordion: {
+ backgroundColor: theme.palette.background.paper,
+ },
+ accordionSummary: {
+ minHeight: 'auto',
+ '&.Mui-expanded': {
+ minHeight: 'auto',
+ },
+ },
+ accordionSummaryContent: {
+ margin: theme.spacing(2, 0),
+ '&.Mui-expanded': {
+ margin: theme.spacing(2, 0),
+ },
+ },
+ accordionDetails: {
+ padding: theme.spacing(0, 0, 1),
+ },
+}));
+
+/**
+ * @public
+ */
+export type SearchTypeAccordionProps = {
+ name: string;
+ types: Array<{
+ value: string;
+ name: string;
+ icon: JSX.Element;
+ }>;
+ defaultValue?: string;
+};
+
+export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
+ const classes = useStyles();
+ const { setPageCursor, setTypes, types } = useSearch();
+ const [expanded, setExpanded] = useState(true);
+ const { defaultValue, name, types: givenTypes } = props;
+
+ const toggleExpanded = () => setExpanded(prevState => !prevState);
+ const handleClick = (type: string) => {
+ return () => {
+ setTypes(type !== '' ? [type] : []);
+ setPageCursor(undefined);
+ setExpanded(false);
+ };
+ };
+
+ // Handle any provided defaultValue
+ useEffect(() => {
+ if (defaultValue) {
+ setTypes([defaultValue]);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const definedTypes = [
+ {
+ value: '',
+ name: 'All',
+ icon: ,
+ },
+ ...givenTypes,
+ ];
+ const selected = types[0] || '';
+
+ return (
+
+
+
+
+ }
+ IconButtonProps={{ size: 'small' }}
+ >
+ {expanded
+ ? 'Collapse'
+ : definedTypes.filter(t => t.value === selected)[0]!.name}
+
+
+
+ {definedTypes.map(type => (
+
+
+
+
+ {cloneElement(type.icon, {
+ className: classes.listItemIcon,
+ })}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/plugins/search/src/components/SearchType/SearchType.stories.tsx b/plugins/search/src/components/SearchType/SearchType.stories.tsx
index 29dd66a3a6..833573f5ee 100644
--- a/plugins/search/src/components/SearchType/SearchType.stories.tsx
+++ b/plugins/search/src/components/SearchType/SearchType.stories.tsx
@@ -14,23 +14,50 @@
* limitations under the License.
*/
-import React, { useState } from 'react';
-import { SearchContext } from '../SearchContext';
+import { Grid, Paper } from '@material-ui/core';
+import CatalogIcon from '@material-ui/icons/MenuBook';
+import DocsIcon from '@material-ui/icons/Description';
+import UsersGroupsIcon from '@material-ui/icons/Person';
+import React, { ComponentType } from 'react';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
import { SearchType } from './SearchType';
export default {
title: 'Plugins/Search/SearchType',
component: SearchType,
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+
+
+ ),
+ ],
};
const values = ['value-1', 'value-2', 'value-3'];
export const Default = () => {
- const [types, setTypes] = useState([]);
-
return (
-
+
-
+
+ );
+};
+
+export const Accordion = () => {
+ return (
+ },
+ { value: 'value-2', name: 'Value Two', icon: },
+ { value: 'value-3', name: 'Value Three', icon: },
+ ]}
+ />
);
};
diff --git a/plugins/search/src/components/SearchType/SearchType.tsx b/plugins/search/src/components/SearchType/SearchType.tsx
index 5290caa0b7..f42802819a 100644
--- a/plugins/search/src/components/SearchType/SearchType.tsx
+++ b/plugins/search/src/components/SearchType/SearchType.tsx
@@ -25,6 +25,10 @@ import {
} from '@material-ui/core';
import React, { ChangeEvent } from 'react';
import { useEffectOnce } from 'react-use';
+import {
+ SearchTypeAccordion,
+ SearchTypeAccordionProps,
+} from './SearchType.Accordion';
import { useSearch } from '../SearchContext';
const useStyles = makeStyles(theme => ({
@@ -41,6 +45,9 @@ const useStyles = makeStyles(theme => ({
},
}));
+/**
+ * @public
+ */
export type SearchTypeProps = {
className?: string;
name: string;
@@ -48,12 +55,8 @@ export type SearchTypeProps = {
defaultValue?: string[] | string | null;
};
-const SearchType = ({
- values = [],
- className,
- name,
- defaultValue,
-}: SearchTypeProps) => {
+const SearchType = (props: SearchTypeProps) => {
+ const { className, defaultValue, name, values = [] } = props;
const classes = useStyles();
const { types, setTypes } = useSearch();
@@ -112,4 +115,14 @@ const SearchType = ({
);
};
+/**
+ * A control surface for the search query's "types" property, displayed as a
+ * single-select collapsible accordion suitable for use in faceted search UIs.
+ * @public
+ */
+SearchType.Accordion = (props: SearchTypeAccordionProps) => {
+ return ;
+};
+
export { SearchType };
+export type { SearchTypeAccordionProps };
diff --git a/plugins/search/src/components/SearchType/index.ts b/plugins/search/src/components/SearchType/index.ts
index 400f3d2a46..e605276455 100644
--- a/plugins/search/src/components/SearchType/index.ts
+++ b/plugins/search/src/components/SearchType/index.ts
@@ -15,3 +15,4 @@
*/
export { SearchType } from './SearchType';
+export type { SearchTypeAccordionProps, SearchTypeProps } from './SearchType';
diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts
index fc0a149c88..2cd9130721 100644
--- a/plugins/search/src/index.ts
+++ b/plugins/search/src/index.ts
@@ -38,6 +38,10 @@ export type { SearchModalProps } from './components/SearchModal';
export { SearchPage as Router } from './components/SearchPage';
export { SearchResultPager } from './components/SearchResultPager';
export { SearchType } from './components/SearchType';
+export type {
+ SearchTypeAccordionProps,
+ SearchTypeProps,
+} from './components/SearchType';
export { SidebarSearch } from './components/SidebarSearch';
export type { SidebarSearchProps } from './components/SidebarSearch';
export type { SidebarSearchModalProps } from './components/SidebarSearchModal';