diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 006584e8d..db8d4b306 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -9,6 +9,7 @@ import { getAcct } from 'soapbox/utils/accounts'; import { displayFqn } from 'soapbox/utils/state'; import RelativeTimestamp from './relative-timestamp'; +import StopPropagation from './stop-propagation'; import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui'; import type { Account as AccountEntity } from 'soapbox/types/entities'; @@ -21,8 +22,6 @@ const InstanceFavicon: React.FC = ({ account }) => { const history = useHistory(); const handleClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - const timelineUrl = `/timeline/${account.domain}`; if (!(e.ctrlKey || e.metaKey)) { history.push(timelineUrl); @@ -167,106 +166,100 @@ const Account = ({ const LinkEl: any = withLinkToProfile ? Link : 'div'; return ( -
- - - {children}} - > - event.stopPropagation()} - > - - {emoji && ( - - )} - - - -
+ +
+ + {children}} + wrapper={(children) => {children}} > - event.stopPropagation()} - > -
- + + {emoji && ( + - - {account.verified && } -
+ )}
- - - @{username} +
+ {children}} + > + +
+ + + {account.verified && } +
+
+
+ + + + @{username} + + {account.favicon && ( + + )} + + {(timestamp) ? ( + <> + · + + {timestampUrl ? ( + + + + ) : ( + + )} + + ) : null} - {account.favicon && ( - - )} + {showEdit ? ( + <> + · - {(timestamp) ? ( - <> - · + + + ) : null} - {timestampUrl ? ( - event.stopPropagation()}> - - - ) : ( - - )} - - ) : null} - - {showEdit ? ( - <> - · - - - - ) : null} - - {actionType === 'muting' && account.mute_expires_at ? ( - <> - · - - - - ) : null} - - - {withAccountNote && ( - - )} - + {actionType === 'muting' && account.mute_expires_at ? ( + <> + · + + + + ) : null} + + + {withAccountNote && ( + + )} + +
+
+ +
+ {withRelationship ? renderAction() : null}
- -
- {withRelationship ? renderAction() : null} -
-
-
+
+ ); }; diff --git a/app/soapbox/components/polls/poll.tsx b/app/soapbox/components/polls/poll.tsx index 2b6f99392..2df985b7e 100644 --- a/app/soapbox/components/polls/poll.tsx +++ b/app/soapbox/components/polls/poll.tsx @@ -5,6 +5,7 @@ import { openModal } from 'soapbox/actions/modals'; import { vote } from 'soapbox/actions/polls'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import StopPropagation from '../stop-propagation'; import { Stack, Text } from '../ui'; import PollFooter from './poll-footer'; @@ -64,8 +65,7 @@ const Poll: React.FC = ({ id, status }): JSX.Element | null => { const showResults = poll.voted || poll.expired; return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
e.stopPropagation()}> + {!showResults && poll.multiple && ( {intl.formatMessage(messages.multiple)} @@ -93,7 +93,7 @@ const Poll: React.FC = ({ id, status }): JSX.Element | null => { selected={selected} /> -
+ ); }; diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index 2c976bf86..925f2115e 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -13,6 +13,7 @@ import OutlineBox from './outline-box'; import StatusContent from './status-content'; import StatusReplyMentions from './status-reply-mentions'; import SensitiveContentOverlay from './statuses/sensitive-content-overlay'; +import StopPropagation from './stop-propagation'; import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; @@ -91,58 +92,60 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => } return ( - - + - - - - - {(status.hidden) && ( - - )} - - - - - {(status.card || status.media_attachments.size > 0) && ( - + + + + + {(status.hidden) && ( + )} + + + + + {(status.card || status.media_attachments.size > 0) && ( + + )} + - - + + ); }; diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index 011fc63a8..54c181c71 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -1,4 +1,3 @@ -import classNames from 'clsx'; import { List as ImmutableList } from 'immutable'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -16,6 +15,7 @@ import { initReport } from 'soapbox/actions/reports'; import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses'; import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper'; import StatusActionButton from 'soapbox/components/status-action-button'; +import { HStack } from 'soapbox/components/ui'; import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks'; import { isLocal } from 'soapbox/utils/accounts'; @@ -127,8 +127,6 @@ const StatusActionBar: React.FC = ({ } else { onOpenUnauthorizedModal('REPLY'); } - - e.stopPropagation(); }; const handleShareClick = () => { @@ -146,18 +144,13 @@ const StatusActionBar: React.FC = ({ } else { onOpenUnauthorizedModal('FAVOURITE'); } - - e.stopPropagation(); }; const handleBookmarkClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(toggleBookmark(status)); }; const handleReblogClick: React.EventHandler = e => { - e.stopPropagation(); - if (me) { const modalReblog = () => dispatch(toggleReblog(status)); const boostModal = settings.get('boostModal'); @@ -172,8 +165,6 @@ const StatusActionBar: React.FC = ({ }; const handleQuoteClick: React.EventHandler = (e) => { - e.stopPropagation(); - if (me) { dispatch(quoteCompose(status)); } else { @@ -199,12 +190,10 @@ const StatusActionBar: React.FC = ({ }; const handleDeleteClick: React.EventHandler = (e) => { - e.stopPropagation(); doDeleteStatus(); }; const handleRedraftClick: React.EventHandler = (e) => { - e.stopPropagation(); doDeleteStatus(true); }; @@ -213,35 +202,29 @@ const StatusActionBar: React.FC = ({ }; const handlePinClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(togglePin(status)); }; const handleMentionClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(mentionCompose(status.account as Account)); }; const handleDirectClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(directCompose(status.account as Account)); }; const handleChatClick: React.EventHandler = (e) => { - e.stopPropagation(); const account = status.account as Account; dispatch(launchChat(account.id, history)); }; const handleMuteClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(initMuteModal(status.account as Account)); }; const handleBlockClick: React.EventHandler = (e) => { - e.stopPropagation(); - const account = status.get('account') as Account; + dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/ban.svg'), heading: , @@ -257,7 +240,6 @@ const StatusActionBar: React.FC = ({ }; const handleOpen: React.EventHandler = (e) => { - e.stopPropagation(); history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}`); }; @@ -269,12 +251,10 @@ const StatusActionBar: React.FC = ({ }; const handleReport: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(initReport(status.account as Account, status)); }; const handleConversationMuteClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(toggleMuteStatus(status)); }; @@ -282,8 +262,6 @@ const StatusActionBar: React.FC = ({ const { uri } = status; const textarea = document.createElement('textarea'); - e.stopPropagation(); - textarea.textContent = uri; textarea.style.position = 'fixed'; @@ -300,18 +278,15 @@ const StatusActionBar: React.FC = ({ }; const onModerate: React.MouseEventHandler = (e) => { - e.stopPropagation(); const account = status.account as Account; dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id })); }; const handleDeleteStatus: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(deleteStatusModal(intl, status.id)); }; const handleToggleStatusSensitivity: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive)); }; @@ -550,74 +525,77 @@ const StatusActionBar: React.FC = ({ const canShare = ('share' in navigator) && status.visibility === 'public'; return ( -
- - - {(features.quotePosts && me) ? ( - - {reblogButton} - - ) : ( - reblogButton - )} + + e.stopPropagation()} + onMouseDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()} + > + - {features.emojiReacts ? ( - + {(features.quotePosts && me) ? ( + + {reblogButton} + + ) : ( + reblogButton + )} + + {features.emojiReacts ? ( + + + + ) : ( - - ) : ( - - )} + )} - {canShare && ( - - )} + {canShare && ( + + )} - - - -
+ + + +
+
); }; diff --git a/app/soapbox/components/status-content.tsx b/app/soapbox/components/status-content.tsx index a495a9f82..2b2754176 100644 --- a/app/soapbox/components/status-content.tsx +++ b/app/soapbox/components/status-content.tsx @@ -12,6 +12,7 @@ import { isRtl } from '../rtl'; import Poll from './polls/poll'; import './status-content.css'; +import StopPropagation from './stop-propagation'; import type { Status, Mention } from 'soapbox/types/entities'; @@ -29,10 +30,12 @@ interface IReadMoreButton { /** Button to expand a truncated status (due to too much content) */ const ReadMoreButton: React.FC = ({ onClick }) => ( - + + + ); interface IStatusContent { @@ -103,6 +106,10 @@ const StatusContent: React.FC = ({ status, onClick, collapsable link.setAttribute('title', link.href); link.addEventListener('click', onLinkClick.bind(link), false); } + + // Prevent bubbling + link.addEventListener('mouseup', e => e.stopPropagation()); + link.addEventListener('mousedown', e => e.stopPropagation()); }); }; diff --git a/app/soapbox/components/status-media.tsx b/app/soapbox/components/status-media.tsx index 71177cb12..f3d418d30 100644 --- a/app/soapbox/components/status-media.tsx +++ b/app/soapbox/components/status-media.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { openModal } from 'soapbox/actions/modals'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; +import StopPropagation from 'soapbox/components/stop-propagation'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card'; import Card from 'soapbox/features/status/components/card'; import Bundle from 'soapbox/features/ui/components/bundle'; @@ -173,7 +174,15 @@ const StatusMedia: React.FC = ({ ); } - return media; + if (media) { + return ( + + {media} + + ); + } else { + return null; + } }; export default StatusMedia; diff --git a/app/soapbox/components/status-reply-mentions.tsx b/app/soapbox/components/status-reply-mentions.tsx index 0964c84ae..3adde6b65 100644 --- a/app/soapbox/components/status-reply-mentions.tsx +++ b/app/soapbox/components/status-reply-mentions.tsx @@ -5,6 +5,7 @@ import { Link } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper'; import HoverStatusWrapper from 'soapbox/components/hover-status-wrapper'; +import StopPropagation from 'soapbox/components/stop-propagation'; import { useAppDispatch } from 'soapbox/hooks'; import type { Account, Status } from 'soapbox/types/entities'; @@ -18,8 +19,6 @@ const StatusReplyMentions: React.FC = ({ status, hoverable const dispatch = useAppDispatch(); const handleOpenMentionsModal: React.MouseEventHandler = (e) => { - e.stopPropagation(); - const account = status.account as Account; dispatch(openModal('MENTIONS', { @@ -50,7 +49,7 @@ const StatusReplyMentions: React.FC = ({ status, hoverable // The typical case with a reply-to and a list of mentions. const accounts = to.slice(0, 2).map(account => { const link = ( - e.stopPropagation()}>@{account.username} + @{account.username} ); if (hoverable) { @@ -73,32 +72,34 @@ const StatusReplyMentions: React.FC = ({ status, hoverable } return ( -
- , - hover: (children: React.ReactNode) => { - if (hoverable) { - return ( - - - {children} - - - ); - } else { - return children; - } - }, - }} - /> -
+ +
+ , + hover: (children: React.ReactNode) => { + if (hoverable) { + return ( + + + {children} + + + ); + } else { + return children; + } + }, + }} + /> +
+
); }; diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 846735dc3..31af2cbaa 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -235,7 +235,8 @@ const Status: React.FC = (props) => { reblogElement = ( event.stopPropagation()} + onClick={e => e.stopPropagation()} + onMouseUp={e => e.stopPropagation()} className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' > @@ -258,7 +259,8 @@ const Status: React.FC = (props) => {
event.stopPropagation()} + onClick={e => e.stopPropagation()} + onMouseUp={e => e.stopPropagation()} className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' > diff --git a/app/soapbox/components/statuses/sensitive-content-overlay.tsx b/app/soapbox/components/statuses/sensitive-content-overlay.tsx index 59faef161..21411bf5c 100644 --- a/app/soapbox/components/statuses/sensitive-content-overlay.tsx +++ b/app/soapbox/components/statuses/sensitive-content-overlay.tsx @@ -5,6 +5,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { useSettings, useSoapboxConfig } from 'soapbox/hooks'; import { defaultMediaVisibility } from 'soapbox/utils/status'; +import StopPropagation from '../stop-propagation'; import { Button, HStack, Text } from '../ui'; import type { Status as StatusEntity } from 'soapbox/types/entities'; @@ -38,9 +39,7 @@ const SensitiveContentOverlay = React.forwardRef(defaultMediaVisibility(status, displayMedia)); - const toggleVisibility = (event: React.MouseEvent) => { - event.stopPropagation(); - + const toggleVisibility = () => { if (onToggleVisibility) { onToggleVisibility(); } else { @@ -64,13 +63,15 @@ const SensitiveContentOverlay = React.forwardRef {visible ? ( - - - )} - - ) : null} - - + + {isUnderReview ? ( + <> + {links.get('support') && ( + + + + )} + + ) : null} + + +
)} diff --git a/app/soapbox/components/stop-propagation.tsx b/app/soapbox/components/stop-propagation.tsx new file mode 100644 index 000000000..f8eff7b4e --- /dev/null +++ b/app/soapbox/components/stop-propagation.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +interface IStopPropagation { + /** Children to render within the bubble. */ + children: React.ReactNode, + /** Whether to prevent mouse events from bubbling. (default: `true`) */ + enabled?: boolean, +} + +/** + * Prevent mouse events from bubbling up. + * + * Why is this needed? Because `onClick`, `onMouseDown`, and `onMouseUp` are 3 separate events. + * To prevent a lot of code duplication, this component can stop all mouse events. + * Plus, placing it in the component tree makes it more readable. + */ +const StopPropagation: React.FC = ({ children, enabled = true }) => { + + const handler: React.MouseEventHandler = (e) => { + if (enabled) { + e.stopPropagation(); + } + }; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
+ {children} +
+ ); +}; + +export default StopPropagation; \ No newline at end of file diff --git a/app/soapbox/components/translate-button.tsx b/app/soapbox/components/translate-button.tsx index 07c778fd2..7c40a4a14 100644 --- a/app/soapbox/components/translate-button.tsx +++ b/app/soapbox/components/translate-button.tsx @@ -4,6 +4,7 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; +import StopPropagation from './stop-propagation'; import { Stack } from './ui'; import type { Status } from 'soapbox/types/entities'; @@ -42,17 +43,21 @@ const TranslateButton: React.FC = ({ status }) => { - + + + ); } return ( - + + + ); }; diff --git a/app/soapbox/components/ui/button/button.tsx b/app/soapbox/components/ui/button/button.tsx index 97de8862b..1b0528efb 100644 --- a/app/soapbox/components/ui/button/button.tsx +++ b/app/soapbox/components/ui/button/button.tsx @@ -8,7 +8,7 @@ import { useButtonStyles } from './useButtonStyles'; import type { ButtonSizes, ButtonThemes } from './useButtonStyles'; -interface IButton { +interface IButton extends Pick, 'onClick' | 'onMouseUp'> { /** Whether this button expands the width of its container. */ block?: boolean, /** Elements inside the