Merge pull request #16647 from luchillo17/fix/BCKSTG-95-search-plugin-accessibility

fix: Add close button & better padding on search input
This commit is contained in:
Patrik Oldsberg
2023-04-25 16:21:10 +02:00
committed by GitHub
8 changed files with 194 additions and 113 deletions
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-search-react': minor
'@backstage/plugin-search': minor
---
Add close button & improve search input.
MUI's Paper wrapping the SearchBar in the SearchPage was removed, we recommend users update their apps accordingly.
SearchBarBase's TextField's label support added & aria-label uses label string if present, tests relying on the default placeholder value should still work unless custom placeholder was given.
@@ -13,26 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { KeyboardEvent, useRef, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
DialogActions,
DialogContent,
DialogTitle,
Grid,
makeStyles,
Paper,
} 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 } from '@backstage/core-components';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
import {
catalogApiRef,
CATALOG_FILTER_EXISTS,
catalogApiRef,
} from '@backstage/plugin-catalog-react';
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { searchPlugin, SearchType } from '@backstage/plugin-search';
@@ -44,12 +29,38 @@ import {
useSearch,
} from '@backstage/plugin-search-react';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
import {
Box,
DialogActions,
DialogContent,
DialogTitle,
Grid,
makeStyles,
} from '@material-ui/core';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import BuildIcon from '@material-ui/icons/Build';
import CloseIcon from '@material-ui/icons/Close';
import React, { KeyboardEvent, useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
const useStyles = makeStyles(theme => ({
dialogTitle: {
gap: theme.spacing(1),
display: 'grid',
alignItems: 'center',
gridTemplateColumns: '1fr auto',
'&> button': {
marginTop: theme.spacing(1),
},
},
container: {
borderRadius: 30,
display: 'flex',
height: '2.4em',
padding: theme.spacing(1),
},
filter: {
'& + &': {
@@ -83,7 +94,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
});
const handleSearchBarKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
(e: KeyboardEvent<HTMLDivElement | HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
toggleModal();
navigate(searchPagePath);
@@ -95,13 +106,17 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
return (
<>
<DialogTitle>
<Paper className={classes.container}>
<Box className={classes.dialogTitle}>
<SearchBar
className={classes.input}
inputProps={{ ref: searchBarRef }}
onKeyDown={handleSearchBarKeyDown}
/>
</Paper>
<IconButton aria-label="close" onClick={toggleModal}>
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
<Grid container direction="column">
@@ -173,15 +188,15 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
alignItems="center"
>
<Grid item>
<Link to={searchPagePath} onClick={toggleModal}>
<Typography
component="span"
className={classes.viewResultsLink}
>
View Full Results
</Typography>
<LaunchIcon color="primary" />
</Link>
<Button
to={searchPagePath}
onClick={toggleModal}
endIcon={<ArrowForwardIcon />}
component={Link}
color="primary"
>
View Full Results
</Button>
</Grid>
</Grid>
</Grid>
@@ -44,9 +44,6 @@ import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import BuildIcon from '@material-ui/icons/Build';
const useStyles = makeStyles((theme: Theme) => ({
bar: {
padding: theme.spacing(1, 0),
},
filter: {
'& + &': {
marginTop: theme.spacing(2.5),
@@ -70,9 +67,7 @@ const SearchPage = () => {
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper className={classes.bar}>
<SearchBar debounceTime={100} />
</Paper>
<SearchBar debounceTime={100} />
</Grid>
{!isMobile && (
<Grid item xs={3}>
+3 -2
View File
@@ -10,7 +10,6 @@ import { AsyncState } from 'react-use/lib/useAsync';
import { AutocompleteProps } from '@material-ui/lab';
import { Extension } from '@backstage/core-plugin-api';
import { ForwardRefExoticComponent } from 'react';
import { InputBaseProps } from '@material-ui/core';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { LinkProps } from '@backstage/core-components';
@@ -26,6 +25,7 @@ import { SearchDocument } from '@backstage/plugin-search-common';
import { SearchQuery } from '@backstage/plugin-search-common';
import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common';
import { SearchResultSet } from '@backstage/plugin-search-common';
import { TextFieldProps } from '@material-ui/core/TextField';
import { TypographyProps } from '@material-ui/core';
// @public (undocumented)
@@ -136,12 +136,13 @@ export const SearchBar: ForwardRefExoticComponent<SearchBarProps>;
export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps>;
// @public
export type SearchBarBaseProps = Omit<InputBaseProps, 'onChange'> & {
export type SearchBarBaseProps = Omit<TextFieldProps, 'onChange'> & {
debounceTime?: number;
clearButton?: boolean;
onClear?: () => void;
onSubmit?: () => void;
onChange: (value: string) => void;
endAdornment?: React_2.ReactNode;
};
// @public
@@ -44,7 +44,7 @@ export default {
export const Default = () => {
return (
<Paper style={{ padding: '8px 0' }}>
<Paper style={{ padding: '8px' }}>
<SearchBar />
</Paper>
);
@@ -52,15 +52,23 @@ export const Default = () => {
export const CustomPlaceholder = () => {
return (
<Paper style={{ padding: '8px 0' }}>
<Paper style={{ padding: '8px' }}>
<SearchBar placeholder="This is a custom placeholder" />
</Paper>
);
};
export const CustomLabel = () => {
return (
<Paper style={{ padding: '8px' }}>
<SearchBar label="This is a custom label" />
</Paper>
);
};
export const Focused = () => {
return (
<Paper style={{ padding: '8px 0' }}>
<Paper style={{ padding: '8px' }}>
{/* 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 */}
<SearchBar autoFocus />
@@ -70,7 +78,7 @@ export const Focused = () => {
export const WithoutClearButton = () => {
return (
<Paper style={{ padding: '8px 0' }}>
<Paper style={{ padding: '8px' }}>
<SearchBar clearButton={false} />
</Paper>
);
@@ -80,7 +88,7 @@ const useStyles = makeStyles({
search: {
display: 'flex',
justifyContent: 'space-between',
padding: '8px 0',
padding: '8px 16px',
borderRadius: '50px',
margin: 'auto',
},
@@ -70,9 +70,28 @@ describe('SearchBar', () => {
);
await waitFor(() => {
expect(
screen.getByPlaceholderText('Search in Mock title'),
).toBeInTheDocument();
expect(screen.getByLabelText('Search')).toBeInTheDocument();
});
});
it('Renders with custom label', async () => {
const label = 'label';
const result = await renderWithEffects(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
[searchApiRef, searchApiMock],
]}
>
<SearchContextProvider>
<SearchBar label={label} />
</SearchContextProvider>
</TestApiProvider>,
);
await waitFor(() => {
expect(result.getByLabelText(label)).toBeInTheDocument();
});
});
@@ -13,39 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {
ChangeEvent,
KeyboardEvent,
useState,
useEffect,
useCallback,
forwardRef,
ComponentType,
ForwardRefExoticComponent,
} from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import {
InputBase,
InputBaseProps,
InputAdornment,
IconButton,
} from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import ClearButton from '@material-ui/icons/Clear';
import {
AnalyticsContext,
configApiRef,
useApi,
} from '@backstage/core-plugin-api';
import { IconButton, InputAdornment, TextField } from '@material-ui/core';
import Button from '@material-ui/core/Button';
import { TextFieldProps } from '@material-ui/core/TextField';
import SearchIcon from '@material-ui/icons/Search';
import React, {
ChangeEvent,
ComponentType,
forwardRef,
ForwardRefExoticComponent,
KeyboardEvent,
useCallback,
useEffect,
useState,
} from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { SearchContextProvider, useSearch } from '../../context';
import { TrackSearch } from '../SearchTracker';
function withContext<T>(Component: ComponentType<T>) {
return forwardRef<unknown, T>((props, ref) => (
return forwardRef<HTMLDivElement, T>((props, ref) => (
<SearchContextProvider inheritParentContextIfAvailable>
<Component {...props} ref={ref} />
</SearchContextProvider>
@@ -57,12 +50,13 @@ function withContext<T>(Component: ComponentType<T>) {
*
* @public
*/
export type SearchBarBaseProps = Omit<InputBaseProps, 'onChange'> & {
export type SearchBarBaseProps = Omit<TextFieldProps, 'onChange'> & {
debounceTime?: number;
clearButton?: boolean;
onClear?: () => void;
onSubmit?: () => void;
onChange: (value: string) => void;
endAdornment?: React.ReactNode;
};
/**
@@ -84,9 +78,10 @@ export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps> =
clearButton = true,
fullWidth = true,
value: defaultValue,
placeholder: defaultPlaceholder,
label,
placeholder,
inputProps: defaultInputProps = {},
endAdornment: defaultEndAdornment,
endAdornment,
...rest
} = props;
@@ -109,7 +104,7 @@ export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps> =
);
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
(e: KeyboardEvent<HTMLDivElement>) => {
if (onKeyDown) onKeyDown(e);
if (onSubmit && e.key === 'Enter') {
onSubmit();
@@ -125,8 +120,10 @@ export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps> =
}
}, [onChange, onClear]);
const placeholder =
defaultPlaceholder ??
const ariaLabel: string | undefined = label ? undefined : 'Search';
const inputPlaceholder =
placeholder ??
`Search in ${configApi.getOptionalString('app.title') || 'Backstage'}`;
const startAdornment = (
@@ -137,24 +134,45 @@ export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps> =
</InputAdornment>
);
const endAdornment = (
const clearButtonEndAdornment = (
<InputAdornment position="end">
<IconButton aria-label="Clear" size="small" onClick={handleClear}>
<ClearButton />
</IconButton>
<Button
aria-label="Clear"
size="small"
onClick={handleClear}
onKeyDown={event => {
if (event.key === 'Enter') {
// write your functionality here
event.stopPropagation();
}
}}
>
Clear
</Button>
</InputAdornment>
);
return (
<TrackSearch>
<InputBase
<TextField
id="search-bar-text-field"
data-testid="search-bar-next"
ref={ref}
variant="outlined"
margin="normal"
inputRef={ref}
value={value}
placeholder={placeholder}
startAdornment={startAdornment}
endAdornment={clearButton ? endAdornment : defaultEndAdornment}
inputProps={{ 'aria-label': 'Search', ...defaultInputProps }}
label={label}
placeholder={inputPlaceholder}
InputProps={{
startAdornment,
endAdornment: clearButton
? clearButtonEndAdornment
: endAdornment,
}}
inputProps={{
'aria-label': ariaLabel,
...defaultInputProps,
}}
fullWidth={fullWidth}
onChange={handleChange}
onKeyDown={handleKeyDown}
@@ -13,9 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { KeyboardEvent, useRef, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Link, useContent } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import {
SearchBar,
SearchContextProvider,
SearchResult,
SearchResultPager,
useSearch,
} from '@backstage/plugin-search-react';
import {
Dialog,
DialogActions,
@@ -23,21 +29,17 @@ import {
DialogTitle,
Divider,
Grid,
Paper,
useTheme,
} from '@material-ui/core';
import Typography from '@material-ui/core/Typography';
import LaunchIcon from '@material-ui/icons/Launch';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import { makeStyles } from '@material-ui/core/styles';
import {
SearchContextProvider,
SearchBar,
SearchResult,
SearchResultPager,
useSearch,
} from '@backstage/plugin-search-react';
import { useRouteRef } from '@backstage/core-plugin-api';
import { Link, useContent } from '@backstage/core-components';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import CloseIcon from '@material-ui/icons/Close';
import React, { KeyboardEvent, useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { rootRouteRef } from '../../plugin';
/**
@@ -78,10 +80,14 @@ export interface SearchModalProps {
}
const useStyles = makeStyles(theme => ({
container: {
borderRadius: 30,
display: 'flex',
height: '2.4em',
dialogTitle: {
gap: theme.spacing(1),
display: 'grid',
alignItems: 'center',
gridTemplateColumns: '1fr auto',
'&> button': {
marginTop: theme.spacing(1),
},
},
input: {
flex: 1,
@@ -92,7 +98,7 @@ const useStyles = makeStyles(theme => ({
viewResultsLink: { verticalAlign: '0.5em' },
}));
export const Modal = () => {
export const Modal = ({ toggleModal }: SearchModalChildrenProps) => {
const classes = useStyles();
const navigate = useNavigate();
const { transitions } = useTheme();
@@ -111,7 +117,7 @@ export const Modal = () => {
}, [focusContent, transitions]);
const handleSearchBarKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
(e: KeyboardEvent<HTMLDivElement | HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
navigate(searchPagePath);
handleSearchResultClick();
@@ -123,13 +129,17 @@ export const Modal = () => {
return (
<>
<DialogTitle>
<Paper className={classes.container}>
<Box className={classes.dialogTitle}>
<SearchBar
className={classes.input}
inputProps={{ ref: searchBarRef }}
onKeyDown={handleSearchBarKeyDown}
/>
</Paper>
<IconButton aria-label="close" onClick={toggleModal}>
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
<Grid
@@ -139,12 +149,15 @@ export const Modal = () => {
alignItems="center"
>
<Grid item>
<Link to={searchPagePath} onClick={handleSearchResultClick}>
<Typography component="span" className={classes.viewResultsLink}>
View Full Results
</Typography>
<LaunchIcon color="primary" />
</Link>
<Button
to={searchPagePath}
onClick={handleSearchResultClick}
endIcon={<ArrowForwardIcon />}
component={Link}
color="primary"
>
View Full Results
</Button>
</Grid>
</Grid>
<Divider />
@@ -186,7 +199,9 @@ export const SearchModal = (props: SearchModalProps) => {
>
{open && (
<SearchContextProvider inheritParentContextIfAvailable>
{(children && children({ toggleModal })) ?? <Modal />}
{(children && children({ toggleModal })) ?? (
<Modal toggleModal={toggleModal} />
)}
</SearchContextProvider>
)}
</Dialog>