Skip to content

Commit 7f094a3

Browse files
committed
SearchWidget: use PopupMenu to display key-navigatable results
1 parent 1b3db73 commit 7f094a3

File tree

2 files changed

+31
-58
lines changed

2 files changed

+31
-58
lines changed

components/widgets/SearchWidget.jsx

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import PropTypes from 'prop-types';
1515
import {v4 as uuidv4} from 'uuid';
1616

1717
import LocaleUtils from '../../utils/LocaleUtils';
18-
import MiscUtils from '../../utils/MiscUtils';
1918
import {SearchResultType} from '../../utils/SearchProviders';
2019
import VectorLayerUtils from '../../utils/VectorLayerUtils';
2120
import Icon from '../Icon';
2221
import InputContainer from './InputContainer';
22+
import PopupMenu from './PopupMenu';
2323
import Spinner from './Spinner';
2424

2525
import './style/SearchWidget.css';
@@ -28,8 +28,6 @@ import './style/SearchWidget.css';
2828
export default class SearchWidget extends React.Component {
2929
static propTypes = {
3030
className: PropTypes.string,
31-
onBlur: PropTypes.func,
32-
onFocus: PropTypes.func,
3331
placeholder: PropTypes.string,
3432
queryGeometries: PropTypes.bool,
3533
resultSelected: PropTypes.func.isRequired,
@@ -42,8 +40,6 @@ export default class SearchWidget extends React.Component {
4240
value: PropTypes.string
4341
};
4442
static defaultProps = {
45-
onBlur: () => {},
46-
onFocus: () => {},
4743
resultTypeFilter: [SearchResultType.PLACE],
4844
searchParams: {},
4945
searchProviders: []
@@ -53,7 +49,7 @@ export default class SearchWidget extends React.Component {
5349
reqId: null,
5450
results: [],
5551
pending: 0,
56-
active: false
52+
resultsVisible: false
5753
};
5854
constructor(props) {
5955
super(props);
@@ -75,8 +71,8 @@ export default class SearchWidget extends React.Component {
7571
<div className="search-widget-container">
7672
<InputContainer>
7773
<input
78-
onBlur={this.onBlur}
7974
onChange={this.textChanged}
75+
onClick={() => this.setState({resultsVisible: true})}
8076
onFocus={this.onFocus}
8177
onKeyDown={this.onKeyDown}
8278
placeholder={this.props.placeholder ?? LocaleUtils.tr("search.search")}
@@ -86,31 +82,29 @@ export default class SearchWidget extends React.Component {
8682
value={this.state.text} />
8783
{this.state.pending > 0 ? (<Spinner role="suffix" />) : (<Icon icon="clear" onClick={this.clear} role="suffix" />)}
8884
</InputContainer>
89-
{(!isEmpty(this.state.results) || this.state.pending > 0) && this.state.active ? this.renderResults() : null}
85+
{(!isEmpty(this.state.results) || this.state.pending > 0) && this.state.resultsVisible ? this.renderResults() : null}
9086
</div>
9187
);
9288
}
9389
renderResults = () => {
9490
return (
95-
<div className="search-widget-results" onMouseDown={this.setPreventBlur}>
96-
{this.state.results.filter(group => this.props.resultTypeFilter.includes(group.type ?? SearchResultType.PLACE)).map(group => (
97-
<div className="search-widget-results-group" key={group.id} onMouseDown={MiscUtils.killEvent}>
98-
<div className="search-widget-results-group-title"><span>{group.title ?? LocaleUtils.tr(group.titlemsgid)}</span></div>
99-
{group.items.map(item => {
100-
item.text = (item.label !== undefined ? item.label : item.text || '').replace(/<\/?\w+\s*\/?>/g, '');
101-
return (
102-
<div className="search-widget-results-group-item" key={item.id} onClick={() => this.resultSelected(group, item)} title={item.text}>{item.text}</div>
103-
);
104-
})}
105-
</div>
106-
))}
107-
</div>
91+
<PopupMenu anchor={this.input} className="search-widget-results" onClose={() => this.setState({resultsVisible: false})} setMaxWidth spaceKeyActivation={false}>
92+
{this.state.results.filter(group => this.props.resultTypeFilter.includes(group.type ?? SearchResultType.PLACE)).map(group => {
93+
return [(
94+
<div className="search-widget-results-group-title" disabled key={group.id}>
95+
<span>{group.title ?? LocaleUtils.tr(group.titlemsgid)}</span>
96+
</div>
97+
),
98+
group.items.map(item => {
99+
item.text = (item.label !== undefined ? item.label : item.text || '').replace(/<\/?\w+\s*\/?>/g, '');
100+
return (
101+
<div className="search-widget-results-item" key={group.id + ":" + item.id} onClick={() => this.resultSelected(group, item)} title={item.text}>{item.text}</div>
102+
);
103+
})];
104+
}).flat()}
105+
</PopupMenu>
108106
);
109107
};
110-
setPreventBlur = () => {
111-
this.preventBlur = true;
112-
setTimeout(() => {this.preventBlur = false; return false;}, 100);
113-
};
114108
textChanged = (ev) => {
115109
this.setState({text: ev.target.value, reqId: null, results: [], pending: 0});
116110
clearTimeout(this.searchTimeout);
@@ -120,23 +114,17 @@ export default class SearchWidget extends React.Component {
120114
this.searchTimeout = setTimeout(this.startSearch, 250);
121115
}
122116
};
123-
onBlur = () => {
124-
if (!this.preventBlur) {
125-
clearTimeout(this.searchTimeout);
126-
this.props.onBlur();
127-
this.setState({active: false});
128-
}
129-
};
130117
onFocus = (ev) => {
131-
ev.target.select();
132-
this.props.onFocus();
133-
this.setState({active: true});
118+
if (this.input && !this.state.resultsVisible) {
119+
ev.target.select();
120+
}
134121
};
135122
onKeyDown = (ev) => {
136123
if (ev.key === 'Enter') {
137124
this.startSearch();
138-
} else if (ev.key === 'Escape') {
139-
ev.target.blur();
125+
} else if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
126+
ev.preventDefault();
127+
this.setState({resultsVisible: true});
140128
}
141129
};
142130
startSearch = () => {
@@ -158,7 +146,8 @@ export default class SearchWidget extends React.Component {
158146
}
159147
return {
160148
results: [...state.results, ...response.results.map(group => ({...group, provider}))],
161-
pending: state.pending - 1
149+
pending: state.pending - 1,
150+
resultsVisible: true
162151
};
163152
});
164153
}, axios);
@@ -186,7 +175,7 @@ export default class SearchWidget extends React.Component {
186175
}
187176
};
188177
clear = () => {
189-
this.setState({results: [], text: ""});
178+
this.setState({results: [], text: "", resultsVisible: false});
190179
this.props.resultSelected(null);
191180
};
192181
}

components/widgets/style/SearchWidget.css

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,15 @@ div.search-widget-container > div.input-container > div.spinner {
1111
height: 2em;
1212
}
1313

14-
div.search-widget-results {
15-
position: absolute;
16-
left: 0;
17-
right: 0;
18-
top: 100%;
19-
border: 1px solid var(--border-color);
20-
background-color: var(--list-bg-color);
21-
max-height: 10em;
22-
overflow-y: auto;
23-
z-index: 1;
24-
box-shadow: 0px 2px 4px rgba(136, 136, 136, 0.5);
25-
}
2614

2715
div.search-widget-results-group-title {
2816
background-color: var(--list-section-bg-color);
2917
color: var(--list-section-text-color);
30-
font-weight: bold;;
31-
}
32-
33-
div.search-widget-results-group-item:hover {
34-
background-color: var(--list-item-bg-color-hover);
35-
color: var(--list-item-text-color-hover);
18+
font-weight: bold;
19+
padding: 0.25em;
3620
}
3721

38-
div.search-widget-results-group div {
22+
div.search-widget-results-item {
3923
padding: 0.25em;
4024
white-space: nowrap;
4125
overflow: hidden;

0 commit comments

Comments
 (0)