Merge pull request #15873 from backstage/search/improve-accessibility
[Search] improve accessibility
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-explore': patch
|
||||
'@backstage/plugin-search-react': patch
|
||||
---
|
||||
|
||||
`ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements.
|
||||
|
||||
Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `<li>` elements.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Navigation items in mobile sidebar now have aria label.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Updated colors for each tab used on search modal, to improve color contrast. Aria label added to tabs wrapper component. `disableRipple` property on the `Tab` component removed to improve keyboard navigation indicator.
|
||||
@@ -235,6 +235,20 @@ export const YourSearchResultListItemExtension = plugin.provide(
|
||||
);
|
||||
```
|
||||
|
||||
If your list item accept props, you can extend the `SearchResultListItemExtensionProps` with your component specific props:
|
||||
|
||||
```tsx
|
||||
export const YourSearchResultListItemExtension: (
|
||||
props: SearchResultListItemExtensionProps<YourSearchResultListItemProps>,
|
||||
) => JSX.Element | null = plugin.provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'YourSearchResultListItem',
|
||||
component: () =>
|
||||
import('./components').then(m => m.YourSearchResultListItem),
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
Additionally, you can define a predicate function that receives a result and returns whether your extension should be used to render it or not:
|
||||
|
||||
```tsx
|
||||
|
||||
@@ -23,17 +23,11 @@ import {
|
||||
Grid,
|
||||
makeStyles,
|
||||
Paper,
|
||||
useTheme,
|
||||
} from '@material-ui/core';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import BuildIcon from '@material-ui/icons/Build';
|
||||
import LaunchIcon from '@material-ui/icons/Launch';
|
||||
import {
|
||||
CatalogIcon,
|
||||
DocsIcon,
|
||||
Link,
|
||||
useContent,
|
||||
} from '@backstage/core-components';
|
||||
import { CatalogIcon, DocsIcon, Link } from '@backstage/core-components';
|
||||
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
|
||||
import {
|
||||
@@ -78,9 +72,6 @@ const rootRouteRef = searchPlugin.routes.root;
|
||||
export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
|
||||
const classes = useStyles();
|
||||
const navigate = useNavigate();
|
||||
const { transitions } = useTheme();
|
||||
const { focusContent } = useContent();
|
||||
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const { term, types } = useSearch();
|
||||
@@ -91,19 +82,14 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
|
||||
searchBarRef?.current?.focus();
|
||||
});
|
||||
|
||||
const handleSearchResultClick = useCallback(() => {
|
||||
toggleModal();
|
||||
setTimeout(focusContent, transitions.duration.leavingScreen);
|
||||
}, [toggleModal, focusContent, transitions]);
|
||||
|
||||
const handleSearchBarKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearchResultClick();
|
||||
toggleModal();
|
||||
navigate(searchPagePath);
|
||||
}
|
||||
},
|
||||
[navigate, searchPagePath, handleSearchResultClick],
|
||||
[navigate, searchPagePath, toggleModal],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -187,7 +173,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Link to={searchPagePath} onClick={handleSearchResultClick}>
|
||||
<Link to={searchPagePath} onClick={toggleModal}>
|
||||
<Typography
|
||||
component="span"
|
||||
className={classes.viewResultsLink}
|
||||
@@ -200,10 +186,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<SearchResult
|
||||
onClick={handleSearchResultClick}
|
||||
onKeyDown={handleSearchResultClick}
|
||||
>
|
||||
<SearchResult onClick={toggleModal}>
|
||||
<CatalogSearchResultListItem icon={<CatalogIcon />} />
|
||||
<TechDocsSearchResultListItem icon={<DocsIcon />} />
|
||||
<ToolSearchResultListItem icon={<BuildIcon />} />
|
||||
|
||||
@@ -100,6 +100,7 @@ const MobileSidebarGroup = (props: SidebarGroupProps) => {
|
||||
return (
|
||||
// Material UI issue: https://github.com/mui-org/material-ui/issues/27820
|
||||
<BottomNavigationAction
|
||||
aria-label={label}
|
||||
label={label}
|
||||
icon={icon}
|
||||
component={Link as any}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { ResultHighlight } from '@backstage/plugin-search-common';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react';
|
||||
import { StarredEntitiesApi } from '@backstage/plugin-catalog-react';
|
||||
import { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { StyleRules } from '@material-ui/core/styles/withStyles';
|
||||
@@ -108,7 +109,7 @@ export const catalogPlugin: BackstagePlugin<
|
||||
|
||||
// @public (undocumented)
|
||||
export const CatalogSearchResultListItem: (
|
||||
props: CatalogSearchResultListItemProps,
|
||||
props: SearchResultListItemExtensionProps<CatalogSearchResultListItemProps>,
|
||||
) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
|
||||
+32
-39
@@ -18,8 +18,6 @@ import React, { ReactNode } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
Divider,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
@@ -67,48 +65,43 @@ export function CatalogSearchResultListItem(
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem alignItems="flex-start">
|
||||
{props.icon && <ListItemIcon>{props.icon}</ListItemIcon>}
|
||||
<div className={classes.flexContainer}>
|
||||
<ListItemText
|
||||
className={classes.itemText}
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={
|
||||
<Link noTrack to={result.location}>
|
||||
{highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight.fields.title}
|
||||
preTag={highlight.preTag}
|
||||
postTag={highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.title
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
highlight?.fields.text ? (
|
||||
{props.icon && <ListItemIcon>{props.icon}</ListItemIcon>}
|
||||
<div className={classes.flexContainer}>
|
||||
<ListItemText
|
||||
className={classes.itemText}
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={
|
||||
<Link noTrack to={result.location}>
|
||||
{highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight.fields.text}
|
||||
text={highlight.fields.title}
|
||||
preTag={highlight.preTag}
|
||||
postTag={highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.text
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
{result.kind && (
|
||||
<Chip label={`Kind: ${result.kind}`} size="small" />
|
||||
)}
|
||||
{result.lifecycle && (
|
||||
<Chip label={`Lifecycle: ${result.lifecycle}`} size="small" />
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
result.title
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
highlight?.fields.text ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight.fields.text}
|
||||
preTag={highlight.preTag}
|
||||
postTag={highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.text
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
{result.kind && <Chip label={`Kind: ${result.kind}`} size="small" />}
|
||||
{result.lifecycle && (
|
||||
<Chip label={`Lifecycle: ${result.lifecycle}`} size="small" />
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ import {
|
||||
fetchApiRef,
|
||||
storageApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
|
||||
import {
|
||||
createSearchResultListItemExtension,
|
||||
SearchResultListItemExtensionProps,
|
||||
} from '@backstage/plugin-search-react';
|
||||
import { DefaultStarredEntitiesApi } from './apis';
|
||||
import { AboutCardProps } from './components/AboutCard';
|
||||
import { DefaultCatalogPageProps } from './components/CatalogPage';
|
||||
@@ -254,7 +257,7 @@ export const RelatedEntitiesCard: <T extends Entity>(
|
||||
|
||||
/** @public */
|
||||
export const CatalogSearchResultListItem: (
|
||||
props: CatalogSearchResultListItemProps,
|
||||
props: SearchResultListItemExtensionProps<CatalogSearchResultListItemProps>,
|
||||
) => JSX.Element | null = catalogPlugin.provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'CatalogSearchResultListItem',
|
||||
|
||||
@@ -19,6 +19,7 @@ import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import { ReactNode } from 'react';
|
||||
import { ResultHighlight } from '@backstage/plugin-search-common';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react';
|
||||
import { TabProps } from '@material-ui/core';
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
@@ -126,7 +127,7 @@ export const ToolExplorerContent: (props: {
|
||||
|
||||
// @public (undocumented)
|
||||
export const ToolSearchResultListItem: (
|
||||
props: ToolSearchResultListItemProps,
|
||||
props: SearchResultListItemExtensionProps<ToolSearchResultListItemProps>,
|
||||
) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
|
||||
+31
-37
@@ -18,8 +18,6 @@ import React, { ReactNode } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
Divider,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
@@ -64,46 +62,42 @@ export function ToolSearchResultListItem(props: ToolSearchResultListItemProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem alignItems="flex-start">
|
||||
{props.icon && <ListItemIcon>{props.icon}</ListItemIcon>}
|
||||
<div className={classes.flexContainer}>
|
||||
<ListItemText
|
||||
className={classes.itemText}
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={
|
||||
<Link noTrack to={result.location}>
|
||||
{props.highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
text={props.highlight.fields.title}
|
||||
preTag={props.highlight.preTag}
|
||||
postTag={props.highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.title
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
props.highlight?.fields.text ? (
|
||||
{props.icon && <ListItemIcon>{props.icon}</ListItemIcon>}
|
||||
<div className={classes.flexContainer}>
|
||||
<ListItemText
|
||||
className={classes.itemText}
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={
|
||||
<Link noTrack to={result.location}>
|
||||
{props.highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
text={props.highlight.fields.text}
|
||||
text={props.highlight.fields.title}
|
||||
preTag={props.highlight.preTag}
|
||||
postTag={props.highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.text
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
{result.tags &&
|
||||
result.tags.map((tag: string) => (
|
||||
<Chip label={tag} size="small" />
|
||||
))}
|
||||
</Box>
|
||||
</div>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
result.title
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
props.highlight?.fields.text ? (
|
||||
<HighlightedSearchResultText
|
||||
text={props.highlight.fields.text}
|
||||
preTag={props.highlight.preTag}
|
||||
postTag={props.highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.text
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
{result.tags?.map((tag: string) => (
|
||||
<Chip label={tag} size="small" />
|
||||
))}
|
||||
</Box>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
|
||||
import {
|
||||
createSearchResultListItemExtension,
|
||||
SearchResultListItemExtensionProps,
|
||||
} from '@backstage/plugin-search-react';
|
||||
import { ExploreClient, exploreApiRef } from './api';
|
||||
import { ToolSearchResultListItemProps } from './components/ToolSearchResultListItem';
|
||||
// import { exampleTools } from './util/examples';
|
||||
@@ -70,7 +73,7 @@ export const explorePlugin = createPlugin({
|
||||
|
||||
/** @public */
|
||||
export const ToolSearchResultListItem: (
|
||||
props: ToolSearchResultListItemProps,
|
||||
props: SearchResultListItemExtensionProps<ToolSearchResultListItemProps>,
|
||||
) => JSX.Element | null = explorePlugin.provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'ToolSearchResultListItem',
|
||||
|
||||
@@ -14,6 +14,7 @@ import { InputBaseProps } from '@material-ui/core';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { LinkProps } from '@backstage/core-components';
|
||||
import { ListItemProps } from '@material-ui/core';
|
||||
import { ListItemTextProps } from '@material-ui/core';
|
||||
import { ListProps } from '@material-ui/core';
|
||||
import { PropsWithChildren } from 'react';
|
||||
@@ -55,6 +56,7 @@ export type DefaultResultListItemProps = {
|
||||
highlight?: ResultHighlight;
|
||||
rank?: number;
|
||||
lineClamp?: number;
|
||||
toggleModal?: () => void;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -391,6 +393,16 @@ export type SearchResultListItemExtensionOptions<
|
||||
predicate?: (result: SearchResult_2) => boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type SearchResultListItemExtensionProps<Props extends {} = {}> = Props &
|
||||
PropsWithChildren<
|
||||
{
|
||||
rank?: number;
|
||||
result?: SearchDocument;
|
||||
noTrack?: boolean;
|
||||
} & Omit<ListItemProps, 'button'>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const SearchResultListItemExtensions: (
|
||||
props: SearchResultListItemExtensionsProps,
|
||||
|
||||
+4
-3
@@ -35,9 +35,10 @@ describe('DefaultResultListItem', () => {
|
||||
|
||||
it('Includes primary/secondary text (title / text)', async () => {
|
||||
await renderInTestApp(<DefaultResultListItem result={result} />);
|
||||
expect(screen.getByRole('listitem')).toHaveTextContent(
|
||||
result.title + result.text,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('link', { name: result.title }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(result.text)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render icon if prop is specified', async () => {
|
||||
|
||||
+41
-49
@@ -21,13 +21,7 @@ import {
|
||||
SearchDocument,
|
||||
} from '@backstage/plugin-search-common';
|
||||
import { HighlightedSearchResultText } from '../HighlightedSearchResultText';
|
||||
import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Box,
|
||||
Divider,
|
||||
} from '@material-ui/core';
|
||||
import { ListItemIcon, ListItemText, Box } from '@material-ui/core';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Link } from '@backstage/core-components';
|
||||
|
||||
@@ -43,6 +37,7 @@ export type DefaultResultListItemProps = {
|
||||
highlight?: ResultHighlight;
|
||||
rank?: number;
|
||||
lineClamp?: number;
|
||||
toggleModal?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -61,48 +56,45 @@ export const DefaultResultListItemComponent = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem alignItems="center">
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={
|
||||
<Link noTrack to={result.location}>
|
||||
{highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight?.fields.title || ''}
|
||||
preTag={highlight?.preTag || ''}
|
||||
postTag={highlight?.postTag || ''}
|
||||
/>
|
||||
) : (
|
||||
result.title
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
<Typography
|
||||
component="span"
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: lineClamp,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{highlight?.fields.text ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight.fields.text}
|
||||
preTag={highlight.preTag}
|
||||
postTag={highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.text
|
||||
)}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{secondaryAction && <Box alignItems="flex-end">{secondaryAction}</Box>}
|
||||
</ListItem>
|
||||
<Divider />
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={
|
||||
<Link noTrack to={result.location}>
|
||||
{highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight?.fields.title || ''}
|
||||
preTag={highlight?.preTag || ''}
|
||||
postTag={highlight?.postTag || ''}
|
||||
/>
|
||||
) : (
|
||||
result.title
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
<Typography
|
||||
component="span"
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: lineClamp,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{highlight?.fields.text ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight.fields.text}
|
||||
preTag={highlight.preTag}
|
||||
postTag={highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.text
|
||||
)}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{secondaryAction && <Box alignItems="flex-end">{secondaryAction}</Box>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { ListItem } from '@material-ui/core';
|
||||
import {
|
||||
wrapInTestApp,
|
||||
renderInTestApp,
|
||||
@@ -217,8 +215,7 @@ describe('SearchResult', () => {
|
||||
}).provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'SearchResultExtension',
|
||||
component: async () => props =>
|
||||
<ListItem>Result: {props.result?.title}</ListItem>,
|
||||
component: async () => props => <>Result: {props.result?.title}</>,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { ListItem, MenuItem } from '@material-ui/core';
|
||||
import { MenuItem } from '@material-ui/core';
|
||||
import DocsIcon from '@material-ui/icons/InsertDriveFile';
|
||||
|
||||
import {
|
||||
@@ -143,8 +143,7 @@ describe('SearchResultGroup', () => {
|
||||
}).provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'SearchResultGroupItemExtension',
|
||||
component: async () => props =>
|
||||
<ListItem>Result: {props.result?.title}</ListItem>,
|
||||
component: async () => props => <>Result: {props.result?.title}</>,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { ListItem } from '@material-ui/core';
|
||||
import {
|
||||
TestApiProvider,
|
||||
renderWithEffects,
|
||||
@@ -226,8 +225,7 @@ describe('SearchResultList', () => {
|
||||
}).provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'SearchResultListItemExtension',
|
||||
component: async () => props =>
|
||||
<ListItem>Result: {props.result?.title}</ListItem>,
|
||||
component: async () => props => <>Result: {props.result?.title}</>,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { ListItem, ListItemText } from '@material-ui/core';
|
||||
import { ListItemText } from '@material-ui/core';
|
||||
|
||||
import {
|
||||
wrapInTestApp,
|
||||
@@ -75,9 +75,9 @@ const createExtension = (
|
||||
predicate,
|
||||
component = async () => (props: { result?: SearchDocument }) =>
|
||||
(
|
||||
<ListItem>
|
||||
<>
|
||||
<ListItemText primary="Default" secondary={props.result?.title} />
|
||||
</ListItem>
|
||||
</>
|
||||
),
|
||||
} = options;
|
||||
return plugin.provide(
|
||||
@@ -120,7 +120,7 @@ describe('extensions', () => {
|
||||
);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', { name: /Search Result 1/ }),
|
||||
screen.getByRole('link', { name: /Search Result 1/ }),
|
||||
);
|
||||
|
||||
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
|
||||
@@ -146,9 +146,7 @@ describe('extensions', () => {
|
||||
expect(screen.getByText('Default')).toBeInTheDocument();
|
||||
expect(screen.getByText('Search Result 1')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', { name: /Search Result 1/ }),
|
||||
);
|
||||
await userEvent.click(screen.getByRole('listitem'));
|
||||
|
||||
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
|
||||
action: 'discover',
|
||||
@@ -184,9 +182,9 @@ describe('extensions', () => {
|
||||
predicate: (result: SearchResult) => result.type === 'explore',
|
||||
component: async () => (props: { result?: SearchDocument }) =>
|
||||
(
|
||||
<ListItem>
|
||||
<>
|
||||
<ListItemText primary="Explore" secondary={props.result?.title} />
|
||||
</ListItem>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { SearchDocument, SearchResult } from '@backstage/plugin-search-common';
|
||||
|
||||
import { Box, List, ListProps } from '@material-ui/core';
|
||||
import { ListItem, List, ListProps, ListItemProps } from '@material-ui/core';
|
||||
|
||||
import { DefaultResultListItem } from './components';
|
||||
|
||||
@@ -73,26 +73,34 @@ const findSearchResultListItemExtensionElement = (
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Props for {@link SearchResultListItemExtension}.
|
||||
* @public
|
||||
* Extends props for any search result list item extension
|
||||
*/
|
||||
type SearchResultListItemExtensionProps = PropsWithChildren<{
|
||||
rank?: number;
|
||||
result?: SearchDocument;
|
||||
noTrack?: boolean;
|
||||
}>;
|
||||
export type SearchResultListItemExtensionProps<Props extends {} = {}> = Props &
|
||||
PropsWithChildren<
|
||||
{
|
||||
rank?: number;
|
||||
result?: SearchDocument;
|
||||
noTrack?: boolean;
|
||||
} & Omit<ListItemProps, 'button'>
|
||||
>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Extends children with extension capabilities.
|
||||
* @param props - see {@link SearchResultListItemExtensionProps}.
|
||||
*/
|
||||
const SearchResultListItemExtension = ({
|
||||
rank,
|
||||
result,
|
||||
noTrack,
|
||||
children,
|
||||
}: SearchResultListItemExtensionProps) => {
|
||||
const SearchResultListItemExtension = (
|
||||
props: SearchResultListItemExtensionProps,
|
||||
) => {
|
||||
const {
|
||||
rank,
|
||||
result,
|
||||
noTrack,
|
||||
children,
|
||||
alignItems = 'flex-start',
|
||||
...rest
|
||||
} = props;
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const handleClickCapture = useCallback(() => {
|
||||
@@ -105,9 +113,14 @@ const SearchResultListItemExtension = ({
|
||||
}, [rank, result, noTrack, analytics]);
|
||||
|
||||
return (
|
||||
<Box role="button" tabIndex={0} onClickCapture={handleClickCapture}>
|
||||
<ListItem
|
||||
divider
|
||||
alignItems={alignItems}
|
||||
onClickCapture={handleClickCapture}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,13 +22,12 @@ import { makeStyles, Tab, Tabs } from '@material-ui/core';
|
||||
const useStyles = makeStyles((theme: BackstageTheme) => ({
|
||||
tabs: {
|
||||
borderBottom: `1px solid ${theme.palette.textVerySubtle}`,
|
||||
padding: theme.spacing(0, 4),
|
||||
},
|
||||
tab: {
|
||||
height: '50px',
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
fontSize: theme.typography.pxToRem(13),
|
||||
color: theme.palette.textSubtle,
|
||||
color: theme.palette.text.primary,
|
||||
minWidth: '130px',
|
||||
},
|
||||
}));
|
||||
@@ -72,6 +71,7 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => {
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
aria-label="List of search types tabs"
|
||||
className={classes.tabs}
|
||||
indicatorColor="primary"
|
||||
value={types.length === 0 ? '' : types[0]}
|
||||
@@ -81,7 +81,6 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => {
|
||||
<Tab
|
||||
key={idx}
|
||||
className={classes.tab}
|
||||
disableRipple
|
||||
label={type.name}
|
||||
value={type.value}
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { ResultHighlight } from '@backstage/plugin-search-common';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react';
|
||||
import { TableColumn } from '@backstage/core-components';
|
||||
import { TableOptions } from '@backstage/core-components';
|
||||
import { TableProps } from '@backstage/core-components';
|
||||
@@ -400,7 +401,7 @@ export type TechDocsSearchProps = {
|
||||
|
||||
// @public
|
||||
export const TechDocsSearchResultListItem: (
|
||||
props: TechDocsSearchResultListItemProps,
|
||||
props: SearchResultListItemExtensionProps<TechDocsSearchResultListItemProps>,
|
||||
) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
|
||||
@@ -33,7 +33,10 @@ import {
|
||||
fetchApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
|
||||
import {
|
||||
createSearchResultListItemExtension,
|
||||
SearchResultListItemExtensionProps,
|
||||
} from '@backstage/plugin-search-react';
|
||||
import { TechDocsSearchResultListItemProps } from './search/components/TechDocsSearchResultListItem';
|
||||
|
||||
/**
|
||||
@@ -162,7 +165,7 @@ export const TechDocsReaderPage = techdocsPlugin.provide(
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsSearchResultListItem: (
|
||||
props: TechDocsSearchResultListItemProps,
|
||||
props: SearchResultListItemExtensionProps<TechDocsSearchResultListItemProps>,
|
||||
) => JSX.Element | null = techdocsPlugin.provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'TechDocsSearchResultListItem',
|
||||
|
||||
@@ -15,13 +15,7 @@
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
Divider,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { ListItemIcon, ListItemText, makeStyles } from '@material-ui/core';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { ResultHighlight } from '@backstage/plugin-search-common';
|
||||
@@ -157,11 +151,8 @@ export const TechDocsSearchResultListItem = (
|
||||
const ListItemWrapper = ({ children }: PropsWithChildren<{}>) =>
|
||||
asListItem ? (
|
||||
<>
|
||||
<ListItem alignItems="flex-start">
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<div className={classes.flexContainer}>{children}</div>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<div className={classes.flexContainer}>{children}</div>
|
||||
</>
|
||||
) : (
|
||||
<>{children}</>
|
||||
|
||||
Reference in New Issue
Block a user