Search improvements (#76)
* Search improvements * Search results keyboard navigation
This commit is contained in:
parent
d853821186
commit
74d65aba67
|
@ -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;
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
.content {
|
||||
width: 100%;
|
||||
margin: 40px 20px;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
.icon {
|
||||
width: 38px;
|
||||
margin-bottom: -5px;
|
||||
margin-right: 10px;
|
||||
opacity: 0.15;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -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:
|
||||
|
|
Reference in New Issue