Skip to content

Commit

Permalink
feat(docs): complete keyboard nav search (#441)
Browse files Browse the repository at this point in the history
  • Loading branch information
levithomason committed Aug 26, 2016
1 parent 17ecae1 commit 89a3ca1
Showing 1 changed file with 137 additions and 32 deletions.
169 changes: 137 additions & 32 deletions docs/app/Components/Sidebar/Sidebar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'lodash/fp'
import React, { Component, PropTypes } from 'react'
import { findDOMNode } from 'react-dom'
import { Link } from 'react-router'
import { Link, routerShape } from 'react-router'

import pkg from 'package.json'
import * as stardust from 'src'
Expand All @@ -14,13 +14,40 @@ import {
Input,
} from 'src'

const parentComponents = _.flow(
_.filter(META.isParent),
_.sortBy('_meta.name')
)(stardust)

const getRoute = (_meta) => `/${_meta.type}s/${_.kebabCase(_meta.name)}`

const MenuItem = ({ meta, children, ...rest }) => (
<Link key={meta.name} to={getRoute(meta)} {...rest}>
{children || meta.name}
</Link>
)
MenuItem.propTypes = {
activeClassName: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
meta: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired,
}
MenuItem.defaultProps = {
activeClassName: 'active',
className: 'item',
}
const selectedItemLabel = <span style={{ color: '#4db', float: 'right' }}>Press Enter</span>

export default class Sidebar extends Component {
static contextTypes = {
router: routerShape,
}
static propTypes = {
style: PropTypes.object,
}
state = { query: '' }

handleSearchChange = e => this.setState({ query: e.target.value })
filteredComponents = parentComponents

componentDidMount() {
document.addEventListener('keydown', this.handleDocumentKeyDown)
Expand All @@ -39,35 +66,106 @@ export default class Sidebar extends Component {
if (!hasModifier && isAZ && bodyHasFocus) this._searchInput.focus()
}

renderItemsByType = (type) => {
handleItemClick = () => {
const { query } = this.state

if (query) this.setState({ query: '' })
if (document.activeElement === this._searchInput) this._searchInput.blur()
}

handleSearchChange = e => this.setState({
selectedItemIndex: 0,
query: e.target.value,
})

handleSearchKeyDown = e => {
const { router } = this.context
const { selectedItemIndex } = this.state
const code = keyboardKey.getCode(e)

if (code === keyboardKey.Enter && this.selectedRoute) {
e.preventDefault()
router.push(this.selectedRoute)
this.selectedRoute = null
this._searchInput.blur()
this.setState({ query: '' })
}

if (code === keyboardKey.ArrowDown) {
e.preventDefault()
const next = _.min([selectedItemIndex + 1, this.filteredComponents.length - 1])
this.selectedRoute = getRoute(this.filteredComponents[next]._meta)
this.setState({ selectedItemIndex: next })
}

if (code === keyboardKey.ArrowUp) {
e.preventDefault()
const next = _.max([selectedItemIndex - 1, 0])
this.selectedRoute = getRoute(this.filteredComponents[next]._meta)
this.setState({ selectedItemIndex: next })
}
}

menuItemsByType = _.map((type) => {
const items = _.flow(
_.filter(_.overEvery([
META.isParent,
META.isType(type),
({ _meta }) => new RegExp(this.state.query, 'i').test(_meta.name),
])),
_.sortBy('_meta.name'),
_.map(({ _meta }) => {
const route = `/${_meta.type}s/${_.kebabCase(_meta.name)}`

return (
<Link to={route} className='item' activeClassName='active' key={_meta.name}>
{_meta.name}
</Link>
)
})
)(stardust)

return _.isEmpty(items) ? [] : (
_.filter(META.isType(type)),
_.map(({ _meta }) => (
<MenuItem key={_meta.name} meta={_meta} onClick={this.handleItemClick} />
))
)(parentComponents)

return (
<div className='item' key={type}>
<div className='header'>{_.capitalize(type)}s</div>
<div className='menu'>{items}</div>
</div>
)
}, typeOrder)

renderSearchItems = () => {
const { selectedItemIndex, query } = this.state
if (!query) return

let itemIndex = -1
const startsWithMatches = []
const containsMatches = []

_.each(component => {
if (new RegExp(`^${_.escapeRegExp(query)}`, 'i').test(component._meta.name)) {
startsWithMatches.push(component)
} else if (new RegExp(_.escapeRegExp(query), 'i').test(component._meta.name)) {
containsMatches.push(component)
}
}, parentComponents)

this.filteredComponents = [...startsWithMatches, ...containsMatches]
const menuItems = _.map(({ _meta }) => {
itemIndex++
const isSelected = itemIndex === selectedItemIndex

if (isSelected) this.selectedRoute = getRoute(_meta)

return (
<MenuItem
key={_meta.name}
meta={_meta}
className={isSelected ? 'active item' : 'item'}
// don't show the current route as active
activeClassName=''
onClick={this.handleItemClick}
>
{_meta.name}
{isSelected && selectedItemLabel}
</MenuItem>
)
}, this.filteredComponents)

return <div className='menu'>{menuItems}</div>
}

render() {
const { style } = this.props
const { query } = this.state
return (
<Menu className='vertical fixed inverted' style={{ ...style }}>
<div className='item'>
Expand All @@ -77,27 +175,34 @@ export default class Sidebar extends Component {
<small><em>{pkg.version}</em></small>
</strong>
</div>
<Link to='/introduction' className='item' activeClassName='active'>
Introduction
</Link>
<a className='item' href='https://github.com/TechnologyAdvice/stardust'>
<Icon name='github' /> GitHub
</a>
<div className='item'>
<div className='header'>Getting Started</div>
<div className='menu'>
<Link to='/introduction' className='item' activeClassName='active'>
Introduction
</Link>
<a className='item' href='https://github.com/TechnologyAdvice/stardust'>
<Icon name='github' /> GitHub
</a>
<a className='item' href='https://github.com/TechnologyAdvice/stardust/blob/master/CHANGELOG.md'>
<Icon name='file text outline' /> CHANGELOG
</a>
</div>
</div>
<div className='item'>
<Input
className='transparent inverted icon'
icon='search'
placeholder='Start typing...'
value={query}
onChange={this.handleSearchChange}
onKeyDown={this.handleSearchKeyDown}
ref={(c) => {
if (c !== null) this._searchInput = findDOMNode(c).querySelector('input')
}}
/>
</div>
{_.map(this.renderItemsByType, typeOrder)}
<a className='item' href='https://github.com/TechnologyAdvice/stardust/blob/master/CHANGELOG.md'>
<Icon name='file text outline' /> CHANGELOG
</a>
{query ? this.renderSearchItems() : this.menuItemsByType}
</Menu>
)
}
Expand Down

0 comments on commit 89a3ca1

Please sign in to comment.