DropTarget: Correctly display background image animation in Safari (#5053)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2024-10-20 18:53:54 +02:00
parent 66ec9f5732
commit f2def08a13
5 changed files with 162 additions and 44 deletions

View File

@ -136,56 +136,71 @@
gap: 0.5rem; gap: 0.5rem;
} }
.drop-target { .dropTarget {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
}
&::before, .dropOutlineContainer {
&::after { pointer-events: none;
content: ""; overflow: visible !important;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: var(--border-radius-default);
pointer-events: none;
opacity: 0; position: absolute;
transition: 250ms opacity; top: 0.5rem;
z-index: 1; left: 0.5rem;
} width: calc(100% - 1rem);
height: calc(100% - 1rem);
&::before { background-color: var(--color-background);
background-image: var(--drag-target-border-hovered);
background-color: var(--color-background);
}
&::after { opacity: 0;
content: attr(data-attach-description); transition: opacity 0.2s;
display: flex; z-index: 1;
justify-content: center; }
align-items: center;
color: var(--color-primary); .dropOutline {
} fill: none;
stroke: var(--color-primary);
stroke-width: 2;
stroke-dasharray: 11, 8;
stroke-linecap: round;
stroke-dashoffset: 0;
animation: outline 0.5s linear infinite;
animation-play-state: paused;
} }
.hovered { .hovered {
.drop-target::before { .dropOutline {
opacity: 0.95; animation-play-state: running;
} }
.drop-target::after { .dropOutlineContainer {
opacity: 1; opacity: 1;
} }
.caption-wrapper, .dropTarget {
.attachments, &::after {
:global(.input-scroller) { content: attr(data-attach-description);
pointer-events: none; position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-primary);
z-index: 1;
}
.caption-wrapper,
.attachments,
:global(.input-scroller),
:global(.custom-scroll),
:global(.custom-scroll-x) {
pointer-events: none !important;
}
} }
} }
@ -212,3 +227,12 @@
} }
} }
} }
@keyframes outline {
from {
stroke-dashoffset: 0;
}
to {
stroke-dashoffset: -19;
}
}

View File

