Skip to content

Instantly share code, notes, and snippets.

@hongz1
Last active March 10, 2023 18:35
Show Gist options
  • Save hongz1/8368c79690b8bdfe5a86f686e3257134 to your computer and use it in GitHub Desktop.
Save hongz1/8368c79690b8bdfe5a86f686e3257134 to your computer and use it in GitHub Desktop.
MUIv5 + React18 compatible version of material-ui-search-bar.
/* eslint-disable react/react-in-jsx-scope -- Unaware of jsxImportSource */
/** @jsxImportSource @emotion/react */
import React from "react";
import styled from "@emotion/styled";
import { css } from "@emotion/react";
import PropTypes from "prop-types";
import { IconButton as MuiIconButton, Input, Paper as MuiPaper } from "@mui/material";
import { Clear as ClearIcon, Search as SearchIcon } from "@mui/icons-material";
const Paper = styled(MuiPaper)`
height: ${(props) => props.theme.spacing(6)};
display: flex;
justify-content: space-between;
`;
const SearchInput = styled(Input)`
width: 100%;
`;
const SearchContainer = styled.div`
margin: auto ${(props) => props.theme.spacing(4)};
width: calc(100% - ${(props) => props.theme.spacing(6 + 4)});
`;
const IconButton = styled(MuiIconButton)`
color: ${(props) => props.theme.palette.action.active};
transform: scale(1, 1);
transition: ${(props) =>
props.theme.transitions.create(["transform", "color"], {
duration: props.theme.transitions.duration.shorter,
easing: props.theme.transitions.easing.easeInOut,
})};
`;
const SearchIconButton = styled(IconButton)`
margin-right: ${(props) => props.theme.spacing(-8)};
`;
const iconButtonHidden = css`
transform: scale(0, 0);
"& > $icon": {
opacity: 0;
}
`;
const iconTransition = {
transition: (theme) =>
theme.transitions.create(["opacity"], {
duration: theme.transitions.duration.shorter,
easing: theme.transitions.easing.easeInOut,
}),
};
/**
* Material design search bar
* @see [Search patterns](https://material.io/archive/guidelines/patterns/search.html)
*/
const SearchBar = React.forwardRef(
(
{ cancelOnEscape, className, classes, closeIcon, disabled, onCancelSearch, onRequestSearch, searchIcon, style, ...inputProps },
ref
) => {
const inputRef = React.useRef();
const [value, setValue] = React.useState(inputProps.value);
React.useEffect(() => {
setValue(inputProps.value);
}, [inputProps.value]);
const handleFocus = React.useCallback(
(e) => {
if (inputProps.onFocus) {
inputProps.onFocus(e);
}
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[inputProps.onFocus]
);
const handleBlur = React.useCallback(
(e) => {
setValue((v) => v.trim());
if (inputProps.onBlur) {
inputProps.onBlur(e);
}
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[inputProps.onBlur]
);
const handleInput = React.useCallback(
(e) => {
setValue(e.target.value);
if (inputProps.onChange) {
inputProps.onChange(e.target.value);
}
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[inputProps.onChange]
);
const handleCancel = React.useCallback(() => {
setValue("");
if (onCancelSearch) {
onCancelSearch();
}
}, [onCancelSearch]);
const handleRequestSearch = React.useCallback(() => {
if (onRequestSearch) {
onRequestSearch(value);
}
}, [onRequestSearch, value]);
const handleKeyUp = React.useCallback(
(e) => {
if (e.charCode === 13 || e.key === "Enter") {
handleRequestSearch();
} else if (cancelOnEscape && (e.charCode === 27 || e.key === "Escape")) {
handleCancel();
}
if (inputProps.onKeyUp) {
inputProps.onKeyUp(e);
}
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[handleRequestSearch, cancelOnEscape, handleCancel, inputProps.onKeyUp]
);
React.useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
blur: () => {
inputRef.current.blur();
},
}));
return (
<Paper className={className} style={style}>
<SearchContainer>
<SearchInput
{...inputProps}
inputRef={inputRef}
onBlur={handleBlur}
value={value}
onChange={handleInput}
onKeyUp={handleKeyUp}
onFocus={handleFocus}
fullWidth
disableUnderline
disabled={disabled}
/>
</SearchContainer>
<SearchIconButton
onClick={handleRequestSearch}
css={value !== "" ? iconButtonHidden : null}
disabled={disabled}
aria-label="btn-search-request"
>
{React.cloneElement(searchIcon, { sx: iconTransition })}
</SearchIconButton>
<IconButton
onClick={handleCancel}
css={value === "" ? iconButtonHidden : null}
disabled={disabled}
aria-label="btn-search-cancel"
>
{React.cloneElement(closeIcon, { sx: iconTransition })}
</IconButton>
</Paper>
);
}
);
SearchBar.defaultProps = {
className: "",
closeIcon: <ClearIcon />,
disabled: false,
placeholder: "Search",
searchIcon: <SearchIcon />,
style: null,
value: "",
};
SearchBar.propTypes = {
/** Whether to clear search on escape */
cancelOnEscape: PropTypes.bool,
/** Override or extend the styles applied to the component. */
classes: PropTypes.object,
/** Custom top-level class */
className: PropTypes.string,
/** Override the close icon. */
closeIcon: PropTypes.node,
/** Disables text field. */
disabled: PropTypes.bool,
/** Fired when the search is cancelled. */
onCancelSearch: PropTypes.func,
/** Fired when the text value changes. */
onChange: PropTypes.func,
/** Fired when the search icon is clicked. */
onRequestSearch: PropTypes.func,
/** Sets placeholder text for the embedded text field. */
placeholder: PropTypes.string,
/** Override the search icon. */
searchIcon: PropTypes.node,
/** Override the inline-styles of the root element. */
style: PropTypes.object,
/** The value of the text field. */
value: PropTypes.string,
};
export default SearchBar;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment