From 70d059beeb5715a20dbf0296072b1eea074c7a1c Mon Sep 17 00:00:00 2001 From: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com> Date: Fri, 20 Aug 2021 14:36:27 -0500 Subject: [PATCH] Keep reaction poppers visible at all times --- .../conversation/Message.stories.tsx | 1 + ts/components/conversation/Message.tsx | 35 +++++++++++++++++-- ts/components/conversation/MessageDetail.tsx | 8 ++++- ts/components/conversation/Quote.stories.tsx | 1 + .../conversation/Timeline.stories.tsx | 9 ++++- ts/components/conversation/Timeline.tsx | 18 +++++++--- .../conversation/TimelineItem.stories.tsx | 1 + ts/components/conversation/TimelineItem.tsx | 5 ++- ts/state/smart/Timeline.tsx | 6 ++-- ts/state/smart/TimelineItem.tsx | 6 ++-- ts/util/lint/exceptions.json | 16 +++++++++ ts/util/lint/linter.ts | 2 ++ 12 files changed, 94 insertions(+), 14 deletions(-) diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index 459bace593c..0e18ba45738 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -84,6 +84,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ checkForAccount: action('checkForAccount'), clearSelectedMessage: action('clearSelectedMessage'), collapseMetadata: overrideProps.collapseMetadata, + containerElementRef: React.createRef(), conversationColor: overrideProps.conversationColor || select('conversationColor', ConversationColors, ConversationColors[0]), diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 7dd6a659ca5..257b2b8afc6 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -1,12 +1,13 @@ // Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React from 'react'; +import React, { RefObject } from 'react'; import ReactDOM, { createPortal } from 'react-dom'; import classNames from 'classnames'; import { drop, groupBy, orderBy, take, unescape } from 'lodash'; import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; import { Manager, Popper, Reference } from 'react-popper'; +import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow'; import { ConversationType, @@ -188,6 +189,7 @@ export type PropsData = { }; export type PropsHousekeeping = { + containerElementRef: RefObject; i18n: LocalizerType; interactionMode: InteractionModeType; theme?: ThemeType; @@ -1443,7 +1445,13 @@ export class Message extends React.PureComponent { {reactionPickerRoot && createPortal( // eslint-disable-next-line consistent-return - + {({ ref, style }) => ( { ); } + private popperPreventOverflowModifier(): Partial { + const { containerElementRef } = this.props; + return { + name: 'preventOverflow', + options: { + altAxis: true, + boundary: containerElementRef.current || undefined, + padding: { + bottom: 16, + left: 8, + right: 8, + top: 16, + }, + }, + }; + } + public toggleReactionViewer = (onlyRemove = false): void => { this.setState(({ reactionViewerRoot }) => { if (reactionViewerRoot) { @@ -2022,7 +2047,11 @@ export class Message extends React.PureComponent { {reactionViewerRoot && createPortal( - + {({ ref, style }) => ( { export class MessageDetail extends React.Component { private readonly focusRef = React.createRef(); + private readonly messageContainerRef = React.createRef(); + public componentDidMount(): void { // When this component is created, it's initially not part of the DOM, and then it's // added off-screen and animated in. This ensures that the focus takes. @@ -289,13 +291,17 @@ export class MessageDetail extends React.Component { return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
-
+
(), conversationColor: 'crimson', conversationId: 'conversationId', conversationType: 'direct', // override diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 59883abf98e..cce7d2eb046 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -376,7 +376,13 @@ const actions = () => ({ unblurAvatar: action('unblurAvatar'), }); -const renderItem = (id: string) => ( +const renderItem = ( + id: string, + _conversationId: unknown, + _onHeightChange: unknown, + _actionProps: unknown, + containerElementRef: React.RefObject +) => ( ( item={items[id]} i18n={i18n} interactionMode="keyboard" + containerElementRef={containerElementRef} conversationId="" renderContact={() => '*ContactName*'} renderUniversalTimerNotification={() => ( diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index ae6eb27af62..b8ed5572858 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -3,7 +3,7 @@ import { debounce, get, isNumber, pick, identity } from 'lodash'; import classNames from 'classnames'; -import React, { CSSProperties, ReactChild, ReactNode } from 'react'; +import React, { CSSProperties, ReactChild, ReactNode, RefObject } from 'react'; import { createSelector } from 'reselect'; import { AutoSizer, @@ -107,7 +107,8 @@ type PropsHousekeepingType = { id: string, conversationId: string, onHeightChange: (messageId: string) => unknown, - actions: PropsActionsType + actions: PropsActionsType, + containerElementRef: RefObject ) => JSX.Element; renderLastSeenIndicator: (id: string) => JSX.Element; renderHeroRow: ( @@ -297,7 +298,9 @@ export class Timeline extends React.PureComponent { public resizeFlag = false; - public listRef = React.createRef(); + private readonly containerRef = React.createRef(); + + private readonly listRef = React.createRef(); public visibleRows: VisibleRowsType | undefined; @@ -808,7 +811,13 @@ export class Timeline extends React.PureComponent { role="row" > window.showDebugLog()}> - {renderItem(messageId, id, this.resizeMessage, actions)} + {renderItem( + messageId, + id, + this.resizeMessage, + actions, + this.containerRef + )}
); @@ -1502,6 +1511,7 @@ export class Timeline extends React.PureComponent { tabIndex={-1} onBlur={this.handleBlur} onKeyDown={this.handleKeyDown} + ref={this.containerRef} > {timelineWarning} diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index 28a3bbf2ae2..2588395f4a5 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -41,6 +41,7 @@ const renderUniversalTimerNotification = () => ( ); const getDefaultProps = () => ({ + containerElementRef: React.createRef(), conversationId: 'conversation-id', id: 'asdf', isSelected: false, diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx index dfb4bf47a87..1bbbb9950c5 100644 --- a/ts/components/conversation/TimelineItem.tsx +++ b/ts/components/conversation/TimelineItem.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React from 'react'; +import React, { RefObject } from 'react'; import { omit } from 'lodash'; import { LocalizerType, ThemeType } from '../../types/Util'; @@ -153,6 +153,7 @@ export type TimelineItemType = | VerificationNotificationType; type PropsLocalType = { + containerElementRef: RefObject; conversationId: string; item?: TimelineItemType; id: string; @@ -179,6 +180,7 @@ export type PropsType = PropsLocalType & export class TimelineItem extends React.PureComponent { public render(): JSX.Element | null { const { + containerElementRef, conversationId, id, isSelected, @@ -204,6 +206,7 @@ export class TimelineItem extends React.PureComponent { unknown, - actionProps: TimelineActionsType + actionProps: TimelineActionsType, + containerElementRef: RefObject ): JSX.Element { return ( ; }; // Workaround: A react component's required properties are filtering up through connect() @@ -38,7 +39,7 @@ function renderUniversalTimerNotification(): JSX.Element { } const mapStateToProps = (state: StateType, props: ExternalProps) => { - const { id, conversationId } = props; + const { id, conversationId, containerElementRef } = props; const messageSelector = getMessageSelector(state); const item = messageSelector(id); @@ -51,6 +52,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { return { item, id, + containerElementRef, conversationId, conversationColor: conversation?.conversationColor, customColor: conversation?.customColor, diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index c6e0f170b1c..33e76c78d55 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -13845,6 +13845,14 @@ "updated": "2019-11-01T22:46:33.013Z", "reasonDetail": "Used for setting focus only" }, + { + "rule": "React-createRef", + "path": "ts/components/conversation/MessageDetail.js", + "line": " this.messageContainerRef = react_1.default.createRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-08-20T16:48:00.885Z", + "reasonDetail": "Needed to confine Poppers. We don't actually manipulate this DOM reference." + }, { "rule": "React-useRef", "path": "ts/components/conversation/Quote.js", @@ -13869,6 +13877,14 @@ "updated": "2019-07-31T00:19:18.696Z", "reasonDetail": "Timeline needs to interact with its child List directly" }, + { + "rule": "React-createRef", + "path": "ts/components/conversation/Timeline.js", + "line": " this.containerRef = react_1.default.createRef();", + "reasonCategory": "usageTrusted", + "updated": "2021-08-20T16:48:00.885Z", + "reasonDetail": "Needed to confine Poppers. We don't actually manipulate this DOM reference." + }, { "rule": "React-useRef", "path": "ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.js", diff --git a/ts/util/lint/linter.ts b/ts/util/lint/linter.ts index 13707e22780..0e1f202e143 100644 --- a/ts/util/lint/linter.ts +++ b/ts/util/lint/linter.ts @@ -26,6 +26,8 @@ const excludedFilesRegexps = [ // Non-distributed files '\\.d\\.ts$', + '.+\\.stories\\.js', + '.+\\.stories\\.tsx', // High-traffic files in our project '^app/.+(ts|js)',