Search improvements (#76)

* Search improvements

* Search results keyboard navigation
This commit is contained in:
Tom Moor 2017-05-31 20:23:09 -07:00 committed by GitHub
parent d853821186
commit 74d65aba67
9 changed files with 156 additions and 105 deletions

View File

@ -1,6 +1,6 @@
// @flow
import React from 'react';
import styles from './CenteredContent.scss';
import styled from 'styled-components';
type Props = {
children?: React.Element<any>,
@ -8,21 +8,27 @@ type Props = {
maxWidth?: string,
};
const CenteredContent = (props: Props) => {
const style = {
maxWidth: props.maxWidth,
...props.style,
const Container = styled.div`
width: 100%;
margin: 40px 20px;
`;
const CenteredContent = ({
children,
maxWidth = '740px',
style,
...rest
}: Props) => {
const styles = {
maxWidth,
...style,
};
return (
<div className={styles.content} style={style}>
{props.children}
</div>
<Container style={styles} {...rest}>
{children}
</Container>
);
};
CenteredContent.defaultProps = {
maxWidth: '740px',
};
export default CenteredContent;

View File

@ -1,4 +0,0 @@
.content {
width: 100%;
margin: 40px 20px;
}

View File

@ -1,5 +1,5 @@
// @flow
import React from 'react';
import React, { Component } from 'react';
import { toJS } from 'mobx';
import { Link } from 'react-router-dom';
import type { Document } from 'types';
@ -10,26 +10,34 @@ import PublishingInfo from 'components/PublishingInfo';
type Props = {
document: Document,
innerRef?: Function,
};
const Container = styled.div`
width: 100%;
padding: 20px 0;
`;
const DocumentLink = styled(Link)`
display: block;
margin: -16px;
margin: 16px -16px;
padding: 16px;
border-radius: 8px;
border: 2px solid transparent;
max-height: 50vh;
overflow: hidden;
width: 100%;
&:hover,
&:active,
&:focus {
background: ${color.smokeLight};
border: 2px solid ${color.smoke};
outline: none;
}
&:focus {
border: 2px solid ${color.slateDark};
}
h1 {
margin-top: 0;
}
&:hover {
background: ${color.smokeLight};
}
`;
// $FlowIssue
@ -37,10 +45,14 @@ const TruncatedMarkdown = styled(Markdown)`
pointer-events: none;
`;
const DocumentPreview = ({ document }: Props) => {
return (
<Container>
<DocumentLink to={document.url}>
class DocumentPreview extends Component {
props: Props;
render() {
const { document, innerRef, ...rest } = this.props;
return (
<DocumentLink to={document.url} innerRef={innerRef} {...rest}>
<PublishingInfo
createdAt={document.createdAt}
createdBy={document.createdBy}
@ -50,8 +62,8 @@ const DocumentPreview = ({ document }: Props) => {
/>
<TruncatedMarkdown text={document.text} limit={150} />
</DocumentLink>
</Container>
);
};
);
}
}
export default DocumentPreview;

View File

@ -1,13 +1,15 @@
// @flow
import React from 'react';
import ReactDOM from 'react-dom';
import { observer } from 'mobx-react';
import _ from 'lodash';
import { Flex } from 'reflexbox';
import { withRouter } from 'react-router';
import { searchUrl } from 'utils/routeHelpers';
import styled from 'styled-components';
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
import SearchField from './components/SearchField';
import styles from './Search.scss';
import SearchStore from './SearchStore';
import Layout, { Title } from 'components/Layout';
@ -21,7 +23,20 @@ type Props = {
notFound: ?boolean,
};
const Container = styled(CenteredContent)`
position: relative;
`;
const ResultsWrapper = styled(Flex)`
position: absolute;
transition: all 200ms ease-in-out;
top: ${props => (props.pinToTop ? '0%' : '50%')};
margin-top: ${props => (props.pinToTop ? '40px' : '-75px')};
width: 100%;
`;
@observer class Search extends React.Component {
firstDocument: HTMLElement;
props: Props;
store: SearchStore;
@ -38,10 +53,20 @@ type Props = {
}
handleKeyDown = ev => {
// ESC
if (ev.which === 27) {
ev.preventDefault();
this.props.history.goBack();
}
// Down
if (ev.which === 40) {
ev.preventDefault();
if (this.firstDocument) {
const element = ReactDOM.findDOMNode(this.firstDocument);
// $FlowFixMe
if (element && element.focus) element.focus();
}
}
};
updateSearchResults = _.debounce(() => {
@ -52,40 +77,46 @@ type Props = {
this.props.history.replace(searchUrl(query));
};
setFirstDocumentRef = ref => {
this.firstDocument = ref;
};
render() {
const query = this.props.match.params.query;
const title = <Title content="Search" />;
const hasResults = this.store.documents.length > 0;
return (
<Layout title={title} search={false} loading={this.store.isFetching}>
<PageTitle title="Search" />
<CenteredContent>
<Container auto>
{this.props.notFound &&
<div>
<h1>Not Found</h1>
<p>We're unable to find the page you're accessing.</p>
<hr />
</div>}
<Flex column auto>
<Flex auto>
<img
src={require('assets/icons/search.svg')}
className={styles.icon}
alt="Search"
/>
<SearchField
searchTerm={this.store.searchTerm}
onKeyDown={this.handleKeyDown}
onChange={this.updateQuery}
value={query}
/>
</Flex>
{this.store.documents.map(document => (
<DocumentPreview key={document.id} document={document} />
))}
</Flex>
</CenteredContent>
<ResultsWrapper pinToTop={hasResults} column auto>
<SearchField
searchTerm={this.store.searchTerm}
onKeyDown={this.handleKeyDown}
onChange={this.updateQuery}
value={query || ''}
/>
<ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{this.store.documents.map((document, index) => (
<DocumentPreview
innerRef={ref => index === 0 && this.setFirstDocumentRef(ref)}
key={document.id}
document={document}
/>
))}
</ArrowKeyNavigation>
</ResultsWrapper>
</Container>
</Layout>
);
}

View File

@ -1,6 +0,0 @@
.icon {
width: 38px;
margin-bottom: -5px;
margin-right: 10px;
opacity: 0.15;
}

View File

@ -1,8 +1,32 @@
// @flow
import React, { Component } from 'react';
import styles from './SearchField.scss';
import { Flex } from 'reflexbox';
import styled from 'styled-components';
import searchImg from 'assets/icons/search.svg';
const Field = styled.input`
width: 100%;
padding: 10px;
font-size: 48px;
font-weight: 400;
outline: none;
border: 0;
::-webkit-input-placeholder { color: #ccc; }
:-moz-placeholder { color: #ccc; }
::-moz-placeholder { color: #ccc; }
:-ms-input-placeholder { color: #ccc; }
`;
const Icon = styled.img`
width: 38px;
margin-bottom: -5px;
margin-right: 10px;
opacity: 0.15;
`;
class SearchField extends Component {
input: HTMLElement;
props: {
onChange: Function,
};
@ -11,17 +35,26 @@ class SearchField extends Component {
this.props.onChange(ev.currentTarget.value ? ev.currentTarget.value : '');
};
focusInput = (ev: SyntheticEvent) => {
this.input.focus();
};
setRef = (ref: HTMLElement) => {
this.input = ref;
};
render() {
return (
<div className={styles.container}>
<input
<Flex>
<Icon src={searchImg} alt="Search" onClick={this.focusInput} />
<Field
{...this.props}
innerRef={this.setRef}
onChange={this.handleChange}
className={styles.field}
placeholder="Search"
autoFocus
/>
</div>
</Flex>
);
}
}

View File

@ -1,31 +0,0 @@
.container {
padding: 40px 0;
}
.field {
width: 100%;
padding: 10px;
font-size: 48px;
font-weight: 400;
outline: none;
border: 0;
// border-bottom: 1px solid #ccc;
}
:global {
::-webkit-input-placeholder {
color: #ccc;
}
:-moz-placeholder {
color: #ccc;
}
::-moz-placeholder {
color: #ccc;
}
:-ms-input-placeholder {
color: #ccc;
}
}

View File

@ -75,6 +75,7 @@
"babel-preset-react-hmre": "1.1.1",
"babel-regenerator-runtime": "6.5.0",
"bcrypt": "^0.8.7",
"boundless-arrow-key-navigation": "^1.0.4",
"bugsnag": "^1.7.0",
"classnames": "2.2.3",
"cross-env": "1.0.7",

View File

@ -1087,6 +1087,21 @@ boom@2.x.x:
dependencies:
hoek "2.x.x"
boundless-arrow-key-navigation@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/boundless-arrow-key-navigation/-/boundless-arrow-key-navigation-1.0.4.tgz#180c4253ad25e54f7cce648efe63d4538e437ae8"
dependencies:
boundless-utils-omit-keys "^1.0.4"
boundless-utils-uuid "^1.0.4"
boundless-utils-omit-keys@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/boundless-utils-omit-keys/-/boundless-utils-omit-keys-1.0.4.tgz#95b9bb03e0a80ff26d8a3c95c1ed5e95daf8465b"
boundless-utils-uuid@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/boundless-utils-uuid/-/boundless-utils-uuid-1.0.4.tgz#d125a0d45cf79fac84e517ea549765a4bbe1ebb6"
bowser@^1.0.0:
version "1.6.1"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.6.1.tgz#9157e9498f456e937173a2918f3b2161e5353eb3"
@ -8222,13 +8237,7 @@ supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
supports-color@^3.1.0, supports-color@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
dependencies:
has-flag "^1.0.0"
supports-color@^3.2.3:
supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
dependencies: