Skip to content
This repository has been archived by the owner on Apr 1, 2019. It is now read-only.

Commit

Permalink
Merge ef01e6e into d350291
Browse files Browse the repository at this point in the history
  • Loading branch information
rlr committed Jan 26, 2016
2 parents d350291 + ef01e6e commit 0dac0b4
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 21 deletions.
185 changes: 178 additions & 7 deletions src/components/Search/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,55 @@ const Platform = require("lib/platform");
const Search = React.createClass({
getInitialState: function () {
return {
focus: false
focus: false,
// The following indexes are to keep track of where the user is if they
// are navigating the widget with the keyboard.
activeIndex: -1,
activeSuggestionIndex: -1,
activeEngineIndex: -1
};
},
setValueAndSuggestions: function (value) {
this.setState({activeIndex: -1, activeSuggestionIndex: -1});
this.props.dispatch(actions.updateSearchString(value));
this.props.dispatch(actions.getSuggestions(this.props.Search.currentEngine.name, value));
},
setValueAndClose: function (value) {
this.props.dispatch(actions.updateSearchString(value));
this.refs.input.blur();
getActiveSuggestion: function () {
// Returns the active/highlighted suggestion, if any.
const suggestions = this.props.Search.suggestions;
const index = this.state.activeSuggestionIndex;
return (suggestions && suggestions.length && index >= 0) ? suggestions[index] : null;
},
getActiveEngine: function () {
// The active engine is different from the current engine. The current
// engine is the current default search engine of the user. The active engine
// is set if any of the alternate search engines has been selected via the
// keyboard.
const index = this.state.activeEngineIndex;
if (index >= 0) {
return this.props.Search.engines[index];
}
return null;
},
getSettingsButtonIsActive: function () {
const index = this.state.activeIndex;
const numSuggestions = this.props.Search.suggestions.length;
const numEngines = this.props.Search.engines.length;
return index === numSuggestions + numEngines;
},
getActiveDescendantId: function () {
// Returns the ID of the element being currently in focus, if any.
const index = this.state.activeIndex;
const numSuggestions = this.props.Search.suggestions.length;
const numEngines = this.props.Search.engines.length;
if (index < numSuggestions) {
return "search-magic-suggestions-" + index;
} else if (index < numSuggestions + numEngines) {
return "search-magic-other-search-partners-" + (index - numSuggestions);
} else if (index === numSuggestions + numEngines) {
return "search-magic-settings-button";
}
return null;
},
performSearch: function (options) {
Platform.search.performSearch({
Expand All @@ -26,25 +65,157 @@ const Search = React.createClass({
searchPurpose: "d"
});
},
handleKeypress: function (evt) {
// Handle the keyboard navigation of the widget.
const index = this.state.activeIndex;
const numSuggestions = this.props.Search.suggestions.length;
const numEngines = this.props.Search.engines.length;
let newIndex = index;
let newSuggestionIndex = this.state.activeSuggestionIndex;
let newEngineIndex = this.state.activeEngineIndex;
switch (evt.key) {
case "ArrowDown":
if (index < numSuggestions + numEngines) {
newIndex++;
if (index < numSuggestions - 1) {
// We are in suggestions, move down until the last one.
newSuggestionIndex++;
} else if (index === numSuggestions - 1) {
// We are on the last suggestion, reset suggestion index and
// start on the engine index.
newSuggestionIndex = -1;
newEngineIndex++;
} else if (index < numSuggestions + numEngines - 1) {
// We are in engines, keep going until the last one.
newEngineIndex++;
} else if (index === numSuggestions + numEngines - 1) {
// We are on the last engine, reset engine index.
newEngineIndex = -1;
}
} else {
// We reached the end. Reset to -1.
newIndex = -1;
}
break;
case "ArrowUp":
if (index > -1) {
newIndex--;
if (index < numSuggestions) {
// We are in suggestions, move on up.
newSuggestionIndex--;
} else if (index === numSuggestions) {
// We are on the first engine, reset engine index and move to
// last suggestion.
newEngineIndex = -1;
newSuggestionIndex = numSuggestions - 1;
} else if (index < numSuggestions + numEngines) {
// We are on the engine list, move on up.
newEngineIndex--;
} else {
// We are on the button, move to last engine.
newEngineIndex = numEngines - 1;
}
} else {
// Nothing is selected, go to the very end.
newIndex = numSuggestions + numEngines;
}
break;
case "Tab":
// Tab only navigates through the engines list.
if (!evt.shiftKey) {
// Shift isn't pressed, go forward.
if (index === numSuggestions + numEngines) {
// We reached the end, let the event go on.
return;
}
if (index < numSuggestions) {
// We aren't in the engines list yet, move to first engine.
newIndex = numSuggestions;
newEngineIndex = 0;
} else {
// We are in the engines list, move along.
newIndex++;
newEngineIndex++;
}
} else {
// Shift is pressed, go backward.
if (index < numSuggestions) {
// We aren't on the engines list, ;et the event move on.
return;
}
if (index === numSuggestions) {
// We are on the first engine, unselect it and go to where we were
// in the suggestions list.
newEngineIndex = -1;
newIndex = newSuggestionIndex;
} else if (index < numSuggestions + numEngines) {
// We are in the engines list, move up the list.
newIndex--;
newEngineIndex--;
} else {
// We are on the button, go to bottom of engine list.
newIndex--;
newEngineIndex = numEngines - 1;
}
}
break;
case "Enter":
evt.preventDefault();
// If the change settings button is selected, fire the action for it.
if (this.getSettingsButtonIsActive()) {
Platform.search.manageEngines();
return;
}

// Otherwise, perform the search with active engine and suggestion.
this.performSearch({
engineName: this.getActiveEngine(),
searchString: this.getActiveSuggestion() || this.props.Search.searchString
});
return;
default:
return;
}

evt.preventDefault();
this.setState({
activeIndex: newIndex,
activeSuggestionIndex: newSuggestionIndex,
activeEngineIndex: newEngineIndex
});
},
resetState: function () {
this.setState(this.getInitialState());
},
render: function () {
const {currentEngine, searchString} = this.props.Search;
const showSearchMagic = !!(searchString && this.state.focus);
return (<form className="search">
<div className="search-input-wrapper">
<div className="search-icon" />
<input ref="input" className="search-input" type="search"
aria-label="Search query" aria-autocomplete="true"
aria-controls="search-magic-container"
aria-expanded={showSearchMagic}
aria-activedescendant={this.getActiveDescendantId()}
autoComplete="off" placeholder="Search" maxLength="256"
value={searchString}
onChange={e => this.setValueAndSuggestions(e.target.value)}
onFocus={() => this.setState({focus: true})}
onBlur={() => setTimeout(() => this.setState({focus: false}), 200)} />
onBlur={() => setTimeout(() => this.resetState(), 200)}
onKeyDown={e => this.handleKeypress(e)} />
<button onClick={e => {
e.preventDefault();
this.performSearch({engineName: currentEngine.name, searchString});
}} className="search-submit">
}} className="search-submit" aria-label="Submit search">
<span className="sr-only" >Search</span>
</button>
<SearchMagic
show={searchString && this.state.focus}
show={showSearchMagic}
performSearch={this.performSearch}
activeSuggestion={this.getActiveSuggestion()}
activeEngine={this.getActiveEngine()}
settingsButtonIsActive={this.getSettingsButtonIsActive()}
manageEngines={() => Platform.search.manageEngines()}
{...this.props.Search} />
</div>
Expand Down
37 changes: 26 additions & 11 deletions src/components/SearchMagic/SearchMagic.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,23 @@ const SearchMagic = React.createClass({
const currentEngine = this.props.currentEngine;
const performSearch = this.props.performSearch;
const currentIcon = currentEngine.icons[0] || {};
return (<div className="search-magic" hidden={!this.props.show}>
let suggestionsIdIndex = 0;
let enginesIdIndex = 0;
return (<div id="search-magic-container" className="search-magic" role="presentation" hidden={!this.props.show}>
<section className="search-magic-title" hidden={!this.props.suggestions.length}>
<Icon padded {...currentIcon} /> {currentEngine.placeholder}
</section>
<section className="search-magic-suggestions" hidden={!this.props.suggestions.length}>
<ul>
<ul role="listbox">
{this.props.suggestions.map(suggestion => {
return (<li key={suggestion}>
<a onClick={() => performSearch({
engineName: currentEngine.name, searchString: suggestion
const active = (suggestion === this.props.activeSuggestion);
const activeEngine = this.props.activeEngine || this.props.currentEngine;
return (<li key={suggestion} role="presentation">
<a id={"search-magic-suggestions-" + suggestionsIdIndex++ }
className={active ? "active" : ""} role="option"
aria-selected={active}
onClick={() => performSearch({
engineName: activeEngine.name, searchString: suggestion
})}>{suggestion}</a>
</li>);
})}
Expand All @@ -42,21 +49,26 @@ const SearchMagic = React.createClass({
<section className="search-magic-title">
<span>Search for <strong>{this.props.searchString}</strong> with:</span>
</section>
<section className="search-magic-other-search-partners">
<section className="search-magic-other-search-partners" role="group">
<ul>
{this.props.engines.map(option => {
const icon = option.icons[0];
return (<li key={option.name}>
<a onClick={() => performSearch({engineName: option.name, searchString: this.props.searchString})}>
const active = this.props.activeEngine && (this.props.activeEngine.name === option.name);
return (<li key={option.name} className={active ? "active" : ""}>
<a id={"search-magic-other-search-partners-" + enginesIdIndex++ } aria-selected={active}
onClick={() => performSearch({engineName: option.name, searchString: this.props.searchString})}>
<Icon {...icon} alt={option.name} /></a>
</li>);
})}
</ul>
</section>
<section className="search-magic-settings">
<button onClick={(e) => {
e.preventDefault();
this.props.manageEngines();
<button id="search-magic-settings-button"
className={this.props.settingsButtonIsActive ? "active" : ""}
aria-selected={this.props.settingsButtonIsActive}
onClick={(e) => {
e.preventDefault();
this.props.manageEngines();
}}>
Change Search Settings
</button>
Expand All @@ -72,6 +84,9 @@ const EngineShape = React.PropTypes.shape({

SearchMagic.propTypes = {
currentEngine: EngineShape.isRequired,
activeEngine: EngineShape,
activeSuggestion: React.PropTypes.string,
settingsButtonIsActive: React.PropTypes.bool,
suggestions: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
engines: React.PropTypes.arrayOf(EngineShape).isRequired,
searchString: React.PropTypes.string,
Expand Down
9 changes: 6 additions & 3 deletions src/components/SearchMagic/SearchMagic.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
li a {
display: block;
padding: 5px 5px 5px 20px;
&:hover {
&:hover,
&.active {
background: $magic-blue;
color: white;
}
Expand All @@ -57,7 +58,8 @@
li {
display: inline-block;
padding: 5px 0;
&:hover {
&:hover,
&.active {
background: $magic-blue;
color: white;
a {border-color: transparent;}
Expand All @@ -81,7 +83,8 @@
text-align: center;
width: 100%;
display: block;
&:hover {
&:hover,
&.active {
background: darken($magic-grey, 5%);
}
}
Expand Down

0 comments on commit 0dac0b4

Please sign in to comment.