refactor
This commit is contained in:
parent
5dcff8f60a
commit
b75cf1fba6
|
|
@ -21,7 +21,7 @@ import { TreeItem } from './treeItem';
|
||||||
import { CopyToClipboard } from './copyToClipboard';
|
import { CopyToClipboard } from './copyToClipboard';
|
||||||
import './links.css';
|
import './links.css';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx, useFlash } from '@web/uiUtils';
|
||||||
|
|
||||||
export function navigate(href: string | URL) {
|
export function navigate(href: string | URL) {
|
||||||
window.history.pushState({}, '', href);
|
window.history.pushState({}, '', href);
|
||||||
|
|
@ -73,8 +73,8 @@ export const AttachmentLink: React.FunctionComponent<{
|
||||||
linkName?: string,
|
linkName?: string,
|
||||||
openInNewTab?: boolean,
|
openInNewTab?: boolean,
|
||||||
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
|
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
|
||||||
const isAnchored = useIsAnchored('attachment-' + result.attachments.indexOf(attachment));
|
const [flash, triggerFlash] = useFlash();
|
||||||
const searchParams = React.useContext(SearchParamsContext);
|
useAnchor('attachment-' + result.attachments.indexOf(attachment), triggerFlash);
|
||||||
return <TreeItem title={<span>
|
return <TreeItem title={<span>
|
||||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||||
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||||
|
|
@ -85,7 +85,7 @@ export const AttachmentLink: React.FunctionComponent<{
|
||||||
)}
|
)}
|
||||||
</span>} loadChildren={attachment.body ? () => {
|
</span>} loadChildren={attachment.body ? () => {
|
||||||
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
||||||
} : undefined} depth={0} style={{ lineHeight: '32px' }} flash={isAnchored ? searchParams : undefined}></TreeItem>;
|
} : undefined} depth={0} style={{ lineHeight: '32px' }} flash={flash}></TreeItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||||
|
|
@ -119,12 +119,12 @@ const kMissingContentType = 'x-playwright/missing';
|
||||||
|
|
||||||
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
||||||
|
|
||||||
export function useAnchor(id: AnchorID, onReveal: () => void) {
|
export function useAnchor(id: AnchorID, onReveal: React.EffectCallback) {
|
||||||
const searchParams = React.useContext(SearchParamsContext);
|
const searchParams = React.useContext(SearchParamsContext);
|
||||||
const isAnchored = useIsAnchored(id);
|
const isAnchored = useIsAnchored(id);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isAnchored)
|
if (isAnchored)
|
||||||
onReveal();
|
return onReveal();
|
||||||
}, [isAnchored, onReveal, searchParams]);
|
}, [isAnchored, onReveal, searchParams]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './treeItem.css';
|
import './treeItem.css';
|
||||||
import * as icons from './icons';
|
import * as icons from './icons';
|
||||||
import { clsx, useFlash } from '@web/uiUtils';
|
import { clsx } from '@web/uiUtils';
|
||||||
|
|
||||||
export const TreeItem: React.FunctionComponent<{
|
export const TreeItem: React.FunctionComponent<{
|
||||||
title: JSX.Element,
|
title: JSX.Element,
|
||||||
|
|
@ -26,11 +26,10 @@ export const TreeItem: React.FunctionComponent<{
|
||||||
expandByDefault?: boolean,
|
expandByDefault?: boolean,
|
||||||
depth: number,
|
depth: number,
|
||||||
style?: React.CSSProperties,
|
style?: React.CSSProperties,
|
||||||
flash?: any
|
flash?: boolean
|
||||||
}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => {
|
}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => {
|
||||||
const addFlashClass = useFlash(flash);
|
|
||||||
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
||||||
return <div className={clsx('tree-item', addFlashClass && 'yellow-flash')} style={style}>
|
return <div className={clsx('tree-item', flash && 'yellow-flash')} style={style}>
|
||||||
<span className='tree-item-title' style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
<span className='tree-item-title' style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||||
{loadChildren && !!expanded && icons.downArrow()}
|
{loadChildren && !!expanded && icons.downArrow()}
|
||||||
{loadChildren && !expanded && icons.rightArrow()}
|
{loadChildren && !expanded && icons.rightArrow()}
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,18 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
|
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
|
||||||
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
||||||
|
const [flash, triggerFlash] = useFlash();
|
||||||
const ref = React.useRef<HTMLSpanElement>(null);
|
const ref = React.useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
||||||
const hasContent = !!attachment.sha1 || !!attachment.path;
|
const hasContent = !!attachment.sha1 || !!attachment.path;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (reveal)
|
if (reveal) {
|
||||||
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [reveal]);
|
return triggerFlash();
|
||||||
const flash = useFlash(reveal);
|
}
|
||||||
|
}, [reveal, triggerFlash]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (expanded && attachmentText === null && placeholder === null) {
|
if (expanded && attachmentText === null && placeholder === null) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { EffectCallback } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
// Recalculates the value when dependencies change.
|
// Recalculates the value when dependencies change.
|
||||||
|
|
@ -225,15 +226,21 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) {
|
||||||
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
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');
|
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(): [boolean, EffectCallback] {
|
||||||
export function useFlash(flash: any | undefined) {
|
const [flash, setFlash] = React.useState(false);
|
||||||
const [flashState, setFlashState] = React.useState(false);
|
const trigger = React.useCallback<React.EffectCallback>(() => {
|
||||||
React.useEffect(() => {
|
let timeout: number | undefined;
|
||||||
if (flash) {
|
setFlash(currentlyFlashing => {
|
||||||
setFlashState(true);
|
if (!currentlyFlashing) {
|
||||||
const timeout = setTimeout(() => setFlashState(false), 1000);
|
timeout = setTimeout(() => setFlash(false), 1000) as any;
|
||||||
return () => clearTimeout(timeout);
|
return true;
|
||||||
}
|
}
|
||||||
}, [flash]);
|
|
||||||
return flashState;
|
// 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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue