Keep reaction poppers visible at all times
This commit is contained in:
parent
f11c366f53
commit
70d059beeb
12 changed files with 94 additions and 14 deletions
|
@ -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]),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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={() => (
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ const renderUniversalTimerNotification = () => (
|
|||
);
|
||||
|
||||
const getDefaultProps = () => ({
|
||||
containerElementRef: React.createRef<HTMLElement>(),
|
||||
conversationId: 'conversation-id',
|
||||
id: 'asdf',
|
||||
isSelected: false,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -26,6 +26,8 @@ const excludedFilesRegexps = [
|
|||
|
||||
// Non-distributed files
|
||||
'\\.d\\.ts$',
|
||||
'.+\\.stories\\.js',
|
||||
'.+\\.stories\\.tsx',
|
||||
|
||||
// High-traffic files in our project
|
||||
'^app/.+(ts|js)',
|
||||
|
|
Loading…
Reference in a new issue