Switch to Search component in tag selector and support X/Esc to clear

This moves debouncing into the search component and adds cancel behavior
from the XUL search textbox. For now, this uses the X button from
Firefox.
This commit is contained in:
Dan Stillman 2019-03-20 04:42:53 -04:00
parent 14d25d273a
commit 42667e7090
6 changed files with 138 additions and 21 deletions

View file

@ -0,0 +1,93 @@
'use strict';
const React = require('react');
const PropTypes = require('prop-types');
class Search extends React.PureComponent {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
state = {
immediateValue: this.props.value
};
static getDerivedStateFromProps(props, state) {
var prevProps = state.prevProps || {};
return {
prevProps: props,
immediateValue: prevProps.value !== props.value
? props.value
: state.immediateValue
};
}
handleInput = (event) => {
var value = event.target.value;
// Update controlled value and cancel button immediately
this.setState({
immediateValue: value
});
// Debounce the search based on the timeout
if (this._timeout) {
clearTimeout(this._timeout);
}
this._timeout = this.props.timeout
&& setTimeout(() => this.props.onSearch(value), this.props.timeout);
}
handleClear = () => {
if (this._timeout) {
clearTimeout(this._timeout);
}
this.setState({
immediateValue: ''
});
this.props.onSearch('');
}
handleKeyDown = (event) => {
if (event.key == 'Escape') {
this.handleClear();
}
}
focus() {
this.inputRef.focus();
}
render() {
return (
<div className="search">
<input
type="search"
onInput={this.handleInput}
onKeyDown={this.handleKeyDown}
ref={this.inputRef}
value={this.state.immediateValue}
/>
{this.state.immediateValue !== ''
? <div
className="search-cancel-button"
onClick={this.handleClear}/>
: ''}
</div>
);
}
static propTypes = {
inputRef: PropTypes.object,
onSearch: PropTypes.func,
timeout: PropTypes.number,
value: PropTypes.string,
};
static defaultProps = {
onSearch: () => {},
timeout: 300,
value: '',
};
}
module.exports = Search;

View file

@ -6,6 +6,7 @@ const TagList = require('./tag-selector/tag-list');
const Input = require('./form/input');
const { Button } = require('./button');
const { IconTagSelectorMenu } = require('./icons');
const Search = require('./search');
class TagSelector extends React.Component {
render() {
@ -13,13 +14,11 @@ class TagSelector extends React.Component {
<div className="tag-selector">
<TagList {...this.props} />
<div className="tag-selector-filter-container">
<Input
type="search"
ref={ref => this.focusTextbox = ref && ref.focus}
<Search
value={this.props.searchString}
onChange={this.props.onSearch}
onSearch={this.props.onSearch}
inputRef={this.searchBoxRef}
className="tag-selector-filter"
size="1"
/>
<Button
icon={<IconTagSelectorMenu />}
@ -46,8 +45,8 @@ TagSelector.propTypes = {
onDragExit: PropTypes.func,
onDrop: PropTypes.func
}),
searchBoxRef: PropTypes.object,
searchString: PropTypes.string,
shouldFocus: PropTypes.bool,
onSelect: PropTypes.func,
onTagContext: PropTypes.func,
onSearch: PropTypes.func,
@ -58,7 +57,6 @@ TagSelector.propTypes = {
TagSelector.defaultProps = {
tags: [],
searchString: '',
shouldFocus: false,
onSelect: () => Promise.resolve(),
onTagContext: () => Promise.resolve(),
onSearch: () => Promise.resolve(),

View file

@ -29,6 +29,11 @@ Zotero.TagSelector = class TagSelectorContainer extends React.Component {
this.displayAllTags = Zotero.Prefs.get('tagSelector.displayAllTags');
this.selectedTags = new Set();
this.state = defaults;
this.searchBoxRef = React.createRef();
}
focusTextbox() {
this.searchBoxRef.focus();
}
// Update trigger #1 (triggered by ZoteroPane)
@ -177,9 +182,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.Component {
});
return <TagSelector
tags={tags}
ref={ref => this.focusTextbox = ref && ref.focusTextbox}
searchBoxRef={this.searchBoxRef}
searchString={this.state.searchString}
shouldFocus={this.state.shouldFocus}
dragObserver={this.dragObserver}
onSelect={this.state.viewOnly ? () => {} : this.handleTagSelected}
onTagContext={this.handleTagContext}
@ -226,9 +230,9 @@ Zotero.TagSelector = class TagSelectorContainer extends React.Component {
}
}
handleSearch = Zotero.Utilities.debounce((searchString) => {
handleSearch = (searchString) => {
this.setState({searchString});
})
}
dragObserver = {
onDragOver: function(event) {

View file

@ -20,6 +20,7 @@
// Components
// --------------------------------------------------
@import "components/tag-selector";
@import "components/button";
@import "components/icons";
@import "components/search";
@import "components/tag-selector";

View file

@ -0,0 +1,21 @@
.search {
position: relative;
}
.search input {
-moz-appearance: searchfield;
flex: 1 0;
min-width: 40px;
height: 24px;
padding-left: 4px;
}
.search .search-cancel-button {
background-image: url(chrome://global/skin/icons/searchfield-cancel.svg);
position: absolute;
width: 14px;
height: 14px;
top: 6px;
right: 6px;
cursor: default;
}

View file

@ -24,6 +24,13 @@
height: 100%;
}
.tag-selector-list {
list-style: none;
display: inline-block;
margin: 0;
padding: 0;
}
.tag-selector-filter-container {
height: auto;
flex: 0 0 1em;
@ -32,16 +39,9 @@
padding: 0.125em 0 0.125em 0.5em;
}
.tag-selector-list {
list-style: none;
display: inline-block;
margin: 0;
padding: 0;
}
.tag-selector-filter {
.tag-selector-filter-container .search {
display: flex;
flex: 1 0;
min-width: 40px;
}
.tag-selector-actions {