diff --git a/frontend/components/CenteredContent/CenteredContent.js b/frontend/components/CenteredContent/CenteredContent.js index 1661cbeb..2d6cdd5a 100644 --- a/frontend/components/CenteredContent/CenteredContent.js +++ b/frontend/components/CenteredContent/CenteredContent.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import styles from './CenteredContent.scss'; +import styled from 'styled-components'; type Props = { children?: React.Element, @@ -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 ( -
- {props.children} -
+ + {children} + ); }; -CenteredContent.defaultProps = { - maxWidth: '740px', -}; - export default CenteredContent; diff --git a/frontend/components/CenteredContent/CenteredContent.scss b/frontend/components/CenteredContent/CenteredContent.scss deleted file mode 100644 index f41b19c2..00000000 --- a/frontend/components/CenteredContent/CenteredContent.scss +++ /dev/null @@ -1,4 +0,0 @@ -.content { - width: 100%; - margin: 40px 20px; -} \ No newline at end of file diff --git a/frontend/components/DocumentPreview/DocumentPreview.js b/frontend/components/DocumentPreview/DocumentPreview.js index 0817d721..03bf9c05 100644 --- a/frontend/components/DocumentPreview/DocumentPreview.js +++ b/frontend/components/DocumentPreview/DocumentPreview.js @@ -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 ( - - +class DocumentPreview extends Component { + props: Props; + + render() { + const { document, innerRef, ...rest } = this.props; + + return ( + { /> - - ); -}; + ); + } +} export default DocumentPreview; diff --git a/frontend/scenes/Search/Search.js b/frontend/scenes/Search/Search.js index 2365f4ec..30dad695 100644 --- a/frontend/scenes/Search/Search.js +++ b/frontend/scenes/Search/Search.js @@ -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 = ; + 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> ); } diff --git a/frontend/scenes/Search/Search.scss b/frontend/scenes/Search/Search.scss deleted file mode 100644 index 7b440a2a..00000000 --- a/frontend/scenes/Search/Search.scss +++ /dev/null @@ -1,6 +0,0 @@ -.icon { - width: 38px; - margin-bottom: -5px; - margin-right: 10px; - opacity: 0.15; -} diff --git a/frontend/scenes/Search/components/SearchField/SearchField.js b/frontend/scenes/Search/components/SearchField/SearchField.js index 87650891..9046e1db 100644 --- a/frontend/scenes/Search/components/SearchField/SearchField.js +++ b/frontend/scenes/Search/components/SearchField/SearchField.js @@ -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> ); } } diff --git a/frontend/scenes/Search/components/SearchField/SearchField.scss b/frontend/scenes/Search/components/SearchField/SearchField.scss deleted file mode 100644 index d0193334..00000000 --- a/frontend/scenes/Search/components/SearchField/SearchField.scss +++ /dev/null @@ -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; - } -} diff --git a/package.json b/package.json index a108a8c8..3baea121 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 57245f23..2349f00e 100644 --- a/yarn.lock +++ b/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: