diff --git a/docs/app/Components/Sidebar/Sidebar.js b/docs/app/Components/Sidebar/Sidebar.js index 52a2f3adc2..722aa81975 100644 --- a/docs/app/Components/Sidebar/Sidebar.js +++ b/docs/app/Components/Sidebar/Sidebar.js @@ -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' @@ -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 }) => ( + + {children || meta.name} + +) +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 = Press Enter + 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) @@ -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 ( - - {_meta.name} - - ) - }) - )(stardust) - - return _.isEmpty(items) ? [] : ( + _.filter(META.isType(type)), + _.map(({ _meta }) => ( + + )) + )(parentComponents) + + return (
{_.capitalize(type)}s
{items}
) + }, 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 ( + + {_meta.name} + {isSelected && selectedItemLabel} + + ) + }, this.filteredComponents) + + return
{menuItems}
} render() { const { style } = this.props + const { query } = this.state return (
@@ -77,27 +175,34 @@ export default class Sidebar extends Component { {pkg.version}
- - Introduction - - - GitHub - +
+
Getting Started
+
+ + Introduction + + + GitHub + + + CHANGELOG + +
+
{ if (c !== null) this._searchInput = findDOMNode(c).querySelector('input') }} />
- {_.map(this.renderItemsByType, typeOrder)} - - CHANGELOG - + {query ? this.renderSearchItems() : this.menuItemsByType}
) }