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;
}
.drop-target {
.dropTarget {
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
&::before,
&::after {
content: "";
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: var(--border-radius-default);
pointer-events: none;
.dropOutlineContainer {
pointer-events: none;
overflow: visible !important;
opacity: 0;
transition: 250ms opacity;
z-index: 1;
}
position: absolute;
top: 0.5rem;
left: 0.5rem;
width: calc(100% - 1rem);
height: calc(100% - 1rem);
&::before {
background-image: var(--drag-target-border-hovered);
background-color: var(--color-background);
}
background-color: var(--color-background);
&::after {
content: attr(data-attach-description);
display: flex;
justify-content: center;
align-items: center;
color: var(--color-primary);
}
opacity: 0;
transition: opacity 0.2s;
z-index: 1;
}
.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 {
.drop-target::before {
opacity: 0.95;
.dropOutline {
animation-play-state: running;
}
.drop-target::after {
.dropOutlineContainer {
opacity: 1;
}
.caption-wrapper,
.attachments,
:global(.input-scroller) {
pointer-events: none;
.dropTarget {
&::after {
content: attr(data-attach-description);
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 useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useDerivedState from '../../../hooks/useDerivedState';
import useEffectOnce from '../../../hooks/useEffectOnce';
import useFlag from '../../../hooks/useFlag';
import useGetSelectionRange from '../../../hooks/useGetSelectionRange';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
import useResizeObserver from '../../../hooks/useResizeObserver';
import useScrolledState from '../../../hooks/useScrolledState';
import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip';
import useEmojiTooltip from './hooks/useEmojiTooltip';
@ -139,6 +141,10 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
onRemoveSymbol,
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 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(() => {
const mainButton = mainButtonRef.current;
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-dropzone
>
<svg className={styles.dropOutlineContainer}>
<rect className={styles.dropOutline} x="0" y="0" width="100%" height="100%" rx="8" />
</svg>
<div
className={buildClassName(
styles.attachments,

View File

@ -8,6 +8,8 @@
margin-left: auto;
margin-right: auto;
margin-bottom: 0.3125rem;
position: relative;
display: flex;
color: #a4acb3;
box-shadow: 0 1px 2px var(--color-default-shadow);
@ -16,9 +18,15 @@
padding: 0.75rem;
}
&.hovered .target-content {
color: var(--color-primary);
background-image: var(--drag-target-border-hovered);
&.hovered {
.target-content {
color: var(--color-primary);
}
.target-outline {
stroke: var(--color-primary);
animation-play-state: running;
}
}
& + & {
@ -29,16 +37,41 @@
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 {
pointer-events: none;
background-image: var(--drag-target-border);
border-radius: 0.5rem;
flex: 1 1 100%;
display: flex;
flex-direction: column;
align-items: 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 {
@ -78,4 +111,13 @@
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 React, { memo } from '../../../lib/teact/teact';
import React, { memo, useRef } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import useEffectOnce from '../../../hooks/useEffectOnce';
import useFlag from '../../../hooks/useFlag';
import useLastCallback from '../../../hooks/useLastCallback';
import useResizeObserver from '../../../hooks/useResizeObserver';
import './DropTarget.scss';
@ -14,9 +17,14 @@ export type OwnProps = {
};
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 handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
const handleDragLeave = useLastCallback((e: React.DragEvent<HTMLDivElement>) => {
const { relatedTarget: toTarget } = e;
if (toTarget) {
@ -24,7 +32,24 @@ const DropTarget: FC<OwnProps> = ({ isQuick, isGeneric, onFileSelect }) => {
}
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(
'DropTarget',
@ -34,11 +59,15 @@ const DropTarget: FC<OwnProps> = ({ isQuick, isGeneric, onFileSelect }) => {
return (
<div
className={className}
ref={ref}
onDrop={onFileSelect}
onDragEnter={markHovered}
onDragLeave={handleDragLeave}
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={`icon icon-${isQuick ? 'photo' : 'document'}`} />
<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-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%);
--layer-blackout-opacity: 0.1;