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;