+ return
{ onClick?.(); setExpanded(!expanded); }} >
{loadChildren && !!expanded && icons.downArrow()}
{loadChildren && !expanded && icons.rightArrow()}
diff --git a/packages/trace-viewer/src/ui/attachmentsTab.tsx b/packages/trace-viewer/src/ui/attachmentsTab.tsx
index f6c0dc2d04..818e180ff4 100644
--- a/packages/trace-viewer/src/ui/attachmentsTab.tsx
+++ b/packages/trace-viewer/src/ui/attachmentsTab.tsx
@@ -37,16 +37,18 @@ const ExpandableAttachment: React.FunctionComponent =
const [expanded, setExpanded] = React.useState(false);
const [attachmentText, setAttachmentText] = React.useState(null);
const [placeholder, setPlaceholder] = React.useState(null);
+ const [flash, triggerFlash] = useFlash();
const ref = React.useRef(null);
const isTextAttachment = isTextualMimeType(attachment.contentType);
const hasContent = !!attachment.sha1 || !!attachment.path;
React.useEffect(() => {
- if (reveal)
+ if (reveal) {
ref.current?.scrollIntoView({ behavior: 'smooth' });
- }, [reveal]);
- const flash = useFlash(reveal);
+ return triggerFlash();
+ }
+ }, [reveal, triggerFlash]);
React.useEffect(() => {
if (expanded && attachmentText === null && placeholder === null) {
diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts
index fe24828b97..c1ba6799b7 100644
--- a/packages/web/src/uiUtils.ts
+++ b/packages/web/src/uiUtils.ts
@@ -14,6 +14,7 @@
limitations under the License.
*/
+import type { EffectCallback } from 'react';
import React from 'react';
// Recalculates the value when dependencies change.
@@ -225,15 +226,21 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) {
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
-// flash is retriggered whenever the value changes
-export function useFlash(flash: any | undefined) {
- const [flashState, setFlashState] = React.useState(false);
- React.useEffect(() => {
- if (flash) {
- setFlashState(true);
- const timeout = setTimeout(() => setFlashState(false), 1000);
- return () => clearTimeout(timeout);
- }
- }, [flash]);
- return flashState;
+export function useFlash(): [boolean, EffectCallback] {
+ const [flash, setFlash] = React.useState(false);
+ const trigger = React.useCallback(() => {
+ let timeout: number | undefined;
+ setFlash(currentlyFlashing => {
+ if (!currentlyFlashing) {
+ timeout = setTimeout(() => setFlash(false), 1000) as any;
+ return true;
+ }
+
+ // It's already flashing, so we remove the class and re-add it after 50ms to trigger the animation again.
+ timeout = setTimeout(() => setFlash(true), 50) as any;
+ return false;
+ });
+ return () => clearTimeout(timeout);
+ }, [setFlash]);
+ return [flash, trigger];
}