Consolidates the search inputs
This commit is contained in:
parent
1b352531ca
commit
67209d8881
13 changed files with 263 additions and 319 deletions
|
@ -2,64 +2,8 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
.LeftPaneSearchInput {
|
.LeftPaneSearchInput {
|
||||||
position: relative;
|
&__input--with-children.module-SearchInput__input--with-children {
|
||||||
margin: 0 16px;
|
padding-left: 50px;
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
&__input {
|
|
||||||
@include font-body-2;
|
|
||||||
border: solid 1px transparent;
|
|
||||||
border-radius: 8px;
|
|
||||||
height: 28px;
|
|
||||||
padding-left: 30px;
|
|
||||||
padding-right: 5px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-black-alpha-08;
|
|
||||||
color: $color-gray-90;
|
|
||||||
|
|
||||||
&:placeholder {
|
|
||||||
color: $color-gray-45;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
background-color: $color-white-alpha-12;
|
|
||||||
color: $color-gray-05;
|
|
||||||
|
|
||||||
&:placeholder {
|
|
||||||
color: $color-gray-25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: solid 1px $color-ultramarine;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--with-text {
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--in-conversation {
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
height: 16px;
|
|
||||||
left: 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
width: 16px;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-45);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-25);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__in-conversation-pill {
|
&__in-conversation-pill {
|
||||||
|
@ -110,22 +54,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__cancel {
|
|
||||||
height: 18px;
|
|
||||||
position: absolute;
|
|
||||||
right: 8px;
|
|
||||||
top: 5px;
|
|
||||||
width: 18px;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-left-pane--width-narrow & {
|
.module-left-pane--width-narrow & {
|
||||||
display: none;
|
&__container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,40 +3,28 @@
|
||||||
|
|
||||||
.module-SearchInput {
|
.module-SearchInput {
|
||||||
&__container {
|
&__container {
|
||||||
border-radius: 8px;
|
margin: {
|
||||||
border: none;
|
left: 16px;
|
||||||
margin: 10px 16px;
|
right: 16px;
|
||||||
padding: 5px 12px;
|
bottom: 8px;
|
||||||
|
}
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@include font-body-2;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-15;
|
|
||||||
border: solid 1px $color-gray-15;
|
|
||||||
color: $color-gray-90;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include dark-theme {
|
|
||||||
background-color: $color-gray-65;
|
|
||||||
border: solid 1px $color-gray-65;
|
|
||||||
color: $color-gray-05;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
border: solid 1px $color-ultramarine;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-45);
|
|
||||||
cursor: text;
|
|
||||||
height: 16px;
|
height: 16px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-45);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-25);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
|
@ -61,4 +49,55 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
@include font-body-2;
|
||||||
|
border: solid 1px transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 28px;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 5px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-black-alpha-08;
|
||||||
|
color: $color-gray-90;
|
||||||
|
|
||||||
|
&:placeholder {
|
||||||
|
color: $color-gray-45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-white-alpha-12;
|
||||||
|
color: $color-gray-05;
|
||||||
|
|
||||||
|
&:placeholder {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: solid 1px $color-ultramarine;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--with-text {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cancel {
|
||||||
|
height: 18px;
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 5px;
|
||||||
|
width: 18px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { FunctionComponent } from 'react';
|
import type { FunctionComponent } from 'react';
|
||||||
|
@ -372,6 +372,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
<div className="module-ForwardMessageModal__main-body">
|
<div className="module-ForwardMessageModal__main-body">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
disabled={candidateConversations.length === 0}
|
disabled={candidateConversations.length === 0}
|
||||||
|
i18n={i18n}
|
||||||
placeholder={i18n('contactSearchPlaceholder')}
|
placeholder={i18n('contactSearchPlaceholder')}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setSearchTerm(event.target.value);
|
setSearchTerm(event.target.value);
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
|
||||||
import { LeftPaneSearchInput } from './LeftPaneSearchInput';
|
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
|
||||||
|
|
||||||
export type PropsType = {
|
|
||||||
clearConversationSearch: () => void;
|
|
||||||
clearSearch: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
i18n: LocalizerType;
|
|
||||||
searchConversation: undefined | ConversationType;
|
|
||||||
searchTerm: string;
|
|
||||||
startSearchCounter: number;
|
|
||||||
updateSearchTerm: (searchTerm: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LeftPaneMainSearchInput = ({
|
|
||||||
clearConversationSearch,
|
|
||||||
clearSearch,
|
|
||||||
disabled,
|
|
||||||
i18n,
|
|
||||||
searchConversation,
|
|
||||||
searchTerm,
|
|
||||||
startSearchCounter,
|
|
||||||
updateSearchTerm,
|
|
||||||
}: PropsType): JSX.Element => {
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
||||||
|
|
||||||
const prevSearchConversationId = usePrevious(
|
|
||||||
undefined,
|
|
||||||
searchConversation?.id
|
|
||||||
);
|
|
||||||
const prevSearchCounter = usePrevious(startSearchCounter, startSearchCounter);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// When user chooses to search in a given conversation we focus the field for them
|
|
||||||
if (
|
|
||||||
searchConversation &&
|
|
||||||
searchConversation.id !== prevSearchConversationId
|
|
||||||
) {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}
|
|
||||||
// When user chooses to start a new search, we focus the field
|
|
||||||
if (startSearchCounter !== prevSearchCounter) {
|
|
||||||
inputRef.current?.select();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
prevSearchConversationId,
|
|
||||||
prevSearchCounter,
|
|
||||||
searchConversation,
|
|
||||||
startSearchCounter,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LeftPaneSearchInput
|
|
||||||
disabled={disabled}
|
|
||||||
i18n={i18n}
|
|
||||||
onBlur={() => {
|
|
||||||
if (!searchConversation && !searchTerm) {
|
|
||||||
clearSearch();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChangeValue={nextSearchTerm => {
|
|
||||||
if (!nextSearchTerm) {
|
|
||||||
if (searchConversation) {
|
|
||||||
clearConversationSearch();
|
|
||||||
} else {
|
|
||||||
clearSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateSearchTerm) {
|
|
||||||
updateSearchTerm(nextSearchTerm);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClear={() => {
|
|
||||||
clearSearch();
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}}
|
|
||||||
ref={inputRef}
|
|
||||||
searchConversation={searchConversation}
|
|
||||||
value={searchTerm}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,123 +1,144 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { FocusEventHandler } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import React, { forwardRef, useRef } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { refMerger } from '../util/refMerger';
|
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
|
import { SearchInput } from './SearchInput';
|
||||||
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
|
clearConversationSearch: () => void;
|
||||||
|
clearSearch: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
|
||||||
onChangeValue: (newValue: string) => unknown;
|
|
||||||
onClear: () => unknown;
|
|
||||||
searchConversation?: ConversationType;
|
searchConversation?: ConversationType;
|
||||||
value: string;
|
searchTerm: string;
|
||||||
|
startSearchCounter: number;
|
||||||
|
updateSearchTerm: (searchTerm: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO DESKTOP-3068: merge with <SearchInput />
|
export const LeftPaneSearchInput = ({
|
||||||
export const LeftPaneSearchInput = forwardRef<HTMLInputElement, PropsType>(
|
clearConversationSearch,
|
||||||
(
|
clearSearch,
|
||||||
{
|
disabled,
|
||||||
disabled,
|
i18n,
|
||||||
i18n,
|
searchConversation,
|
||||||
onBlur,
|
searchTerm,
|
||||||
onChangeValue,
|
startSearchCounter,
|
||||||
onClear,
|
updateSearchTerm,
|
||||||
searchConversation,
|
}: PropsType): JSX.Element => {
|
||||||
value,
|
const inputRef = useRef<null | HTMLInputElement>(null);
|
||||||
},
|
|
||||||
outerRef
|
|
||||||
) => {
|
|
||||||
const inputRef = useRef<null | HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const emptyOrClear =
|
const prevSearchConversationId = usePrevious(
|
||||||
searchConversation && value ? () => onChangeValue('') : onClear;
|
undefined,
|
||||||
|
searchConversation?.id
|
||||||
|
);
|
||||||
|
const prevSearchCounter = usePrevious(startSearchCounter, startSearchCounter);
|
||||||
|
|
||||||
const label = i18n(searchConversation ? 'searchIn' : 'search');
|
useEffect(() => {
|
||||||
|
// When user chooses to search in a given conversation we focus the field for them
|
||||||
|
if (
|
||||||
|
searchConversation &&
|
||||||
|
searchConversation.id !== prevSearchConversationId
|
||||||
|
) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
// When user chooses to start a new search, we focus the field
|
||||||
|
if (startSearchCounter !== prevSearchCounter) {
|
||||||
|
inputRef.current?.select();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
prevSearchConversationId,
|
||||||
|
prevSearchCounter,
|
||||||
|
searchConversation,
|
||||||
|
startSearchCounter,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
const changeValue = (nextSearchTerm: string) => {
|
||||||
<div className="LeftPaneSearchInput">
|
if (!nextSearchTerm) {
|
||||||
{searchConversation ? (
|
if (searchConversation) {
|
||||||
// Clicking the non-X part of the pill should focus the input but have a normal
|
clearConversationSearch();
|
||||||
// cursor. This effectively simulates `pointer-events: none` while still
|
} else {
|
||||||
// letting us change the cursor.
|
clearSearch();
|
||||||
// eslint-disable-next-line max-len
|
}
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
|
||||||
<div
|
return;
|
||||||
className="LeftPaneSearchInput__in-conversation-pill"
|
}
|
||||||
onClick={() => {
|
|
||||||
inputRef.current?.focus();
|
if (updateSearchTerm) {
|
||||||
}}
|
updateSearchTerm(nextSearchTerm);
|
||||||
>
|
}
|
||||||
<Avatar
|
};
|
||||||
acceptedMessageRequest={searchConversation.acceptedMessageRequest}
|
|
||||||
avatarPath={searchConversation.avatarPath}
|
const clearAndFocus = () => {
|
||||||
badge={undefined}
|
clearSearch();
|
||||||
color={searchConversation.color}
|
inputRef.current?.focus();
|
||||||
conversationType={searchConversation.type}
|
};
|
||||||
i18n={i18n}
|
|
||||||
isMe={searchConversation.isMe}
|
const label = i18n(searchConversation ? 'searchIn' : 'search');
|
||||||
noteToSelf={searchConversation.isMe}
|
|
||||||
sharedGroupNames={searchConversation.sharedGroupNames}
|
return (
|
||||||
size={AvatarSize.SIXTEEN}
|
<SearchInput
|
||||||
title={searchConversation.title}
|
disabled={disabled}
|
||||||
unblurredAvatarPath={searchConversation.unblurredAvatarPath}
|
label={label}
|
||||||
/>
|
hasSearchIcon={!searchConversation}
|
||||||
<button
|
i18n={i18n}
|
||||||
aria-label={i18n('clearSearch')}
|
moduleClassName="LeftPaneSearchInput"
|
||||||
className="LeftPaneSearchInput__in-conversation-pill__x-button"
|
onBlur={() => {
|
||||||
onClick={onClear}
|
if (!searchConversation && !searchTerm) {
|
||||||
type="button"
|
clearSearch();
|
||||||
/>
|
}
|
||||||
</div>
|
}}
|
||||||
) : (
|
onChange={event => {
|
||||||
<div className="LeftPaneSearchInput__icon" />
|
changeValue(event.currentTarget.value);
|
||||||
)}
|
}}
|
||||||
<input
|
onClear={() => {
|
||||||
aria-label={label}
|
if (searchConversation && searchTerm) {
|
||||||
className={classNames(
|
changeValue('');
|
||||||
'LeftPaneSearchInput__input',
|
} else {
|
||||||
value && 'LeftPaneSearchInput__input--with-text',
|
clearAndFocus();
|
||||||
searchConversation && 'LeftPaneSearchInput__input--in-conversation'
|
}
|
||||||
)}
|
}}
|
||||||
dir="auto"
|
ref={inputRef}
|
||||||
disabled={disabled}
|
placeholder={label}
|
||||||
onBlur={onBlur}
|
value={searchTerm}
|
||||||
onChange={event => {
|
>
|
||||||
onChangeValue(event.currentTarget.value);
|
{searchConversation && (
|
||||||
|
// Clicking the non-X part of the pill should focus the input but have a normal
|
||||||
|
// cursor. This effectively simulates `pointer-events: none` while still
|
||||||
|
// letting us change the cursor.
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
|
<div
|
||||||
|
className="LeftPaneSearchInput__in-conversation-pill"
|
||||||
|
onClick={() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
onKeyDown={event => {
|
>
|
||||||
const { ctrlKey, key } = event;
|
<Avatar
|
||||||
|
acceptedMessageRequest={searchConversation.acceptedMessageRequest}
|
||||||
// On Linux, this key combo selects all text.
|
avatarPath={searchConversation.avatarPath}
|
||||||
if (window.platform === 'linux' && ctrlKey && key === '/') {
|
badge={undefined}
|
||||||
event.preventDefault();
|
color={searchConversation.color}
|
||||||
event.stopPropagation();
|
conversationType={searchConversation.type}
|
||||||
} else if (key === 'Escape') {
|
i18n={i18n}
|
||||||
emptyOrClear();
|
isMe={searchConversation.isMe}
|
||||||
event.preventDefault();
|
noteToSelf={searchConversation.isMe}
|
||||||
event.stopPropagation();
|
sharedGroupNames={searchConversation.sharedGroupNames}
|
||||||
}
|
size={AvatarSize.SIXTEEN}
|
||||||
}}
|
title={searchConversation.title}
|
||||||
placeholder={label}
|
unblurredAvatarPath={searchConversation.unblurredAvatarPath}
|
||||||
ref={refMerger(inputRef, outerRef)}
|
/>
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
{value && (
|
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('cancel')}
|
aria-label={i18n('clearSearch')}
|
||||||
className="LeftPaneSearchInput__cancel"
|
className="LeftPaneSearchInput__in-conversation-pill__x-button"
|
||||||
onClick={emptyOrClear}
|
onClick={clearAndFocus}
|
||||||
tabIndex={-1}
|
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
);
|
</SearchInput>
|
||||||
}
|
);
|
||||||
);
|
};
|
||||||
|
|
|
@ -1,13 +1,26 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
import type {
|
||||||
|
ChangeEvent,
|
||||||
|
FocusEventHandler,
|
||||||
|
KeyboardEvent,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
|
|
||||||
export type PropTypes = {
|
export type PropTypes = {
|
||||||
|
readonly children?: ReactNode;
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
|
readonly label?: string;
|
||||||
|
readonly hasSearchIcon?: boolean;
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
readonly moduleClassName?: string;
|
readonly moduleClassName?: string;
|
||||||
|
readonly onClear?: () => unknown;
|
||||||
|
readonly onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
readonly onChange: (ev: ChangeEvent<HTMLInputElement>) => unknown;
|
readonly onChange: (ev: ChangeEvent<HTMLInputElement>) => unknown;
|
||||||
readonly onKeyDown?: (ev: KeyboardEvent<HTMLInputElement>) => unknown;
|
readonly onKeyDown?: (ev: KeyboardEvent<HTMLInputElement>) => unknown;
|
||||||
readonly placeholder: string;
|
readonly placeholder: string;
|
||||||
|
@ -19,8 +32,14 @@ const BASE_CLASS_NAME = 'module-SearchInput';
|
||||||
export const SearchInput = forwardRef<HTMLInputElement, PropTypes>(
|
export const SearchInput = forwardRef<HTMLInputElement, PropTypes>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
children,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
hasSearchIcon = true,
|
||||||
|
i18n,
|
||||||
|
label,
|
||||||
moduleClassName,
|
moduleClassName,
|
||||||
|
onClear,
|
||||||
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
@ -31,18 +50,48 @@ export const SearchInput = forwardRef<HTMLInputElement, PropTypes>(
|
||||||
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||||
return (
|
return (
|
||||||
<div className={getClassName('__container')}>
|
<div className={getClassName('__container')}>
|
||||||
<i className={getClassName('__icon')} />
|
{hasSearchIcon && <i className={getClassName('__icon')} />}
|
||||||
|
{children}
|
||||||
<input
|
<input
|
||||||
className={getClassName('__input')}
|
aria-label={label || i18n('search')}
|
||||||
|
className={classNames(
|
||||||
|
getClassName('__input'),
|
||||||
|
value && getClassName('__input--with-text'),
|
||||||
|
children && getClassName('__input--with-children')
|
||||||
|
)}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
onBlur={onBlur}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={event => {
|
||||||
|
const { ctrlKey, key } = event;
|
||||||
|
|
||||||
|
// On Linux, this key combo selects all text.
|
||||||
|
if (window.platform === 'linux' && ctrlKey && key === '/') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else if (key === 'Escape' && onClear) {
|
||||||
|
onClear();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown?.(event);
|
||||||
|
}}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
|
{value && onClear && (
|
||||||
|
<button
|
||||||
|
aria-label={i18n('cancel')}
|
||||||
|
className={getClassName('__cancel')}
|
||||||
|
onClick={onClear}
|
||||||
|
tabIndex={-1}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { FunctionComponent } from 'react';
|
import type { FunctionComponent } from 'react';
|
||||||
|
@ -139,6 +139,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||||
</h1>
|
</h1>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
disabled={candidateContacts.length === 0}
|
disabled={candidateContacts.length === 0}
|
||||||
|
i18n={i18n}
|
||||||
placeholder={i18n('contactSearchPlaceholder')}
|
placeholder={i18n('contactSearchPlaceholder')}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setSearchTerm(event.target.value);
|
setSearchTerm(event.target.value);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ReactChild } from 'react';
|
import type { ReactChild } from 'react';
|
||||||
|
@ -13,7 +13,7 @@ import { RowType } from '../ConversationList';
|
||||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import type { ConversationType } from '../../state/ducks/conversations';
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { LeftPaneMainSearchInput } from '../LeftPaneMainSearchInput';
|
import { LeftPaneSearchInput } from '../LeftPaneSearchInput';
|
||||||
import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper';
|
import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper';
|
||||||
import { LeftPaneSearchHelper } from './LeftPaneSearchHelper';
|
import { LeftPaneSearchHelper } from './LeftPaneSearchHelper';
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LeftPaneMainSearchInput
|
<LeftPaneSearchInput
|
||||||
clearConversationSearch={clearConversationSearch}
|
clearConversationSearch={clearConversationSearch}
|
||||||
clearSearch={clearSearch}
|
clearSearch={clearSearch}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -116,6 +116,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
return (
|
return (
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
i18n={i18n}
|
||||||
moduleClassName="module-left-pane__compose-search-form"
|
moduleClassName="module-left-pane__compose-search-form"
|
||||||
onChange={onChangeComposeSearchTerm}
|
onChange={onChangeComposeSearchTerm}
|
||||||
placeholder={i18n('contactSearchPlaceholder')}
|
placeholder={i18n('contactSearchPlaceholder')}
|
||||||
|
|
|
@ -105,6 +105,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
return (
|
return (
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
i18n={i18n}
|
||||||
moduleClassName="module-left-pane__compose-search-form"
|
moduleClassName="module-left-pane__compose-search-form"
|
||||||
onChange={onChangeComposeSearchTerm}
|
onChange={onChangeComposeSearchTerm}
|
||||||
placeholder={i18n('contactSearchPlaceholder')}
|
placeholder={i18n('contactSearchPlaceholder')}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { RowType } from '../ConversationList';
|
||||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import { handleKeydownForSearch } from './handleKeydownForSearch';
|
import { handleKeydownForSearch } from './handleKeydownForSearch';
|
||||||
import { LeftPaneMainSearchInput } from '../LeftPaneMainSearchInput';
|
import { LeftPaneSearchInput } from '../LeftPaneSearchInput';
|
||||||
|
|
||||||
export type LeftPaneInboxPropsType = {
|
export type LeftPaneInboxPropsType = {
|
||||||
conversations: ReadonlyArray<ConversationListItemPropsType>;
|
conversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||||
|
@ -90,7 +90,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
||||||
updateSearchTerm: (searchTerm: string) => unknown;
|
updateSearchTerm: (searchTerm: string) => unknown;
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
return (
|
return (
|
||||||
<LeftPaneMainSearchInput
|
<LeftPaneSearchInput
|
||||||
clearConversationSearch={clearConversationSearch}
|
clearConversationSearch={clearConversationSearch}
|
||||||
clearSearch={clearSearch}
|
clearSearch={clearSearch}
|
||||||
disabled={this.searchDisabled}
|
disabled={this.searchDisabled}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { RowType } from '../ConversationList';
|
||||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||||
import { handleKeydownForSearch } from './handleKeydownForSearch';
|
import { handleKeydownForSearch } from './handleKeydownForSearch';
|
||||||
import type { ConversationType } from '../../state/ducks/conversations';
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { LeftPaneMainSearchInput } from '../LeftPaneMainSearchInput';
|
import { LeftPaneSearchInput } from '../LeftPaneSearchInput';
|
||||||
|
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import { Emojify } from '../conversation/Emojify';
|
import { Emojify } from '../conversation/Emojify';
|
||||||
|
@ -108,7 +108,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
updateSearchTerm: (searchTerm: string) => unknown;
|
updateSearchTerm: (searchTerm: string) => unknown;
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
return (
|
return (
|
||||||
<LeftPaneMainSearchInput
|
<LeftPaneSearchInput
|
||||||
clearConversationSearch={clearConversationSearch}
|
clearConversationSearch={clearConversationSearch}
|
||||||
clearSearch={clearSearch}
|
clearSearch={clearSearch}
|
||||||
disabled={this.searchDisabled}
|
disabled={this.searchDisabled}
|
||||||
|
|
|
@ -7515,20 +7515,12 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-07-21T18:34:59.251Z"
|
"updated": "2020-07-21T18:34:59.251Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/LeftPaneMainSearchInput.tsx",
|
|
||||||
"line": " const inputRef = useRef<HTMLInputElement | null>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2022-01-26T23:11:05.369Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/LeftPaneSearchInput.tsx",
|
"path": "ts/components/LeftPaneSearchInput.tsx",
|
||||||
"line": " const inputRef = useRef<null | HTMLInputElement>(null);",
|
"line": " const inputRef = useRef<null | HTMLInputElement>(null);",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-10-29T22:48:58.354Z",
|
"updated": "2022-02-11T20:49:03.879Z"
|
||||||
"reasonDetail": "Only used to focus the input."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
|
|
Loading…
Reference in a new issue