Skip to content

Commit

Permalink
[WIP] Search: Add dropdown with search suggestions
Browse files Browse the repository at this point in the history
 - [16e369c] added suggestion dropdown, listing all tags for now
 - [4e0688d] added single tag icon
 - [e803ce9] filter search for tag using .includes
 - [128e35b] add update and debounce back in
 - [e4c1a19] handle special characters in tags
 - [337ca13] clean up styles, add dark mode styles
 - [7e91ed8] tune tag search a bit
 - [c2f843e] fix styles for wrapping long tag names, add some padding
  • Loading branch information
dmsnell committed Oct 22, 2019
1 parent e54b2ae commit fd65592
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 22 deletions.
16 changes: 16 additions & 0 deletions lib/icons/tag.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

export default function TagIcon() {
return (
<svg
className="icon-tag"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<rect x="0" fill="none" width="24" height="24" />
<path d="M18.29,5.71v5.17l-7,7L6.12,12.71l7-7h5.17m2-2h-8L4,12a1,1,0,0,0,0,1.41L10.59,20A1,1,0,0,0,12,20l8.29-8.29v-8Zm-4.5,5.5a1,1,0,1,0-1-1A1,1,0,0,0,15.79,9.21Z" />
</svg>
);
}
61 changes: 39 additions & 22 deletions lib/search-field/index.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { debounce, isEmpty } from 'lodash';
import SmallCrossIcon from '../icons/cross-small';
import appState from '../flux/app-state';
import { tracks } from '../analytics';
import SearchSuggestions from '../search-suggestions';

const { search, setSearchFocus } = appState.actionCreators;
const { recordEvent } = tracks;
const KEY_ESC = 27;
const KEY_ENTER = 13;
const SEARCH_DELAY = 500;

export class SearchField extends Component {
Expand All @@ -24,6 +26,7 @@ export class SearchField extends Component {

state = {
query: '',
searchSelected: false,
};

componentDidUpdate() {
Expand All @@ -36,21 +39,29 @@ export class SearchField extends Component {
}
}

doSearch = query => {
this.setState({ query, searchSelected: true });
this.debouncedSearch(query);
};

interceptEsc = event => {
if (KEY_ESC === event.keyCode) {
if (this.state.query === '') {
this.inputField.blur();
}
this.clearQuery();
}
if (KEY_ENTER === event.keyCode) {
this.doSearch(this.state.query);
}
};

storeInput = r => (this.inputField = r);

debouncedSearch = debounce(query => this.props.onSearch(query), SEARCH_DELAY);

update = ({ target: { value: query } }) => {
this.setState({ query });
this.setState({ query, searchSelected: false });
this.debouncedSearch(query);
};

Expand All @@ -62,32 +73,38 @@ export class SearchField extends Component {

render() {
const { isTagSelected, placeholder } = this.props;
const { query } = this.state;
const { query, searchSelected } = this.state;
const hasQuery = query && query.length > 0;
const shouldShowSuggestions = hasQuery && !searchSelected;

const screenReaderLabel =
'Search ' + (isTagSelected ? 'notes with tag ' : '') + placeholder;

return (
<div className="search-field">
<input
aria-label={screenReaderLabel}
ref={this.storeInput}
type="text"
placeholder={placeholder}
onChange={this.update}
onKeyUp={this.interceptEsc}
value={query}
spellCheck={false}
/>
<button
aria-label="Clear search"
hidden={!hasQuery}
onClick={this.clearQuery}
>
<SmallCrossIcon />
</button>
</div>
<Fragment>
<div className="search-field">
<input
aria-label={screenReaderLabel}
ref={this.storeInput}
type="text"
placeholder={placeholder}
onChange={this.update}
onKeyUp={this.interceptEsc}
value={query}
spellCheck={false}
/>
<button
aria-label="Clear search"
hidden={!hasQuery}
onClick={this.clearQuery}
>
<SmallCrossIcon />
</button>
{shouldShowSuggestions && (
<SearchSuggestions query={query} onSearch={this.doSearch} />
)}
</div>
</Fragment>
);
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/search-field/style.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.search-field {
display: flex;
position: relative;
flex-direction: row;
align-items: center;
width: 100%;
Expand Down
57 changes: 57 additions & 0 deletions lib/search-suggestions/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import SmallSearchIcon from '../icons/search-small';
import TagIcon from '../icons/tag';

export class SearchSuggestions extends Component {
static displayName = 'SearchSuggestions';

static propTypes = {
onSearch: PropTypes.func.isRequired,
query: PropTypes.string.isRequired,
tags: PropTypes.array.isRequired,
};

render() {
const { query, onSearch, tags } = this.props;
// const screenReaderLabel =
// 'Search ' + (isTagSelected ? 'notes with tag ' : '') + placeholder;
const shouldShowTagSuggestions = query.length > 2;

return (
<div className="search-suggestions">
<div className="search-suggestion-row" onClick={() => onSearch(query)}>
<SmallSearchIcon />
<div className="search-suggestion">{query}</div>
</div>
{shouldShowTagSuggestions &&
tags
.filter(function(tag) {
return tag.id.startsWith(encodeURIComponent(query));
})
.map(tag => (
<div
key={tag.id}
className="search-suggestion-row"
onClick={() => onSearch(`tag:${tag.id}`)}
>
<TagIcon />
<div className="search-suggestion">
{decodeURIComponent(tag.id)}
</div>
</div>
))}
</div>
);
}
}

const mapStateToProps = ({ appState: state }) => ({
tags: state.tags,
});

export default connect(
mapStateToProps,
null
)(SearchSuggestions);
56 changes: 56 additions & 0 deletions lib/search-suggestions/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.search-suggestions {
position: absolute;
top: 32px;
left: 0;
z-index: 10;
width: 100%;
padding: 0 5px;
border-radius: 5px;
border: 1px solid $studio-gray-10;

.search-suggestion-row {
cursor: pointer;
padding: 5px;
position: relative;
}
.search-suggestion {
overflow-wrap: anywhere;
margin-left: 28px;
}

.icon-search-small, .icon-tag {
position: absolute;
transition: $anim-transition;
width: 24px;
}
}

.theme-light {
.search-suggestions {
background: $studio-white;
}

.search-suggestion-row {
.icon-search-small, .icon-tag {
fill: $studio-gray-50;
}
&:hover {
background: $studio-blue-5;
}
}
}

.theme-dark {
.search-suggestions {
background: $studio-gray-90;
color: $studio-white;
}
.search-suggestion-row {
.icon-search-small, .icon-tag {
fill: $studio-gray-30;
}
&:hover {
background: $studio-gray-70;
}
}
}
1 change: 1 addition & 0 deletions scss/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
@import 'revision-selector/style';
@import 'search-bar/style';
@import 'search-field/style';
@import 'search-suggestions/style';
@import 'tag-email-tooltip/style';
@import 'tag-field/style';
@import 'tag-input/style';
Expand Down

0 comments on commit fd65592

Please sign in to comment.