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:
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user