diff --git a/app/soapbox/components/autosuggest_input.js b/app/soapbox/components/autosuggest_input.js
index 8f750ab3f..cf5c8f04b 100644
--- a/app/soapbox/components/autosuggest_input.js
+++ b/app/soapbox/components/autosuggest_input.js
@@ -7,6 +7,7 @@ import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
+import Icon from 'soapbox/components/icon';
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;
@@ -47,21 +48,28 @@ export default class AutosuggestInput extends ImmutablePureComponent {
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
autoFocus: PropTypes.bool,
+ autoSelect: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
searchTokens: PropTypes.arrayOf(PropTypes.string),
maxLength: PropTypes.number,
+ menu: PropTypes.arrayOf(PropTypes.object),
};
static defaultProps = {
autoFocus: false,
+ autoSelect: true,
searchTokens: ImmutableList(['@', ':', '#']),
};
+ getFirstIndex = () => {
+ return this.props.autoSelect ? 0 : -1;
+ }
+
state = {
suggestionsHidden: true,
focused: false,
- selectedSuggestion: 0,
+ selectedSuggestion: this.getFirstIndex(),
lastToken: null,
tokenStart: 0,
};
@@ -81,8 +89,10 @@ export default class AutosuggestInput extends ImmutablePureComponent {
}
onKeyDown = (e) => {
- const { suggestions, disabled } = this.props;
+ const { suggestions, menu, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
+ const firstIndex = this.getFirstIndex();
+ const lastIndex = suggestions.size + (menu || []).length - 1;
if (disabled) {
e.preventDefault();
@@ -106,26 +116,33 @@ export default class AutosuggestInput extends ImmutablePureComponent {
break;
case 'ArrowDown':
- if (suggestions.size > 0 && !suggestionsHidden) {
+ if (!suggestionsHidden && (suggestions.size > 0 || menu)) {
e.preventDefault();
- this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
+ this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, lastIndex) });
}
break;
case 'ArrowUp':
- if (suggestions.size > 0 && !suggestionsHidden) {
+ if (!suggestionsHidden && (suggestions.size > 0 || menu)) {
e.preventDefault();
- this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
+ this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, firstIndex) });
}
break;
case 'Enter':
case 'Tab':
// Select suggestion
- if (suggestions.size > 0 && !suggestionsHidden) {
+ if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.size > 0 || menu)) {
e.preventDefault();
e.stopPropagation();
- this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
+ this.setState({ selectedSuggestion: firstIndex });
+
+ if (selectedSuggestion < suggestions.size) {
+ this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
+ } else {
+ const item = menu[selectedSuggestion - suggestions.size];
+ this.handleMenuItemAction(item);
+ }
}
break;
@@ -186,11 +203,51 @@ export default class AutosuggestInput extends ImmutablePureComponent {
);
}
+ handleMenuItemAction = item => {
+ this.onBlur();
+ item.action();
+ }
+
+ handleMenuItemClick = item => {
+ return e => {
+ e.preventDefault();
+ this.handleMenuItemAction(item);
+ };
+ }
+
+ renderMenu = () => {
+ const { menu, suggestions } = this.props;
+ const { selectedSuggestion } = this.state;
+
+ if (!menu) {
+ return null;
+ }
+
+ return menu.map((item, i) => (
+
+ {item.icon && (
+
+ )}
+
+ {item.text}
+
+ ));
+ };
+
render() {
- const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
+ const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
+ const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
+
if (isRtl(value)) {
style.direction = 'rtl';
}
@@ -220,8 +277,9 @@ export default class AutosuggestInput extends ImmutablePureComponent {
/>
-
+
{suggestions.map(this.renderSuggestion)}
+ {this.renderMenu()}
);
diff --git a/app/soapbox/features/compose/components/search.js b/app/soapbox/features/compose/components/search.js
index ad42208b4..f14f79a84 100644
--- a/app/soapbox/features/compose/components/search.js
+++ b/app/soapbox/features/compose/components/search.js
@@ -1,47 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import Overlay from 'react-overlays/lib/Overlay';
-import Motion from '../../ui/util/optional_motion';
-import spring from 'react-motion/lib/spring';
+import { defineMessages, injectIntl } from 'react-intl';
import Icon from 'soapbox/components/icon';
+import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input';
import classNames from 'classnames';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
+ action: { id: 'search.action', defaultMessage: 'Search for “{query}”' },
});
-class SearchPopout extends React.PureComponent {
-
- static propTypes = {
- style: PropTypes.object,
- };
-
- render() {
- const { style } = this.props;
- const extraInformation = ;
- return (
-
-
- {({ opacity, scaleX, scaleY }) => (
-
-
-
- - #example
- - @username
- - URL
- - URL
-
- {extraInformation}
-
- )}
-
-
- );
- }
-
-}
-
export default @injectIntl
class Search extends React.PureComponent {
@@ -56,13 +24,16 @@ class Search extends React.PureComponent {
onSubmit: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired,
+ onSelected: PropTypes.func,
openInRoute: PropTypes.bool,
autoFocus: PropTypes.bool,
+ autosuggest: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
static defaultProps = {
autoFocus: false,
+ ausosuggest: false,
}
state = {
@@ -81,15 +52,18 @@ class Search extends React.PureComponent {
}
}
- handleKeyUp = (e) => {
+ handleSubmit = () => {
+ this.props.onSubmit();
+
+ if (this.props.openInRoute) {
+ this.context.router.history.push('/search');
+ }
+ }
+
+ handleKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
-
- this.props.onSubmit();
-
- if (this.props.openInRoute) {
- this.context.router.history.push('/search');
- }
+ this.handleSubmit();
} else if (e.key === 'Escape') {
document.querySelector('.ui').parentElement.focus();
}
@@ -104,34 +78,51 @@ class Search extends React.PureComponent {
this.setState({ expanded: false });
}
+ handleSelected = accountId => {
+ const { onSelected } = this.props;
+
+ if (onSelected) {
+ onSelected(accountId, this.context.router.history);
+ }
+ }
+
+ makeMenu = () => {
+ const { intl, value } = this.props;
+
+ return [
+ { text: intl.formatMessage(messages.action, { query: value }), icon: require('@tabler/icons/icons/search.svg'), action: this.handleSubmit },
+ ];
+ }
+
render() {
- const { intl, value, autoFocus, submitted } = this.props;
- const { expanded } = this.state;
+ const { intl, value, autoFocus, autosuggest, submitted } = this.props;
const hasValue = value.length > 0 || submitted;
+ const Component = autosuggest ? AutosuggestAccountInput : 'input';
+
return (
);
}
diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js
index 6e144d55d..58198eac4 100644
--- a/app/soapbox/features/compose/components/search_results.js
+++ b/app/soapbox/features/compose/components/search_results.js
@@ -7,8 +7,6 @@ import StatusContainer from '../../../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Hashtag from '../../../components/hashtag';
import FilterBar from '../../search/components/filter_bar';
-import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
-import { WhoToFollowPanel } from 'soapbox/features/ui/util/async-components';
import ScrollableList from 'soapbox/components/scrollable_list';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
@@ -24,6 +22,7 @@ export default class SearchResults extends ImmutablePureComponent {
selectedFilter: PropTypes.string.isRequired,
selectFilter: PropTypes.func.isRequired,
features: PropTypes.object.isRequired,
+ suggestions: ImmutablePropTypes.list,
};
handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter);
@@ -31,15 +30,7 @@ export default class SearchResults extends ImmutablePureComponent {
handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter);
render() {
- const { value, results, submitted, selectedFilter, features } = this.props;
-
- if (!submitted && features.suggestions && results.isEmpty()) {
- return (
-
- {Component => }
-
- );
- }
+ const { value, results, submitted, selectedFilter, suggestions } = this.props;
let searchResults;
let hasMore = false;
@@ -47,14 +38,16 @@ export default class SearchResults extends ImmutablePureComponent {
let noResultsMessage;
let placeholderComponent = PlaceholderStatus;
- if (selectedFilter === 'accounts' && results.get('accounts')) {
+ if (selectedFilter === 'accounts') {
hasMore = results.get('accountsHasMore');
loaded = results.get('accountsLoaded');
placeholderComponent = PlaceholderAccount;
- if (results.get('accounts').size > 0) {
+ if (results.get('accounts') && results.get('accounts').size > 0) {
searchResults = results.get('accounts').map(accountId => );
- } else {
+ } else if (!submitted && suggestions && !suggestions.isEmpty()) {
+ searchResults = suggestions.map(suggestion => );
+ } else if (loaded) {
noResultsMessage = (
- {submitted && }
+
{noResultsMessage || (
({
submitted: state.getIn(['search', 'submitted']),
});
+function redirectToAccount(accountId, routerHistory) {
+ return (dispatch, getState) => {
+ const acct = getState().getIn(['accounts', accountId, 'acct']);
+
+ if (acct && routerHistory) {
+ routerHistory.push(`/@${acct}`);
+ }
+ };
+}
+
const mapDispatchToProps = (dispatch, { autoSubmit }) => {
const debouncedSubmit = debounce(() => {
@@ -40,6 +50,11 @@ const mapDispatchToProps = (dispatch, { autoSubmit }) => {
dispatch(showSearch());
},
+ onSelected(accountId, routerHistory) {
+ dispatch(clearSearch());
+ dispatch(redirectToAccount(accountId, routerHistory));
+ },
+
};
};
diff --git a/app/soapbox/features/ui/components/tabs_bar.js b/app/soapbox/features/ui/components/tabs_bar.js
index a4b1d5bb5..959ce245c 100644
--- a/app/soapbox/features/ui/components/tabs_bar.js
+++ b/app/soapbox/features/ui/components/tabs_bar.js
@@ -88,7 +88,7 @@ class TabsBar extends React.PureComponent {
)}
-
+
diff --git a/app/styles/autosuggest.scss b/app/styles/autosuggest.scss
index ea322e233..33a8f062d 100644
--- a/app/styles/autosuggest.scss
+++ b/app/styles/autosuggest.scss
@@ -102,3 +102,23 @@
.autosuggest-account .display-name__account {
color: var(--primary-text-color--faint);
}
+
+.autosuggest-input__action {
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ border-radius: 4px;
+ font-weight: bold;
+ text-decoration: none;
+ color: var(--primary-text-color--faint);
+
+ &:hover,
+ &.selected {
+ background-color: var(--brand-color--med);
+ }
+
+ .svg-icon {
+ margin-right: 8px;
+ transform: translateY(-1px);
+ }
+}
diff --git a/app/styles/chats.scss b/app/styles/chats.scss
index 9266a43e5..ba324be24 100644
--- a/app/styles/chats.scss
+++ b/app/styles/chats.scss
@@ -9,7 +9,7 @@
max-height: calc(100vh - 70px);
display: flex;
flex-direction: column;
- z-index: 999;
+ z-index: 1000; // Above AccountHeader
transition: 0.05s;
&--main {
diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss
index b0f6dd308..51157bf31 100644
--- a/app/styles/components/buttons.scss
+++ b/app/styles/components/buttons.scss
@@ -159,7 +159,7 @@ a.button {
justify-content: center;
.svg-icon {
- margin-left: 6px;
+ margin: 0 0 0 6px;
width: 20px;
height: 20px;
}
diff --git a/app/styles/components/search.scss b/app/styles/components/search.scss
index 39c56a308..db0605185 100644
--- a/app/styles/components/search.scss
+++ b/app/styles/components/search.scss
@@ -2,7 +2,7 @@
position: relative;
}
-.search__input {
+input.search__input {
@include search-input;
display: block;
padding: 7px 30px 6px 10px;
@@ -165,11 +165,6 @@
border-bottom: none;
}
-.search-page {
- height: 100%;
- min-height: 140px;
-}
-
.column {
.search {
padding: 10px 15px;
@@ -177,7 +172,7 @@
border-bottom: 1px solid hsla(var(--primary-text-color_hsl), 0.2);
}
- .search__input {
+ input.search__input {
background-color: var(--background-color);
border-radius: 8px;
padding: 12px 36px 12px 16px;
@@ -202,3 +197,7 @@
}
}
}
+
+.search .autosuggest-textarea__suggestions {
+ border-radius: 4px;
+}
diff --git a/app/styles/components/tabs-bar.scss b/app/styles/components/tabs-bar.scss
index 037f70251..2827206d5 100644
--- a/app/styles/components/tabs-bar.scss
+++ b/app/styles/components/tabs-bar.scss
@@ -3,7 +3,6 @@
box-sizing: border-box;
background: var(--brand-color);
flex: 0 0 auto;
- overflow-y: auto;
height: 50px;
width: 100%;
position: sticky;