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 {
|
||||
display: flex;
|
||||
padding: 8px 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&__input {
|
||||
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;
|
||||
}
|
||||
|
||||
&__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 {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__scroller {
|
||||
&__input__scroller {
|
||||
max-height: 300px;
|
||||
min-height: 300px;
|
||||
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 {
|
||||
flex-grow: 1;
|
||||
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/SafetyNumberChangeDialog.scss';
|
||||
@import './components/SafetyNumberViewer.scss';
|
||||
@import './components/SearchInput.scss';
|
||||
@import './components/SearchResultsLoadingFakeHeader.scss';
|
||||
@import './components/SearchResultsLoadingFakeRow.scss';
|
||||
@import './components/TimelineWarning.scss';
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
} from '../quill/util';
|
||||
import { SignalClipboard } from '../quill/signal-clipboard';
|
||||
import { DirectionalBlot } from '../quill/block/blot';
|
||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||
|
||||
Quill.register('formats/emoji', EmojiBlot);
|
||||
Quill.register('formats/mention', MentionBlot);
|
||||
|
@ -80,17 +81,7 @@ export type Props = {
|
|||
};
|
||||
|
||||
const MAX_LENGTH = 64 * 1024;
|
||||
|
||||
function getClassName(
|
||||
moduleClassName?: string,
|
||||
modifier?: string | null
|
||||
): string | undefined {
|
||||
if (!moduleClassName || !modifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${moduleClassName}${modifier}`;
|
||||
}
|
||||
const BASE_CLASS_NAME = 'module-composition-input';
|
||||
|
||||
export const CompositionInput: React.ComponentType<Props> = props => {
|
||||
const {
|
||||
|
@ -525,7 +516,7 @@ export const CompositionInput: React.ComponentType<Props> = props => {
|
|||
|
||||
return (
|
||||
<ReactQuill
|
||||
className="module-composition-input__quill"
|
||||
className={`${BASE_CLASS_NAME}__quill`}
|
||||
onFocus={() => callbacksRef.current.onFocus()}
|
||||
onBlur={() => callbacksRef.current.onBlur()}
|
||||
onChange={() => callbacksRef.current.onChange()}
|
||||
|
@ -642,29 +633,19 @@ export const CompositionInput: React.ComponentType<Props> = props => {
|
|||
// eslint-disable-next-line max-len
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||
|
||||
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-composition-input__input',
|
||||
getClassName(moduleClassName, '__input')
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<div className={getClassName('__input')} ref={ref}>
|
||||
<div
|
||||
ref={scrollerRef}
|
||||
onClick={focus}
|
||||
className={classNames(
|
||||
'module-composition-input__input__scroller',
|
||||
large
|
||||
? 'module-composition-input__input__scroller--large'
|
||||
: null,
|
||||
getClassName(moduleClassName, '__scroller'),
|
||||
large
|
||||
? getClassName(moduleClassName, '__scroller--large')
|
||||
: null
|
||||
getClassName('__input__scroller'),
|
||||
large ? getClassName('__input__scroller--large') : null
|
||||
)}
|
||||
>
|
||||
{reactQuill}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { EmojiPickDataType } from './emoji/EmojiPicker';
|
|||
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import { BodyRangeType, LocalizerType } from '../types/Util';
|
||||
import { ModalHost } from './ModalHost';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
import { assert } from '../util/assert';
|
||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||
|
@ -325,11 +326,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
</div>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
<div className="module-ForwardMessageModal__search">
|
||||
<i className="module-ForwardMessageModal__search--icon" />
|
||||
<input
|
||||
type="text"
|
||||
className="module-ForwardMessageModal__search--input"
|
||||
<SearchInput
|
||||
disabled={candidateConversations.length === 0}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
onChange={event => {
|
||||
|
@ -338,7 +335,6 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
ref={inputRef}
|
||||
value={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
{candidateConversations.length ? (
|
||||
<Measure bounds>
|
||||
{({ contentRect, measureRef }: MeasuredComponentProps) => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { noop } from 'lodash';
|
|||
import { LocalizerType } from '../types/Util';
|
||||
import { ModalHost } from './ModalHost';
|
||||
import { Theme } from '../util/theme';
|
||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||
|
||||
type PropsType = {
|
||||
children: ReactNode;
|
||||
|
@ -19,6 +20,8 @@ type PropsType = {
|
|||
theme?: Theme;
|
||||
};
|
||||
|
||||
const BASE_CLASS_NAME = 'module-Modal';
|
||||
|
||||
export function Modal({
|
||||
children,
|
||||
hasXButton,
|
||||
|
@ -31,23 +34,23 @@ export function Modal({
|
|||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
const hasHeader = Boolean(hasXButton || title);
|
||||
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||
|
||||
return (
|
||||
<ModalHost onClose={onClose} theme={theme}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-Modal',
|
||||
hasHeader ? 'module-Modal--has-header' : 'module-Modal--no-header',
|
||||
moduleClassName
|
||||
getClassName(''),
|
||||
getClassName(hasHeader ? '--has-header' : '--no-header')
|
||||
)}
|
||||
>
|
||||
{hasHeader && (
|
||||
<div className="module-Modal__header">
|
||||
<div className={getClassName('__header')}>
|
||||
{hasXButton && (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
type="button"
|
||||
className="module-Modal__close-button"
|
||||
className={getClassName('__close-button')}
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
|
@ -57,8 +60,8 @@ export function Modal({
|
|||
{title && (
|
||||
<h1
|
||||
className={classNames(
|
||||
'module-Modal__title',
|
||||
hasXButton ? 'module-Modal__title--with-x-button' : null
|
||||
getClassName('__title'),
|
||||
hasXButton ? getClassName('__title--with-x-button') : null
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
|
@ -67,9 +70,10 @@ export function Modal({
|
|||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames('module-Modal__body', {
|
||||
'module-Modal__body--scrolled': scrolled,
|
||||
})}
|
||||
className={classNames(
|
||||
getClassName('__body'),
|
||||
scrolled ? getClassName('__body--scrolled') : null
|
||||
)}
|
||||
onScroll={event => {
|
||||
setScrolled((event.target as HTMLDivElement).scrollTop > 2);
|
||||
}}
|
||||
|
@ -83,6 +87,14 @@ export function Modal({
|
|||
|
||||
Modal.Footer = ({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>): ReactElement => (
|
||||
<div className="module-Modal__footer">{children}</div>
|
||||
moduleClassName,
|
||||
}: 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 classNames from 'classnames';
|
||||
|
||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||
|
||||
export const SpinnerSvgSizes = ['small', 'normal'] as const;
|
||||
export type SpinnerSvgSize = typeof SpinnerSvgSizes[number];
|
||||
|
||||
|
@ -29,14 +31,16 @@ export const Spinner = ({
|
|||
size,
|
||||
svgSize,
|
||||
direction,
|
||||
}: Props): JSX.Element => (
|
||||
}: Props): JSX.Element => {
|
||||
const getClassName = getClassNamesFor('module-spinner', moduleClassName);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-spinner__container',
|
||||
`module-spinner__container--${svgSize}`,
|
||||
direction ? `module-spinner__container--${direction}` : null,
|
||||
direction ? `module-spinner__container--${svgSize}-${direction}` : null,
|
||||
moduleClassName ? `${moduleClassName}__container` : null
|
||||
getClassName('__container'),
|
||||
getClassName(`__container--${svgSize}`),
|
||||
getClassName(direction && `__container--${direction}`),
|
||||
getClassName(direction && `__container--${svgSize}-${direction}`)
|
||||
)}
|
||||
style={{
|
||||
height: size,
|
||||
|
@ -45,21 +49,20 @@ export const Spinner = ({
|
|||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-spinner__circle',
|
||||
`module-spinner__circle--${svgSize}`,
|
||||
direction ? `module-spinner__circle--${direction}` : null,
|
||||
direction ? `module-spinner__circle--${svgSize}-${direction}` : null,
|
||||
moduleClassName ? `${moduleClassName}__circle` : null
|
||||
getClassName('__circle'),
|
||||
getClassName(`__circle--${svgSize}`),
|
||||
getClassName(direction && `__circle--${direction}`),
|
||||
getClassName(direction && `__circle--${svgSize}-${direction}`)
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-spinner__arc',
|
||||
`module-spinner__arc--${svgSize}`,
|
||||
direction ? `module-spinner__arc--${direction}` : null,
|
||||
direction ? `module-spinner__arc--${svgSize}-${direction}` : null,
|
||||
moduleClassName ? `${moduleClassName}__arc` : null
|
||||
getClassName('__arc'),
|
||||
getClassName(`__arc--${svgSize}`),
|
||||
getClassName(direction && `__arc--${direction}`),
|
||||
getClassName(direction && `__arc--${svgSize}-${direction}`)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ import { ContactPill } from '../../../ContactPill';
|
|||
import { ConversationList, Row, RowType } from '../../../ConversationList';
|
||||
import { ContactCheckboxDisabledReason } from '../../../conversationList/ContactCheckbox';
|
||||
import { Button, ButtonVariant } from '../../../Button';
|
||||
import { SearchInput } from '../../../SearchInput';
|
||||
|
||||
type PropsType = {
|
||||
candidateContacts: ReadonlyArray<ConversationType>;
|
||||
|
@ -130,9 +131,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
<h1 className="module-AddGroupMembersModal__header">
|
||||
{i18n('AddGroupMembersModal--title')}
|
||||
</h1>
|
||||
<input
|
||||
type="text"
|
||||
className="module-AddGroupMembersModal__search-input"
|
||||
<SearchInput
|
||||
disabled={candidateContacts.length === 0}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
onChange={event => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ConversationType } from '../../state/ducks/conversations';
|
|||
import { ContactCheckboxDisabledReason } from '../conversationList/ContactCheckbox';
|
||||
import { ContactPills } from '../ContactPills';
|
||||
import { ContactPill } from '../ContactPill';
|
||||
import { SearchInput } from '../SearchInput';
|
||||
import {
|
||||
AddGroupMemberErrorDialog,
|
||||
AddGroupMemberErrorDialogMode,
|
||||
|
@ -152,17 +153,13 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="module-left-pane__compose-search-form">
|
||||
<input
|
||||
type="text"
|
||||
ref={focusRef}
|
||||
className="module-left-pane__compose-search-form__input"
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
dir="auto"
|
||||
value={this.searchTerm}
|
||||
<SearchInput
|
||||
moduleClassName="module-left-pane__compose-search-form"
|
||||
onChange={onChangeComposeSearchTerm}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
ref={focusRef}
|
||||
value={this.searchTerm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{Boolean(this.selectedContacts.length) && (
|
||||
<ContactPills>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { LeftPaneHelper } from './LeftPaneHelper';
|
|||
import { Row, RowType } from '../ConversationList';
|
||||
import { PropsDataType as ContactListItemPropsType } from '../conversationList/ContactListItem';
|
||||
import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||
import { SearchInput } from '../SearchInput';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import {
|
||||
instance as phoneNumberInstance,
|
||||
|
@ -93,17 +94,13 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
}>): ReactChild {
|
||||
return (
|
||||
<>
|
||||
<div className="module-left-pane__compose-search-form">
|
||||
<input
|
||||
type="text"
|
||||
ref={focusRef}
|
||||
className="module-left-pane__compose-search-form__input"
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
dir="auto"
|
||||
value={this.searchTerm}
|
||||
<SearchInput
|
||||
moduleClassName="module-left-pane__compose-search-form"
|
||||
onChange={onChangeComposeSearchTerm}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
ref={focusRef}
|
||||
value={this.searchTerm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{this.getRowCount() ? null : (
|
||||
<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…
Reference in a new issue