One SearchInput to rule them all
This commit is contained in:
parent
c62b5a900e
commit
24b7790829
15 changed files with 266 additions and 232 deletions
|
@ -7329,39 +7329,8 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-left-pane__compose-search-form {
|
.module-left-pane__compose-search-form {
|
||||||
display: flex;
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
padding: 5px 12px;
|
|
||||||
|
|
||||||
border-radius: 17px;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
@include font-body-1;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-05;
|
|
||||||
color: $color-gray-90;
|
|
||||||
border: solid 1px $color-gray-02;
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
color: $color-gray-05;
|
|
||||||
background-color: $color-gray-95;
|
|
||||||
border: solid 1px $color-gray-80;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:placeholder {
|
|
||||||
color: $color-gray-45;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: solid 1px $ultramarine-ui-light;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,36 +29,6 @@
|
||||||
@include modal-close-button;
|
@include modal-close-button;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__search-input {
|
|
||||||
margin: 10px $padding;
|
|
||||||
padding: 5px 12px;
|
|
||||||
|
|
||||||
border-radius: 17px;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
@include font-body-2;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-05;
|
|
||||||
color: $color-gray-90;
|
|
||||||
border: solid 1px $color-gray-02;
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
color: $color-gray-05;
|
|
||||||
background-color: $color-gray-95;
|
|
||||||
border: solid 1px $color-gray-80;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:placeholder {
|
|
||||||
color: $color-gray-45;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: solid 1px $ultramarine-ui-light;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-ContactPills {
|
.module-ContactPills {
|
||||||
max-height: 50px;
|
max-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__scroller {
|
&__input__scroller {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
@ -149,67 +149,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__search {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: none;
|
|
||||||
margin: 10px 16px;
|
|
||||||
padding: 5px 12px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
@include font-body-2;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-02;
|
|
||||||
border: solid 1px $color-gray-02;
|
|
||||||
color: $color-gray-90;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include dark-theme {
|
|
||||||
background: $color-gray-65;
|
|
||||||
border: solid 1px $color-gray-65;
|
|
||||||
color: $color-gray-05;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--icon {
|
|
||||||
cursor: text;
|
|
||||||
height: 16px;
|
|
||||||
left: 8px;
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
width: 16px;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-45);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include keyboard-mode {
|
|
||||||
&:focus-within {
|
|
||||||
border: solid 1px $ultramarine-ui-light;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--input {
|
|
||||||
background: inherit;
|
|
||||||
border: none;
|
|
||||||
padding-left: 16px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:placeholder {
|
|
||||||
color: $color-gray-45;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include dark-theme {
|
|
||||||
color: $color-gray-05;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__list-wrapper {
|
&__list-wrapper {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
64
stylesheets/components/SearchInput.scss
Normal file
64
stylesheets/components/SearchInput.scss
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.module-SearchInput {
|
||||||
|
&__container {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
margin: 10px 16px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@include font-body-2;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-02;
|
||||||
|
border: solid 1px $color-gray-02;
|
||||||
|
color: $color-gray-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background: $color-gray-90;
|
||||||
|
border: solid 1px $color-gray-90;
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border: solid 1px $ultramarine-ui-light;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-45);
|
||||||
|
cursor: text;
|
||||||
|
height: 16px;
|
||||||
|
left: 8px;
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
background: inherit;
|
||||||
|
border: none;
|
||||||
|
padding-left: 16px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:placeholder {
|
||||||
|
color: $color-gray-45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,7 @@
|
||||||
@import './components/Modal.scss';
|
@import './components/Modal.scss';
|
||||||
@import './components/SafetyNumberChangeDialog.scss';
|
@import './components/SafetyNumberChangeDialog.scss';
|
||||||
@import './components/SafetyNumberViewer.scss';
|
@import './components/SafetyNumberViewer.scss';
|
||||||
|
@import './components/SearchInput.scss';
|
||||||
@import './components/SearchResultsLoadingFakeHeader.scss';
|
@import './components/SearchResultsLoadingFakeHeader.scss';
|
||||||
@import './components/SearchResultsLoadingFakeRow.scss';
|
@import './components/SearchResultsLoadingFakeRow.scss';
|
||||||
@import './components/TimelineWarning.scss';
|
@import './components/TimelineWarning.scss';
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
} from '../quill/util';
|
} from '../quill/util';
|
||||||
import { SignalClipboard } from '../quill/signal-clipboard';
|
import { SignalClipboard } from '../quill/signal-clipboard';
|
||||||
import { DirectionalBlot } from '../quill/block/blot';
|
import { DirectionalBlot } from '../quill/block/blot';
|
||||||
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
|
|
||||||
Quill.register('formats/emoji', EmojiBlot);
|
Quill.register('formats/emoji', EmojiBlot);
|
||||||
Quill.register('formats/mention', MentionBlot);
|
Quill.register('formats/mention', MentionBlot);
|
||||||
|
@ -80,17 +81,7 @@ export type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_LENGTH = 64 * 1024;
|
const MAX_LENGTH = 64 * 1024;
|
||||||
|
const BASE_CLASS_NAME = 'module-composition-input';
|
||||||
function getClassName(
|
|
||||||
moduleClassName?: string,
|
|
||||||
modifier?: string | null
|
|
||||||
): string | undefined {
|
|
||||||
if (!moduleClassName || !modifier) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${moduleClassName}${modifier}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CompositionInput: React.ComponentType<Props> = props => {
|
export const CompositionInput: React.ComponentType<Props> = props => {
|
||||||
const {
|
const {
|
||||||
|
@ -525,7 +516,7 @@ export const CompositionInput: React.ComponentType<Props> = props => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactQuill
|
<ReactQuill
|
||||||
className="module-composition-input__quill"
|
className={`${BASE_CLASS_NAME}__quill`}
|
||||||
onFocus={() => callbacksRef.current.onFocus()}
|
onFocus={() => callbacksRef.current.onFocus()}
|
||||||
onBlur={() => callbacksRef.current.onBlur()}
|
onBlur={() => callbacksRef.current.onBlur()}
|
||||||
onChange={() => callbacksRef.current.onChange()}
|
onChange={() => callbacksRef.current.onChange()}
|
||||||
|
@ -642,29 +633,19 @@ export const CompositionInput: React.ComponentType<Props> = props => {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||||
|
|
||||||
|
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Manager>
|
<Manager>
|
||||||
<Reference>
|
<Reference>
|
||||||
{({ ref }) => (
|
{({ ref }) => (
|
||||||
<div
|
<div className={getClassName('__input')} ref={ref}>
|
||||||
className={classNames(
|
|
||||||
'module-composition-input__input',
|
|
||||||
getClassName(moduleClassName, '__input')
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref={scrollerRef}
|
ref={scrollerRef}
|
||||||
onClick={focus}
|
onClick={focus}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-composition-input__input__scroller',
|
getClassName('__input__scroller'),
|
||||||
large
|
large ? getClassName('__input__scroller--large') : null
|
||||||
? 'module-composition-input__input__scroller--large'
|
|
||||||
: null,
|
|
||||||
getClassName(moduleClassName, '__scroller'),
|
|
||||||
large
|
|
||||||
? getClassName(moduleClassName, '__scroller--large')
|
|
||||||
: null
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{reactQuill}
|
{reactQuill}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||||
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||||
import { BodyRangeType, LocalizerType } from '../types/Util';
|
import { BodyRangeType, LocalizerType } from '../types/Util';
|
||||||
import { ModalHost } from './ModalHost';
|
import { ModalHost } from './ModalHost';
|
||||||
|
import { SearchInput } from './SearchInput';
|
||||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||||
|
@ -325,11 +326,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="module-ForwardMessageModal__main-body">
|
<div className="module-ForwardMessageModal__main-body">
|
||||||
<div className="module-ForwardMessageModal__search">
|
<SearchInput
|
||||||
<i className="module-ForwardMessageModal__search--icon" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="module-ForwardMessageModal__search--input"
|
|
||||||
disabled={candidateConversations.length === 0}
|
disabled={candidateConversations.length === 0}
|
||||||
placeholder={i18n('contactSearchPlaceholder')}
|
placeholder={i18n('contactSearchPlaceholder')}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
|
@ -338,7 +335,6 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{candidateConversations.length ? (
|
{candidateConversations.length ? (
|
||||||
<Measure bounds>
|
<Measure bounds>
|
||||||
{({ contentRect, measureRef }: MeasuredComponentProps) => {
|
{({ contentRect, measureRef }: MeasuredComponentProps) => {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { noop } from 'lodash';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { ModalHost } from './ModalHost';
|
import { ModalHost } from './ModalHost';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -19,6 +20,8 @@ type PropsType = {
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BASE_CLASS_NAME = 'module-Modal';
|
||||||
|
|
||||||
export function Modal({
|
export function Modal({
|
||||||
children,
|
children,
|
||||||
hasXButton,
|
hasXButton,
|
||||||
|
@ -31,23 +34,23 @@ export function Modal({
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
|
||||||
const hasHeader = Boolean(hasXButton || title);
|
const hasHeader = Boolean(hasXButton || title);
|
||||||
|
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalHost onClose={onClose} theme={theme}>
|
<ModalHost onClose={onClose} theme={theme}>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-Modal',
|
getClassName(''),
|
||||||
hasHeader ? 'module-Modal--has-header' : 'module-Modal--no-header',
|
getClassName(hasHeader ? '--has-header' : '--no-header')
|
||||||
moduleClassName
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{hasHeader && (
|
{hasHeader && (
|
||||||
<div className="module-Modal__header">
|
<div className={getClassName('__header')}>
|
||||||
{hasXButton && (
|
{hasXButton && (
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('close')}
|
aria-label={i18n('close')}
|
||||||
type="button"
|
type="button"
|
||||||
className="module-Modal__close-button"
|
className={getClassName('__close-button')}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -57,8 +60,8 @@ export function Modal({
|
||||||
{title && (
|
{title && (
|
||||||
<h1
|
<h1
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-Modal__title',
|
getClassName('__title'),
|
||||||
hasXButton ? 'module-Modal__title--with-x-button' : null
|
hasXButton ? getClassName('__title--with-x-button') : null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
|
@ -67,9 +70,10 @@ export function Modal({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames('module-Modal__body', {
|
className={classNames(
|
||||||
'module-Modal__body--scrolled': scrolled,
|
getClassName('__body'),
|
||||||
})}
|
scrolled ? getClassName('__body--scrolled') : null
|
||||||
|
)}
|
||||||
onScroll={event => {
|
onScroll={event => {
|
||||||
setScrolled((event.target as HTMLDivElement).scrollTop > 2);
|
setScrolled((event.target as HTMLDivElement).scrollTop > 2);
|
||||||
}}
|
}}
|
||||||
|
@ -83,6 +87,14 @@ export function Modal({
|
||||||
|
|
||||||
Modal.Footer = ({
|
Modal.Footer = ({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: ReactNode }>): ReactElement => (
|
moduleClassName,
|
||||||
<div className="module-Modal__footer">{children}</div>
|
}: Readonly<{
|
||||||
|
children: ReactNode;
|
||||||
|
moduleClassName?: string;
|
||||||
|
}>): ReactElement => (
|
||||||
|
<div
|
||||||
|
className={getClassNamesFor(BASE_CLASS_NAME, moduleClassName)('__footer')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
48
ts/components/SearchInput.tsx
Normal file
48
ts/components/SearchInput.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { ChangeEvent, KeyboardEvent, forwardRef } from 'react';
|
||||||
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
|
|
||||||
|
export type PropTypes = {
|
||||||
|
readonly disabled?: boolean;
|
||||||
|
readonly moduleClassName?: string;
|
||||||
|
readonly onChange: (ev: ChangeEvent<HTMLInputElement>) => unknown;
|
||||||
|
readonly onKeyDown?: (ev: KeyboardEvent<HTMLInputElement>) => unknown;
|
||||||
|
readonly placeholder: string;
|
||||||
|
readonly value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BASE_CLASS_NAME = 'module-SearchInput';
|
||||||
|
|
||||||
|
export const SearchInput = forwardRef<HTMLInputElement, PropTypes>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
disabled = false,
|
||||||
|
moduleClassName,
|
||||||
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||||
|
return (
|
||||||
|
<div className={getClassName('__container')}>
|
||||||
|
<i className={getClassName('__icon')} />
|
||||||
|
<input
|
||||||
|
className={getClassName('__input')}
|
||||||
|
dir="auto"
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
ref={ref}
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -4,6 +4,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
|
|
||||||
export const SpinnerSvgSizes = ['small', 'normal'] as const;
|
export const SpinnerSvgSizes = ['small', 'normal'] as const;
|
||||||
export type SpinnerSvgSize = typeof SpinnerSvgSizes[number];
|
export type SpinnerSvgSize = typeof SpinnerSvgSizes[number];
|
||||||
|
|
||||||
|
@ -29,14 +31,16 @@ export const Spinner = ({
|
||||||
size,
|
size,
|
||||||
svgSize,
|
svgSize,
|
||||||
direction,
|
direction,
|
||||||
}: Props): JSX.Element => (
|
}: Props): JSX.Element => {
|
||||||
|
const getClassName = getClassNamesFor('module-spinner', moduleClassName);
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-spinner__container',
|
getClassName('__container'),
|
||||||
`module-spinner__container--${svgSize}`,
|
getClassName(`__container--${svgSize}`),
|
||||||
direction ? `module-spinner__container--${direction}` : null,
|
getClassName(direction && `__container--${direction}`),
|
||||||
direction ? `module-spinner__container--${svgSize}-${direction}` : null,
|
getClassName(direction && `__container--${svgSize}-${direction}`)
|
||||||
moduleClassName ? `${moduleClassName}__container` : null
|
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
height: size,
|
height: size,
|
||||||
|
@ -45,21 +49,20 @@ export const Spinner = ({
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-spinner__circle',
|
getClassName('__circle'),
|
||||||
`module-spinner__circle--${svgSize}`,
|
getClassName(`__circle--${svgSize}`),
|
||||||
direction ? `module-spinner__circle--${direction}` : null,
|
getClassName(direction && `__circle--${direction}`),
|
||||||
direction ? `module-spinner__circle--${svgSize}-${direction}` : null,
|
getClassName(direction && `__circle--${svgSize}-${direction}`)
|
||||||
moduleClassName ? `${moduleClassName}__circle` : null
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-spinner__arc',
|
getClassName('__arc'),
|
||||||
`module-spinner__arc--${svgSize}`,
|
getClassName(`__arc--${svgSize}`),
|
||||||
direction ? `module-spinner__arc--${direction}` : null,
|
getClassName(direction && `__arc--${direction}`),
|
||||||
direction ? `module-spinner__arc--${svgSize}-${direction}` : null,
|
getClassName(direction && `__arc--${svgSize}-${direction}`)
|
||||||
moduleClassName ? `${moduleClassName}__arc` : null
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { ContactPill } from '../../../ContactPill';
|
||||||
import { ConversationList, Row, RowType } from '../../../ConversationList';
|
import { ConversationList, Row, RowType } from '../../../ConversationList';
|
||||||
import { ContactCheckboxDisabledReason } from '../../../conversationList/ContactCheckbox';
|
import { ContactCheckboxDisabledReason } from '../../../conversationList/ContactCheckbox';
|
||||||
import { Button, ButtonVariant } from '../../../Button';
|
import { Button, ButtonVariant } from '../../../Button';
|
||||||
|
import { SearchInput } from '../../../SearchInput';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
candidateContacts: ReadonlyArray<ConversationType>;
|
candidateContacts: ReadonlyArray<ConversationType>;
|
||||||
|
@ -130,9 +131,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||||
<h1 className="module-AddGroupMembersModal__header">
|
<h1 className="module-AddGroupMembersModal__header">
|
||||||
{i18n('AddGroupMembersModal--title')}
|
{i18n('AddGroupMembersModal--title')}
|
||||||
</h1>
|
</h1>
|
||||||
<input
|
<SearchInput
|
||||||
type="text"
|
|
||||||
className="module-AddGroupMembersModal__search-input"
|
|
||||||
disabled={candidateContacts.length === 0}
|
disabled={candidateContacts.length === 0}
|
||||||
placeholder={i18n('contactSearchPlaceholder')}
|
placeholder={i18n('contactSearchPlaceholder')}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { ContactCheckboxDisabledReason } from '../conversationList/ContactCheckbox';
|
import { ContactCheckboxDisabledReason } from '../conversationList/ContactCheckbox';
|
||||||
import { ContactPills } from '../ContactPills';
|
import { ContactPills } from '../ContactPills';
|
||||||
import { ContactPill } from '../ContactPill';
|
import { ContactPill } from '../ContactPill';
|
||||||
|
import { SearchInput } from '../SearchInput';
|
||||||
import {
|
import {
|
||||||
AddGroupMemberErrorDialog,
|
AddGroupMemberErrorDialog,
|
||||||
AddGroupMemberErrorDialogMode,
|
AddGroupMemberErrorDialogMode,
|
||||||
|
@ -152,17 +153,13 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="module-left-pane__compose-search-form">
|
<SearchInput
|
||||||
<input
|
moduleClassName="module-left-pane__compose-search-form"
|
||||||
type="text"
|
|
||||||
ref={focusRef}
|
|
||||||
className="module-left-pane__compose-search-form__input"
|
|
||||||
placeholder={i18n('contactSearchPlaceholder')}
|
|
||||||
dir="auto"
|
|
||||||
value={this.searchTerm}
|
|
||||||
onChange={onChangeComposeSearchTerm}
|
onChange={onChangeComposeSearchTerm}
|
||||||
|
placeholder={i18n('contactSearchPlaceholder')}
|
||||||
|
ref={focusRef}
|
||||||
|
value={this.searchTerm}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{Boolean(this.selectedContacts.length) && (
|
{Boolean(this.selectedContacts.length) && (
|
||||||
<ContactPills>
|
<ContactPills>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { LeftPaneHelper } from './LeftPaneHelper';
|
||||||
import { Row, RowType } from '../ConversationList';
|
import { Row, RowType } from '../ConversationList';
|
||||||
import { PropsDataType as ContactListItemPropsType } from '../conversationList/ContactListItem';
|
import { PropsDataType as ContactListItemPropsType } from '../conversationList/ContactListItem';
|
||||||
import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||||
|
import { SearchInput } from '../SearchInput';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import {
|
import {
|
||||||
instance as phoneNumberInstance,
|
instance as phoneNumberInstance,
|
||||||
|
@ -93,17 +94,13 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="module-left-pane__compose-search-form">
|
<SearchInput
|
||||||
<input
|
moduleClassName="module-left-pane__compose-search-form"
|
||||||
type="text"
|
|
||||||
ref={focusRef}
|
|
||||||
className="module-left-pane__compose-search-form__input"
|
|
||||||
placeholder={i18n('contactSearchPlaceholder')}
|
|
||||||
dir="auto"
|
|
||||||
value={this.searchTerm}
|
|
||||||
onChange={onChangeComposeSearchTerm}
|
onChange={onChangeComposeSearchTerm}
|
||||||
|
placeholder={i18n('contactSearchPlaceholder')}
|
||||||
|
ref={focusRef}
|
||||||
|
value={this.searchTerm}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.getRowCount() ? null : (
|
{this.getRowCount() ? null : (
|
||||||
<div className="module-left-pane__compose-no-contacts">
|
<div className="module-left-pane__compose-no-contacts">
|
||||||
|
|
41
ts/test-both/util/getClassNamesFor_test.ts
Normal file
41
ts/test-both/util/getClassNamesFor_test.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { getClassNamesFor } from '../../util/getClassNamesFor';
|
||||||
|
|
||||||
|
describe('getClassNamesFor', () => {
|
||||||
|
it('returns a function', () => {
|
||||||
|
const f = getClassNamesFor('hello-world');
|
||||||
|
assert.isFunction(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a function that adds a modifier', () => {
|
||||||
|
const f = getClassNamesFor('module');
|
||||||
|
assert.equal(f('__modifier'), 'module__modifier');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not add anything if there is no modifier', () => {
|
||||||
|
const f = getClassNamesFor('module');
|
||||||
|
assert.equal(f(), '');
|
||||||
|
assert.equal(f(undefined && '__modifier'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('but does return the top level module if the modifier is empty string', () => {
|
||||||
|
const f = getClassNamesFor('module1', 'module2');
|
||||||
|
assert.equal(f(''), 'module1 module2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds multiple class names', () => {
|
||||||
|
const f = getClassNamesFor('module1', 'module2', 'module3');
|
||||||
|
assert.equal(
|
||||||
|
f('__modifier'),
|
||||||
|
'module1__modifier module2__modifier module3__modifier'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips parent modules that are undefined', () => {
|
||||||
|
const f = getClassNamesFor('module1', undefined, 'module3');
|
||||||
|
assert.equal(f('__modifier'), 'module1__modifier module3__modifier');
|
||||||
|
});
|
||||||
|
});
|
17
ts/util/getClassNamesFor.ts
Normal file
17
ts/util/getClassNamesFor.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export function getClassNamesFor(
|
||||||
|
...modules: Array<string | undefined>
|
||||||
|
): (modifier?: string) => string {
|
||||||
|
return modifier => {
|
||||||
|
const cx = modules.map(parentModule =>
|
||||||
|
parentModule && modifier !== undefined
|
||||||
|
? `${parentModule}${modifier}`
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
return classNames(cx);
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue