From 24b7790829d46ae078a89573de43fef1ccd47466 Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Mon, 10 May 2021 20:50:43 -0400 Subject: [PATCH] One SearchInput to rule them all --- stylesheets/_modules.scss | 31 --------- .../components/AddGroupMembersModal.scss | 30 --------- .../components/ForwardMessageModal.scss | 63 +----------------- stylesheets/components/SearchInput.scss | 64 ++++++++++++++++++ stylesheets/manifest.scss | 1 + ts/components/CompositionInput.tsx | 35 +++------- ts/components/ForwardMessageModal.tsx | 24 +++---- ts/components/Modal.tsx | 36 ++++++---- ts/components/SearchInput.tsx | 48 ++++++++++++++ ts/components/Spinner.tsx | 65 ++++++++++--------- .../ChooseGroupMembersModal.tsx | 5 +- .../LeftPaneChooseGroupMembersHelper.tsx | 19 +++--- .../leftPane/LeftPaneComposeHelper.tsx | 19 +++--- ts/test-both/util/getClassNamesFor_test.ts | 41 ++++++++++++ ts/util/getClassNamesFor.ts | 17 +++++ 15 files changed, 266 insertions(+), 232 deletions(-) create mode 100644 stylesheets/components/SearchInput.scss create mode 100644 ts/components/SearchInput.tsx create mode 100644 ts/test-both/util/getClassNamesFor_test.ts create mode 100644 ts/util/getClassNamesFor.ts diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 95a10a147a09..cb60bf93c593 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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; - } } } diff --git a/stylesheets/components/AddGroupMembersModal.scss b/stylesheets/components/AddGroupMembersModal.scss index 34d9af2c9453..b8188c0659f0 100644 --- a/stylesheets/components/AddGroupMembersModal.scss +++ b/stylesheets/components/AddGroupMembersModal.scss @@ -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; } diff --git a/stylesheets/components/ForwardMessageModal.scss b/stylesheets/components/ForwardMessageModal.scss index e456362ae135..2d16fe63f131 100644 --- a/stylesheets/components/ForwardMessageModal.scss +++ b/stylesheets/components/ForwardMessageModal.scss @@ -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; diff --git a/stylesheets/components/SearchInput.scss b/stylesheets/components/SearchInput.scss new file mode 100644 index 000000000000..106c6eb60db0 --- /dev/null +++ b/stylesheets/components/SearchInput.scss @@ -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; + } + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index a7d212289bef..b89dff99b5e2 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -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'; diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index f3c65c888c97..050f2ba81e83 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -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 => { const { @@ -525,7 +516,7 @@ export const CompositionInput: React.ComponentType = props => { return ( callbacksRef.current.onFocus()} onBlur={() => callbacksRef.current.onBlur()} onChange={() => callbacksRef.current.onChange()} @@ -642,29 +633,19 @@ export const CompositionInput: React.ComponentType = 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 ( {({ ref }) => ( -
+
{reactQuill} diff --git a/ts/components/ForwardMessageModal.tsx b/ts/components/ForwardMessageModal.tsx index 00c3980c7239..8cbe9e97a5e0 100644 --- a/ts/components/ForwardMessageModal.tsx +++ b/ts/components/ForwardMessageModal.tsx @@ -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,20 +326,15 @@ export const ForwardMessageModal: FunctionComponent = ({
) : (
-
- - { - setSearchTerm(event.target.value); - }} - ref={inputRef} - value={searchTerm} - /> -
+ { + setSearchTerm(event.target.value); + }} + ref={inputRef} + value={searchTerm} + /> {candidateConversations.length ? ( {({ contentRect, measureRef }: MeasuredComponentProps) => { diff --git a/ts/components/Modal.tsx b/ts/components/Modal.tsx index a7173c90ed70..5557566100e3 100644 --- a/ts/components/Modal.tsx +++ b/ts/components/Modal.tsx @@ -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 (
{hasHeader && ( -
+
{hasXButton && (
)}
{ setScrolled((event.target as HTMLDivElement).scrollTop > 2); }} @@ -83,6 +87,14 @@ export function Modal({ Modal.Footer = ({ children, -}: Readonly<{ children: ReactNode }>): ReactElement => ( -
{children}
+ moduleClassName, +}: Readonly<{ + children: ReactNode; + moduleClassName?: string; +}>): ReactElement => ( +
+ {children} +
); diff --git a/ts/components/SearchInput.tsx b/ts/components/SearchInput.tsx new file mode 100644 index 000000000000..5e4ea959047d --- /dev/null +++ b/ts/components/SearchInput.tsx @@ -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) => unknown; + readonly onKeyDown?: (ev: KeyboardEvent) => unknown; + readonly placeholder: string; + readonly value: string; +}; + +const BASE_CLASS_NAME = 'module-SearchInput'; + +export const SearchInput = forwardRef( + ( + { + disabled = false, + moduleClassName, + onChange, + onKeyDown, + placeholder, + value, + }, + ref + ) => { + const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName); + return ( +
+ + +
+ ); + } +); diff --git a/ts/components/Spinner.tsx b/ts/components/Spinner.tsx index e6fc9bf02674..9da55878c71f 100644 --- a/ts/components/Spinner.tsx +++ b/ts/components/Spinner.tsx @@ -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,37 +31,38 @@ export const Spinner = ({ size, svgSize, direction, -}: Props): JSX.Element => ( -
+}: Props): JSX.Element => { + const getClassName = getClassNamesFor('module-spinner', moduleClassName); + + return (
-
-
-); + style={{ + height: size, + width: size, + }} + > +
+
+
+ ); +}; diff --git a/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx b/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx index 711ab29ed3e7..ab0e0efffdd2 100644 --- a/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx +++ b/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx @@ -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; @@ -130,9 +131,7 @@ export const ChooseGroupMembersModal: FunctionComponent = ({

{i18n('AddGroupMembersModal--title')}

- { diff --git a/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx b/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx index dfc8cedae272..3247f9924929 100644 --- a/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx +++ b/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx @@ -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 -
- -
+ {Boolean(this.selectedContacts.length) && ( diff --git a/ts/components/leftPane/LeftPaneComposeHelper.tsx b/ts/components/leftPane/LeftPaneComposeHelper.tsx index ddcc77500287..e51680ff0277 100644 --- a/ts/components/leftPane/LeftPaneComposeHelper.tsx +++ b/ts/components/leftPane/LeftPaneComposeHelper.tsx @@ -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): ReactChild { return ( <> -
- -
+ {this.getRowCount() ? null : (
diff --git a/ts/test-both/util/getClassNamesFor_test.ts b/ts/test-both/util/getClassNamesFor_test.ts new file mode 100644 index 000000000000..eb266efdcd5c --- /dev/null +++ b/ts/test-both/util/getClassNamesFor_test.ts @@ -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'); + }); +}); diff --git a/ts/util/getClassNamesFor.ts b/ts/util/getClassNamesFor.ts new file mode 100644 index 000000000000..89547fe54a32 --- /dev/null +++ b/ts/util/getClassNamesFor.ts @@ -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 +): (modifier?: string) => string { + return modifier => { + const cx = modules.map(parentModule => + parentModule && modifier !== undefined + ? `${parentModule}${modifier}` + : undefined + ); + return classNames(cx); + }; +}