@ -33,11 +33,13 @@ import { getHtmlTextLength } from './helpers/getHtmlTextLength';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useDerivedState from '../../../hooks/useDerivedState'; import useDerivedState from '../../../hooks/useDerivedState';
import useEffectOnce from '../../../hooks/useEffectOnce';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useGetSelectionRange from '../../../hooks/useGetSelectionRange'; import useGetSelectionRange from '../../../hooks/useGetSelectionRange';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated'; import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
import useResizeObserver from '../../../hooks/useResizeObserver';
import useScrolledState from '../../../hooks/useScrolledState'; import useScrolledState from '../../../hooks/useScrolledState';
import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip'; import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip';
import useEmojiTooltip from './hooks/useEmojiTooltip'; import useEmojiTooltip from './hooks/useEmojiTooltip';
@ -139,6 +141,10 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
onRemoveSymbol, onRemoveSymbol,
onEmojiSelect, onEmojiSelect,
}) => { }) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const svgRef = useRef<SVGSVGElement>(null);
const { addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings } = getActions(); const { addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings } = getActions();
const lang = useOldLang(); const lang = useOldLang();
@ -388,6 +394,23 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
})); }));
}); });
const handleResize = useLastCallback(() => {
const svg = svgRef.current;
if (!svg) {
return;
}
// Get inner width, without padding
const { width, height } = svg.getBoundingClientRect();
svg.viewBox.baseVal.width = width;
svg.viewBox.baseVal.height = height;
});
// Can't listen for SVG resize
useResizeObserver(ref, handleResize);
useEffectOnce(handleResize);
useEffect(() => { useEffect(() => {
const mainButton = mainButtonRef.current; const mainButton = mainButtonRef.current;
const input = document.getElementById(ATTACHMENT_MODAL_INPUT_ID); const input = document.getElementById(ATTACHMENT_MODAL_INPUT_ID);
@ -575,6 +598,9 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
data-attach-description={lang('Preview.Dragging.AddItems', 10)} data-attach-description={lang('Preview.Dragging.AddItems', 10)}
data-dropzone data-dropzone
> >
<svg className={styles.dropOutlineContainer}>
<rect className={styles.dropOutline} x="0" y="0" width="100%" height="100%" rx="8" />
</svg>
<div <div
className={buildClassName( className={buildClassName(
styles.attachments, styles.attachments,

View File

@ -8,6 +8,8 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 0.3125rem; margin-bottom: 0.3125rem;
position: relative;
display: flex; display: flex;
color: #a4acb3; color: #a4acb3;
box-shadow: 0 1px 2px var(--color-default-shadow); box-shadow: 0 1px 2px var(--color-default-shadow);
@ -16,9 +18,15 @@
padding: 0.75rem; padding: 0.75rem;
} }
&.hovered .target-content { &.hovered {
color: var(--color-primary); .target-content {
background-image: var(--drag-target-border-hovered); color: var(--color-primary);
}
.target-outline {
stroke: var(--color-primary);
animation-play-state: running;
}
} }
& + & { & + & {
@ -29,16 +37,41 @@
margin-bottom: 0; margin-bottom: 0;
} }
.target-outline-container {
pointer-events: none;
overflow: visible;
width: 100%;
height: 100%;
}
.target-outline {
fill: none;
stroke: var(--color-placeholders);
stroke-width: 2;
stroke-dasharray: 11, 8;
stroke-linecap: round;
stroke-dashoffset: 0;
transition: 0.2s stroke;
animation: outline 0.5s linear infinite;
animation-play-state: paused;
}
.target-content { .target-content {
pointer-events: none; pointer-events: none;
background-image: var(--drag-target-border);
border-radius: 0.5rem; border-radius: 0.5rem;
flex: 1 1 100%; flex: 1 1 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: 0.2s color, 0.2s background-image; transition: 0.2s color;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
} }
.icon { .icon {
@ -78,4 +111,13 @@
font-size: 0.875rem; font-size: 0.875rem;
} }
} }
@keyframes outline {
from {
stroke-dashoffset: 0;
}
to {
stroke-dashoffset: -19;
}
}
} }

View File

@ -1,9 +1,12 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact'; import React, { memo, useRef } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useEffectOnce from '../../../hooks/useEffectOnce';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useLastCallback from '../../../hooks/useLastCallback';
import useResizeObserver from '../../../hooks/useResizeObserver';
import './DropTarget.scss'; import './DropTarget.scss';
@ -14,9 +17,14 @@ export type OwnProps = {
}; };
const DropTarget: FC<OwnProps> = ({ isQuick, isGeneric, onFileSelect }) => { const DropTarget: FC<OwnProps> = ({ isQuick, isGeneric, onFileSelect }) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const svgRef = useRef<SVGSVGElement>(null);
const [isHovered, markHovered, unmarkHovered] = useFlag(); const [isHovered, markHovered, unmarkHovered] = useFlag();
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => { const handleDragLeave = useLastCallback((e: React.DragEvent<HTMLDivElement>) => {
const { relatedTarget: toTarget } = e; const { relatedTarget: toTarget } = e;
if (toTarget) { if (toTarget) {
@ -24,7 +32,24 @@ const DropTarget: FC<OwnProps> = ({ isQuick, isGeneric, onFileSelect }) => {
} }
unmarkHovered(); unmarkHovered();
}; });
const handleResize = useLastCallback(() => {
const svg = svgRef.current;
if (!svg) {
return;
}
// Get inner width, without padding
const { width, height } = svg.getBoundingClientRect();
svg.viewBox.baseVal.width = width;
svg.viewBox.baseVal.height = height;
});
// Can't listen for SVG resize
useResizeObserver(ref, handleResize);
useEffectOnce(handleResize);
const className = buildClassName( const className = buildClassName(
'DropTarget', 'DropTarget',
@ -34,11 +59,15 @@ const DropTarget: FC<OwnProps> = ({ isQuick, isGeneric, onFileSelect }) => {
return ( return (
<div <div
className={className} className={className}
ref={ref}
onDrop={onFileSelect} onDrop={onFileSelect}
onDragEnter={markHovered} onDragEnter={markHovered}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
data-dropzone data-dropzone
> >
<svg className="target-outline-container">
<rect className="target-outline" x="0" y="0" width="100%" height="100%" rx="8" />
</svg>
<div className="target-content"> <div className="target-content">
<div className={`icon icon-${isQuick ? 'photo' : 'document'}`} /> <div className={`icon icon-${isQuick ? 'photo' : 'document'}`} />
<div className="title">Drop files here to send them</div> <div className="title">Drop files here to send them</div>

View File

@ -286,9 +286,6 @@ $color-message-story-mention-to: #74bcff;
--spinner-gray-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzcwNzU3OSIvPjwvc3ZnPg==); --spinner-gray-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzcwNzU3OSIvPjwvc3ZnPg==);
--spinner-yellow-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iI0ZERDc2NCIvPjwvc3ZnPg==); --spinner-yellow-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iI0ZERDc2NCIvPjwvc3ZnPg==);
--drag-target-border: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='%23DDDFE0' stroke-width='4' stroke-dasharray='9.1%2c 10.5' stroke-dashoffset='3' stroke-linecap='round'/%3e%3c/svg%3e");
--drag-target-border-hovered: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='%2363A2E3' stroke-width='4' stroke-dasharray='9.1%2c 10.5' stroke-dashoffset='3' stroke-linecap='round'/%3e%3c/svg%3e");
--premium-gradient: linear-gradient(84.4deg, #6C93FF -4.85%, #976FFF 51.72%, #DF69D1 110.7%); --premium-gradient: linear-gradient(84.4deg, #6C93FF -4.85%, #976FFF 51.72%, #DF69D1 110.7%);
--layer-blackout-opacity: 0.1; --layer-blackout-opacity: 0.1;