[DevTools] Use Visually Lighter Skeletons (#34185)

The skeletons right now are too jarring because they're visually heavier
than the content that comes in later. This makes them draw attention to
themselves as flashing things.

A good skeleton and loading indicator should ideally start as invisible
as possible and then gradually become more visible the longer time
passes so that if it loads quickly then it was never much visible at
all.

Even at its max it should never be heavier weight than the final content
so that it visually reverts into lesser. Another rule of thumb is that
it should be as close as possible to the final content in size but if
it's unknown it should always be smaller than the final content so that
the content grows into its slot rather than the slot contracting.

This makes the skeleton fade from invisible into the dimmest color just
as a subtle hint that something is still loading.

I also added a missing skeleton since the stack traces in rendered by
can now suspend while source mapping.

The other tweak I did is use disabled buttons in all the cases where we
load the ability to enable a button. This is more subtle and if you
hover over you can see why it's still disabled. Rather than flashing the
button each time you change element.
This commit is contained in:
Sebastian Markbåge
2025-08-12 23:10:31 -04:00
committed by GitHub
parent 4c18aab076
commit 651df45dd9
7 changed files with 83 additions and 45 deletions
@@ -24,7 +24,6 @@ import FetchFileWithCachingContext from './FetchFileWithCachingContext';
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
import OpenInEditorButton from './OpenInEditorButton';
import InspectedElementViewSourceButton from './InspectedElementViewSourceButton';
import Skeleton from './Skeleton';
import useEditorURL from '../useEditorURL';
import styles from './InspectedElement.css';
@@ -203,7 +202,9 @@ export default function InspectedElementWrapper(_: Props): React.Node {
}
return (
<div className={styles.InspectedElement}>
<div
className={styles.InspectedElement}
key={inspectedElementID /* Force reset when selected Element changes */}>
<div className={styles.TitleRow} data-testname="InspectedElement-Title">
{strictModeBadge}
@@ -232,13 +233,11 @@ export default function InspectedElementWrapper(_: Props): React.Node {
!!editorURL &&
source != null &&
symbolicatedSourcePromise != null && (
<React.Suspense fallback={<Skeleton height={16} width={24} />}>
<OpenInEditorButton
editorURL={editorURL}
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
</React.Suspense>
<OpenInEditorButton
editorURL={editorURL}
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
)}
{canToggleError && (
@@ -294,9 +293,6 @@ export default function InspectedElementWrapper(_: Props): React.Node {
{inspectedElement !== null && symbolicatedSourcePromise != null && (
<InspectedElementView
key={
inspectedElementID /* Force reset when selected Element changes */
}
element={element}
hookNames={hookNames}
inspectedElement={inspectedElement}
@@ -36,7 +36,12 @@ function InspectedElementSourcePanel({
<div className={styles.SourceHeaderRow}>
<div className={styles.SourceHeader}>source</div>
<React.Suspense fallback={<Skeleton height={16} width={16} />}>
<React.Suspense
fallback={
<Button disabled={true} title="Loading source maps...">
<ButtonIcon type="copy" />
</Button>
}>
<CopySourceButton
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
@@ -24,3 +24,7 @@
overflow: hidden;
text-overflow: ellipsis;
}
.RenderedBySkeleton {
padding-left: 1.25rem;
}
@@ -24,6 +24,7 @@ import {enableStyleXFeatures} from 'react-devtools-feature-flags';
import InspectedElementSourcePanel from './InspectedElementSourcePanel';
import StackTraceView from './StackTraceView';
import OwnerView from './OwnerView';
import Skeleton from './Skeleton';
import styles from './InspectedElementView.css';
@@ -170,34 +171,40 @@ export default function InspectedElementView({
className={styles.InspectedElementSection}
data-testname="InspectedElementView-Owners">
<div className={styles.OwnersHeader}>rendered by</div>
<React.Suspense
fallback={
<div className={styles.RenderedBySkeleton}>
<Skeleton height={16} width="40%" />
</div>
}>
{showStack ? <StackTraceView stack={stack} /> : null}
{showOwnersList &&
owners?.map(owner => (
<Fragment key={owner.id}>
<OwnerView
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
environmentName={
inspectedElement.env === owner.env ? null : owner.env
}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
{owner.stack != null && owner.stack.length > 0 ? (
<StackTraceView stack={owner.stack} />
) : null}
</Fragment>
))}
{showStack ? <StackTraceView stack={stack} /> : null}
{showOwnersList &&
owners?.map(owner => (
<Fragment key={owner.id}>
<OwnerView
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
environmentName={
inspectedElement.env === owner.env ? null : owner.env
}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
{owner.stack != null && owner.stack.length > 0 ? (
<StackTraceView stack={owner.stack} />
) : null}
</Fragment>
))}
{rootType !== null && (
<div className={styles.OwnersMetaField}>{rootType}</div>
)}
{rendererLabel !== null && (
<div className={styles.OwnersMetaField}>{rendererLabel}</div>
)}
{rootType !== null && (
<div className={styles.OwnersMetaField}>{rootType}</div>
)}
{rendererLabel !== null && (
<div className={styles.OwnersMetaField}>{rendererLabel}</div>
)}
</React.Suspense>
</div>
)}
@@ -11,7 +11,6 @@ import * as React from 'react';
import ButtonIcon from '../ButtonIcon';
import Button from '../Button';
import Skeleton from './Skeleton';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
@@ -27,7 +26,12 @@ function InspectedElementViewSourceButton({
symbolicatedSourcePromise,
}: Props): React.Node {
return (
<React.Suspense fallback={<Skeleton height={16} width={24} />}>
<React.Suspense
fallback={
<Button disabled={true} title="Loading source maps...">
<ButtonIcon type="view-source" />
</Button>
}>
<ActualSourceButton
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
@@ -5,9 +5,9 @@
@keyframes pulse {
0%, 100% {
background-color: var(--color-dim);
background-color: none;
}
50% {
background-color: var(--color-dimmest)
background-color: var(--color-dimmest);
}
}
@@ -24,7 +24,11 @@ type Props = {
className?: string,
};
function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
function ActualOpenInEditorButton({
editorURL,
source,
className,
}: Props): React.Node {
let disable;
if (source == null) {
disable = true;
@@ -68,4 +72,22 @@ function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
);
}
function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
return (
<React.Suspense
fallback={
<Button disabled={true} className={className}>
<ButtonIcon type="editor" />
<ButtonLabel>Loading source maps...</ButtonLabel>
</Button>
}>
<ActualOpenInEditorButton
editorURL={editorURL}
source={source}
className={className}
/>
</React.Suspense>
);
}
export default OpenInEditorButton;