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';