terriajs
Version:
Geospatial data visualization platform.
201 lines (182 loc) • 5.64 kB
JSX
import React from "react";
import PropTypes from "prop-types";
import createReactClass from "create-react-class";
import debounce from "lodash-es/debounce";
import Icon, { StyledIcon } from "../../Styled/Icon";
import styled, { withTheme } from "styled-components";
import Box, { BoxSpan } from "../../Styled/Box";
import Text from "../../Styled/Text";
import { RawButton } from "../../Styled/Button";
const SearchInput = styled.input`
box-sizing: border-box;
margin-top: 0;
margin-bottom: 0;
border: none;
border-radius: 20px;
height: 40px;
width: 100%;
display: block;
padding: 0.5rem 40px;
vertical-align: middle;
-webkit-appearance: none;
`;
export const DEBOUNCE_INTERVAL = 1000;
/**
* Simple dumb search box component that leaves the actual execution of searches to the component that renders it. Note
* that just like an input, this calls onSearchTextChanged when the value is changed, and expects that its parent
* component will listen for this and update searchText with the new value.
*/
export const SearchBox = createReactClass({
displayName: "SearchBox",
propTypes: {
/** Called when the search changes, after a debounce of {@link DEBOUNCE_INTERVAL} ms */
onSearchTextChanged: PropTypes.func.isRequired,
/** Called when an actual search is triggered, either by clicking the button or pressing Enter */
onDoSearch: PropTypes.func.isRequired,
/** The search text to display in the search box */
searchText: PropTypes.string.isRequired,
/** Called when the search box receives focus */
onFocus: PropTypes.func,
placeholder: PropTypes.string,
onClear: PropTypes.func,
alwaysShowClear: PropTypes.bool,
debounceDuration: PropTypes.number,
inputBoxRef: PropTypes.object,
autoFocus: PropTypes.bool,
theme: PropTypes.object
},
getDefaultProps() {
return {
placeholder: "Search",
alwaysShowClear: false,
autoFocus: false
};
},
/* eslint-disable-next-line camelcase */
UNSAFE_componentWillMount() {
this.searchWithDebounce = debounce(this.search, DEBOUNCE_INTERVAL);
},
componentDidUpdate(prevProps) {
if (
prevProps.debounceDuration !== this.props.debounceDuration &&
this.props.debounceDuration > 0
) {
// Before we create a new debounced search - make sure there are no previous search values waiting to be called
this.searchWithDebounce.flush();
this.searchWithDebounce = debounce(
this.search,
this.props.debounceDuration
);
}
},
componentWillUnmount() {
this.searchWithDebounce.cancel();
},
hasValue() {
return this.props.searchText.length > 0;
},
search() {
this.searchWithDebounce.cancel();
this.props.onDoSearch();
},
handleChange(event) {
const value = event.target.value;
// immediately bypass debounce if we started with no value
if (this.props.searchText.length === 0) {
this.props.onSearchTextChanged(value);
this.search();
} else {
this.props.onSearchTextChanged(value);
this.searchWithDebounce();
}
},
clearSearch() {
this.props.onSearchTextChanged("");
this.search();
if (this.props.onClear) {
this.props.onClear();
}
},
onKeyDown(event) {
if (event.keyCode === 13) {
this.search();
}
},
render() {
const clearButton = (
<Box position="absolute" topRight fullHeight styledWidth={"40px"}>
{/* The type="button" here stops the browser from assuming the close button is the submit button */}
<RawButton
type="button"
onClick={() => this.clearSearch()}
fullWidth
fullHeight
>
<BoxSpan centered>
<StyledIcon
glyph={Icon.GLYPHS.close}
styledWidth={"15px"}
fillColor={this.props.theme.charcoalGrey}
opacity={"0.5"}
/>
</BoxSpan>
</RawButton>
</Box>
);
return (
<form
autoComplete="off"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
this.search();
}}
css={`
position: relative;
width: 100%;
`}
>
<label
htmlFor="search"
css={`
position: absolute;
`}
>
<Box paddedRatio={2}>
{/* Without position:absolute the icon runs away from the search bar in safari. Ideally we should redo the search bar using simple flexbox */}
<StyledIcon
glyph={Icon.GLYPHS.search}
styledWidth={"20px"}
fillColor={this.props.theme.charcoalGrey}
opacity={"0.5"}
css={`
position: absolute;
`}
/>
</Box>
</label>
<Text large semiBold>
<SearchInput
ref={this.props.inputBoxRef}
id="search"
type="text"
name="search"
value={this.props.searchText}
onChange={this.handleChange}
onFocus={this.props.onFocus}
onKeyDown={this.onKeyDown}
placeholder={this.props.placeholder}
autoComplete="off"
autoFocus={this.props.autoFocus}
rounded
/>
</Text>
{(this.props.alwaysShowClear || this.hasValue()) && clearButton}
</form>
);
}
});
const SearchBoxWithRef = (props, ref) => (
<SearchBox {...props} inputBoxRef={ref} />
);
export default withTheme(React.forwardRef(SearchBoxWithRef));