Various search UI improvements

This commit is contained in:
Evan Hahn 2021-11-01 13:43:02 -05:00 committed by GitHub
parent 630394d91d
commit a9cb621eb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 835 additions and 577 deletions

View file

@ -2,8 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import classNames from 'classnames';
import { debounce, get } from 'lodash';
import { Manager, Popper, Reference } from 'react-popper';
import { createPortal } from 'react-dom';
@ -13,20 +11,14 @@ import { AvatarPopup } from './AvatarPopup';
import type { LocalizerType } from '../types/Util';
import type { AvatarColorType } from '../types/Colors';
import type { ConversationType } from '../state/ducks/conversations';
import { LeftPaneSearchInput } from './LeftPaneSearchInput';
export type PropsType = {
searchTerm: string;
searchConversationName?: string;
searchConversationId?: string;
searchConversation: undefined | ConversationType;
startSearchCounter: number;
selectedConversation: undefined | ConversationType;
// To be used as an ID
ourConversationId: string;
ourUuid: string;
ourNumber: string;
regionCode: string;
// For display
phoneNumber?: string;
isMe?: boolean;
@ -42,24 +34,6 @@ export type PropsType = {
i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => void;
startSearch: () => void;
searchInConversation: (id: string, name: string) => void;
searchMessages: (
query: string,
options: {
searchConversationId?: string;
regionCode: string;
}
) => void;
searchDiscussions: (
query: string,
options: {
ourConversationId: string;
ourNumber: string;
ourUuid: string;
noteToSelf: string;
}
) => void;
startUpdate: () => unknown;
clearConversationSearch: () => void;
clearSearch: () => void;
@ -89,12 +63,12 @@ export class MainHeader extends React.Component<PropsType, StateType> {
}
public componentDidUpdate(prevProps: PropsType): void {
const { searchConversationId, startSearchCounter } = this.props;
const { searchConversation, startSearchCounter } = this.props;
// When user chooses to search in a given conversation we focus the field for them
if (
searchConversationId &&
searchConversationId !== prevProps.searchConversationId
searchConversation &&
searchConversation.id !== prevProps.searchConversation?.id
) {
this.setFocus();
}
@ -157,46 +131,16 @@ export class MainHeader extends React.Component<PropsType, StateType> {
}
}
public search = debounce((searchTerm: string): void => {
const {
i18n,
ourConversationId,
ourNumber,
ourUuid,
regionCode,
searchDiscussions,
searchMessages,
searchConversationId,
} = this.props;
if (searchDiscussions && !searchConversationId) {
searchDiscussions(searchTerm, {
noteToSelf: i18n('noteToSelf').toLowerCase(),
ourConversationId,
ourNumber,
ourUuid,
});
}
if (searchMessages) {
searchMessages(searchTerm, {
searchConversationId,
regionCode,
});
}
}, 200);
public updateSearch = (event: React.FormEvent<HTMLInputElement>): void => {
private updateSearch = (searchTerm: string): void => {
const {
updateSearchTerm,
clearConversationSearch,
clearSearch,
searchConversationId,
searchConversation,
} = this.props;
const searchTerm = event.currentTarget.value;
if (!searchTerm) {
if (searchConversationId) {
if (searchConversation) {
clearConversationSearch();
} else {
clearSearch();
@ -208,132 +152,30 @@ export class MainHeader extends React.Component<PropsType, StateType> {
if (updateSearchTerm) {
updateSearchTerm(searchTerm);
}
if (searchTerm.length < 1) {
return;
}
this.search(searchTerm);
};
public clearSearch = (): void => {
const { clearSearch } = this.props;
clearSearch();
this.setFocus();
};
public clearConversationSearch = (): void => {
const { clearConversationSearch } = this.props;
clearConversationSearch();
this.setFocus();
};
private handleInputBlur = (): void => {
const { clearSearch, searchConversationId, searchTerm } = this.props;
if (!searchConversationId && !searchTerm) {
const { clearSearch, searchConversation, searchTerm } = this.props;
if (!searchConversation && !searchTerm) {
clearSearch();
}
};
public handleInputKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>
): void => {
const {
clearConversationSearch,
clearSearch,
searchConversationId,
searchTerm,
} = this.props;
const { ctrlKey, metaKey, key } = event;
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
const commandOrCtrl = commandKey || controlKey;
// On linux, this keyboard combination selects all text
if (commandOrCtrl && key === '/') {
event.preventDefault();
event.stopPropagation();
return;
}
if (key !== 'Escape') {
return;
}
if (searchConversationId && searchTerm) {
clearConversationSearch();
} else {
clearSearch();
}
event.preventDefault();
event.stopPropagation();
};
public handleGlobalKeyDown = (event: KeyboardEvent): void => {
const { showingAvatarPopup } = this.state;
const {
i18n,
selectedConversation,
startSearch,
searchInConversation,
} = this.props;
const { ctrlKey, metaKey, shiftKey, key } = event;
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
const commandOrCtrl = commandKey || controlKey;
const commandAndCtrl = commandKey && ctrlKey;
const { key } = event;
if (showingAvatarPopup && key === 'Escape') {
this.hideAvatarPopup();
} else if (
commandOrCtrl &&
!commandAndCtrl &&
!shiftKey &&
(key === 'f' || key === 'F')
) {
startSearch();
event.preventDefault();
event.stopPropagation();
} else if (
selectedConversation &&
commandOrCtrl &&
!commandAndCtrl &&
shiftKey &&
(key === 'f' || key === 'F')
) {
const name = selectedConversation.isMe
? i18n('noteToSelf')
: selectedConversation.title;
searchInConversation(selectedConversation.id, name);
event.preventDefault();
event.stopPropagation();
}
};
public handleXButton = (): void => {
const {
searchConversationId,
clearConversationSearch,
clearSearch,
} = this.props;
if (searchConversationId) {
clearConversationSearch();
} else {
clearSearch();
}
this.setFocus();
};
public setFocus = (): void => {
if (this.inputRef.current) {
this.inputRef.current.focus();
@ -356,8 +198,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
name,
phoneNumber,
profileName,
searchConversationId,
searchConversationName,
searchConversation,
searchTerm,
showArchivedConversations,
startComposing,
@ -367,13 +208,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
} = this.props;
const { showingAvatarPopup, popperRoot } = this.state;
const placeholder = searchConversationName
? i18n('searchIn', [searchConversationName])
: i18n('search');
const isSearching = Boolean(
searchConversationId || searchTerm.trim().length
);
const isSearching = Boolean(searchConversation || searchTerm.trim().length);
return (
<div className="module-main-header">
@ -447,59 +282,16 @@ export class MainHeader extends React.Component<PropsType, StateType> {
)
: null}
</Manager>
<div className="module-main-header__search">
{searchConversationId ? (
<button
className="module-main-header__search__in-conversation-pill"
onClick={this.clearSearch}
tabIndex={-1}
type="button"
aria-label={i18n('clearSearch')}
>
<div className="module-main-header__search__in-conversation-pill__avatar-container">
<div className="module-main-header__search__in-conversation-pill__avatar" />
</div>
<div className="module-main-header__search__in-conversation-pill__x-button" />
</button>
) : (
<button
className="module-main-header__search__icon"
onClick={this.setFocus}
tabIndex={-1}
type="button"
aria-label={i18n('search')}
/>
)}
<input
disabled={disabled}
type="text"
ref={this.inputRef}
className={classNames(
'module-main-header__search__input',
searchTerm
? 'module-main-header__search__input--with-text'
: null,
searchConversationId
? 'module-main-header__search__input--in-conversation'
: null
)}
placeholder={placeholder}
dir="auto"
onBlur={this.handleInputBlur}
onKeyDown={this.handleInputKeyDown}
value={searchTerm}
onChange={this.updateSearch}
/>
{searchTerm ? (
<button
tabIndex={-1}
className="module-main-header__search__cancel-icon"
onClick={this.handleXButton}
type="button"
aria-label={i18n('cancel')}
/>
) : null}
</div>
<LeftPaneSearchInput
disabled={disabled}
i18n={i18n}
onBlur={this.handleInputBlur}
onChangeValue={this.updateSearch}
onClear={this.clearSearch}
ref={this.inputRef}
searchConversation={searchConversation}
value={searchTerm}
/>
{!isSearching && (
<button
aria-label={i18n('newConversation')}