Keep reaction poppers visible at all times

This commit is contained in:
Evan Hahn 2021-08-20 14:36:27 -05:00 committed by GitHub
parent f11c366f53
commit 70d059beeb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 94 additions and 14 deletions

View file

@ -84,6 +84,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
checkForAccount: action('checkForAccount'),
clearSelectedMessage: action('clearSelectedMessage'),
collapseMetadata: overrideProps.collapseMetadata,
containerElementRef: React.createRef<HTMLElement>(),
conversationColor:
overrideProps.conversationColor ||
select('conversationColor', ConversationColors, ConversationColors[0]),

View file

@ -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<HTMLElement>;
i18n: LocalizerType;
interactionMode: InteractionModeType;
theme?: ThemeType;
@ -1443,7 +1445,13 @@ export class Message extends React.PureComponent<Props, State> {
{reactionPickerRoot &&
createPortal(
// eslint-disable-next-line consistent-return
<Popper placement="top" modifiers={[offsetDistanceModifier(4)]}>
<Popper
placement="top"
modifiers={[
offsetDistanceModifier(4),
this.popperPreventOverflowModifier(),
]}
>
{({ ref, style }) => (
<SmartReactionPicker
ref={ref}
@ -1801,6 +1809,23 @@ export class Message extends React.PureComponent<Props, State> {
);
}
private popperPreventOverflowModifier(): Partial<PreventOverflowModifier> {
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<Props, State> {
</Reference>
{reactionViewerRoot &&
createPortal(
<Popper placement={popperPlacement}>
<Popper
placement={popperPlacement}
strategy="fixed"
modifiers={[this.popperPreventOverflowModifier()]}
>
{({ ref, style }) => (
<ReactionViewer
ref={ref}

View file

@ -94,6 +94,8 @@ const _keyForError = (error: Error): string => {
export class MessageDetail extends React.Component<Props> {
private readonly focusRef = React.createRef<HTMLDivElement>();
private readonly messageContainerRef = React.createRef<HTMLDivElement>();
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<Props> {
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
<div className="module-message-detail__message-container">
<div
className="module-message-detail__message-container"
ref={this.messageContainerRef}
>
<Message
{...message}
renderingContext="conversation/MessageDetail"
checkForAccount={checkForAccount}
clearSelectedMessage={clearSelectedMessage}
contactNameColor={contactNameColor}
containerElementRef={this.messageContainerRef}
deleteMessage={deleteMessage}
deleteMessageForEveryone={deleteMessageForEveryone}
disableMenu

View file

@ -38,6 +38,7 @@ const defaultMessageProps: MessagesProps = {
canDownload: true,
checkForAccount: action('checkForAccount'),
clearSelectedMessage: action('default--clearSelectedMessage'),
containerElementRef: React.createRef<HTMLElement>(),
conversationColor: 'crimson',
conversationId: 'conversationId',
conversationType: 'direct', // override

View file

@ -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<HTMLElement>
) => (
<TimelineItem
id=""
isSelected={false}
@ -384,6 +390,7 @@ const renderItem = (id: string) => (
item={items[id]}
i18n={i18n}
interactionMode="keyboard"
containerElementRef={containerElementRef}
conversationId=""
renderContact={() => '*ContactName*'}
renderUniversalTimerNotification={() => (

View file

@ -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<HTMLElement>
) => JSX.Element;
renderLastSeenIndicator: (id: string) => JSX.Element;
renderHeroRow: (
@ -297,7 +298,9 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
public resizeFlag = false;
public listRef = React.createRef<List>();
private readonly containerRef = React.createRef<HTMLDivElement>();
private readonly listRef = React.createRef<List>();
public visibleRows: VisibleRowsType | undefined;
@ -808,7 +811,13 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
role="row"
>
<ErrorBoundary i18n={i18n} showDebugLog={() => window.showDebugLog()}>
{renderItem(messageId, id, this.resizeMessage, actions)}
{renderItem(
messageId,
id,
this.resizeMessage,
actions,
this.containerRef
)}
</ErrorBoundary>
</div>
);
@ -1502,6 +1511,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
tabIndex={-1}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
ref={this.containerRef}
>
{timelineWarning}

View file

@ -41,6 +41,7 @@ const renderUniversalTimerNotification = () => (
);
const getDefaultProps = () => ({
containerElementRef: React.createRef<HTMLElement>(),
conversationId: 'conversation-id',
id: 'asdf',
isSelected: false,

View file

@ -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<HTMLElement>;
conversationId: string;
item?: TimelineItemType;
id: string;
@ -179,6 +180,7 @@ export type PropsType = PropsLocalType &
export class TimelineItem extends React.PureComponent<PropsType> {
public render(): JSX.Element | null {
const {
containerElementRef,
conversationId,
id,
isSelected,
@ -204,6 +206,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
<Message
{...omit(this.props, ['item'])}
{...item.data}
containerElementRef={containerElementRef}
i18n={i18n}
theme={theme}
renderingContext="conversation/TimelineItem"

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { isEmpty, mapValues, pick } from 'lodash';
import React from 'react';
import React, { RefObject } from 'react';
import { connect } from 'react-redux';
import memoizee from 'memoizee';
@ -66,11 +66,13 @@ function renderItem(
messageId: string,
conversationId: string,
onHeightChange: (messageId: string) => unknown,
actionProps: TimelineActionsType
actionProps: TimelineActionsType,
containerElementRef: RefObject<HTMLElement>
): JSX.Element {
return (
<SmartTimelineItem
{...actionProps}
containerElementRef={containerElementRef}
conversationId={conversationId}
id={messageId}
onHeightChange={createBoundOnHeightChange(onHeightChange, messageId)}

View file

@ -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 { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
@ -21,6 +21,7 @@ import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
type ExternalProps = {
id: string;
conversationId: string;
containerElementRef: RefObject<HTMLElement>;
};
// 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,

View file

@ -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",

View file

@ -26,6 +26,8 @@ const excludedFilesRegexps = [
// Non-distributed files
'\\.d\\.ts$',
'.+\\.stories\\.js',
'.+\\.stories\\.tsx',
// High-traffic files in our project
'^app/.+(ts|js)',