1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-02 03:25:53 +01:00

feat: Stats popup style tweaks (#8361)

This commit is contained in:
David Luzar 2024-08-11 19:33:44 +02:00 committed by GitHub
parent f7b3befd0a
commit 97981804d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 351 additions and 326 deletions

@ -9,6 +9,7 @@ import { t } from "../packages/excalidraw/i18n";
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard"; import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
import type { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types"; import type { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
import type { UIAppState } from "../packages/excalidraw/types"; import type { UIAppState } from "../packages/excalidraw/types";
import { Stats } from "../packages/excalidraw";
type StorageSizes = { scene: number; total: number }; type StorageSizes = { scene: number; total: number };
@ -51,39 +52,33 @@ const CustomStats = (props: Props) => {
} }
return ( return (
<> <Stats.StatsRows order={-1}>
<tr> <Stats.StatsRow heading>{t("stats.version")}</Stats.StatsRow>
<th colSpan={2}>{t("stats.storage")}</th> <Stats.StatsRow
</tr> style={{ textAlign: "center", cursor: "pointer" }}
<tr> onClick={async () => {
<td>{t("stats.scene")}</td> try {
<td>{nFormatter(storageSizes.scene, 1)}</td> await copyTextToSystemClipboard(getVersion());
</tr> props.setToast(t("toast.copyToClipboard"));
<tr> } catch {}
<td>{t("stats.total")}</td> }}
<td>{nFormatter(storageSizes.total, 1)}</td> title={t("stats.versionCopy")}
</tr> >
<tr> {timestamp}
<th colSpan={2}>{t("stats.version")}</th> <br />
</tr> {hash}
<tr> </Stats.StatsRow>
<td
colSpan={2} <Stats.StatsRow heading>{t("stats.storage")}</Stats.StatsRow>
style={{ textAlign: "center", cursor: "pointer" }} <Stats.StatsRow columns={2}>
onClick={async () => { <div>{t("stats.scene")}</div>
try { <div>{nFormatter(storageSizes.scene, 1)}</div>
await copyTextToSystemClipboard(getVersion()); </Stats.StatsRow>
props.setToast(t("toast.copyToClipboard")); <Stats.StatsRow columns={2}>
} catch {} <div>{t("stats.total")}</div>
}} <div>{nFormatter(storageSizes.total, 1)}</div>
title={t("stats.versionCopy")} </Stats.StatsRow>
> </Stats.StatsRows>
{timestamp}
<br />
{hash}
</td>
</tr>
</>
); );
}; };

@ -39,6 +39,8 @@ Please add the latest change on the top under the correct section.
### Breaking Changes ### Breaking Changes
- Stats container CSS changed, so if you're using `renderCustomStats`, you may need to adjust your styles to retain the same layout.
- `updateScene` API has changed due to the added `Store` component as part of the multiplayer undo / redo initiative. Specifically, `sceneData` property `commitToHistory: boolean` was replaced with `storeAction: StoreActionType`. Make sure to update all instances of `updateScene` according to the _before / after_ table below. [#7898](https://github.com/excalidraw/excalidraw/pull/7898) - `updateScene` API has changed due to the added `Store` component as part of the multiplayer undo / redo initiative. Specifically, `sceneData` property `commitToHistory: boolean` was replaced with `storeAction: StoreActionType`. Make sure to update all instances of `updateScene` according to the _before / after_ table below. [#7898](https://github.com/excalidraw/excalidraw/pull/7898)
| | Before `commitToHistory` | After `storeAction` | Notes | | | Before `commitToHistory` | After `storeAction` | Notes |

@ -27,99 +27,6 @@
& > * { & > * {
pointer-events: var(--ui-pointerEvents); pointer-events: var(--ui-pointerEvents);
} }
& > .Stats {
width: 204px;
position: absolute;
top: 60px;
font-size: 12px;
z-index: var(--zIndex-layerUI);
pointer-events: var(--ui-pointerEvents);
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
h2 {
margin: 0;
}
}
.sectionContent {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.elementType {
font-size: 12px;
font-weight: 700;
margin-top: 8px;
}
.elementsCount {
width: 100%;
font-size: 12px;
display: flex;
justify-content: space-between;
margin-top: 8px;
}
.statsItem {
margin-top: 8px;
width: 100%;
margin-bottom: 4px;
display: grid;
gap: 4px;
.label {
margin-right: 4px;
}
}
h3 {
white-space: nowrap;
margin: 0;
}
.close {
height: 16px;
width: 16px;
cursor: pointer;
svg {
width: 100%;
height: 100%;
}
}
table {
width: 100%;
th {
border-bottom: 1px solid var(--input-border-color);
padding: 4px;
}
tr {
td:nth-child(2) {
min-width: 24px;
text-align: right;
}
}
}
.divider {
width: 100%;
height: 1px;
background-color: var(--default-border-color);
}
:root[dir="rtl"] & {
left: 12px;
right: initial;
}
}
} }
&__footer { &__footer {

@ -31,7 +31,11 @@ const Collapsible = ({
{label} {label}
<InlineIcon icon={open ? collapseUpIcon : collapseDownIcon} /> <InlineIcon icon={open ? collapseUpIcon : collapseDownIcon} />
</div> </div>
{open && <>{children}</>} {open && (
<div style={{ display: "flex", flexDirection: "column" }}>
{children}
</div>
)}
</> </>
); );
}; };

@ -5,7 +5,7 @@
&:focus-within { &:focus-within {
box-shadow: 0 0 0 1px var(--color-primary-darkest); box-shadow: 0 0 0 1px var(--color-primary-darkest);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-md);
} }
} }
@ -24,11 +24,11 @@
color: var(--popup-text-color); color: var(--popup-text-color);
:root[dir="ltr"] & { :root[dir="ltr"] & {
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg); border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
} }
:root[dir="rtl"] & { :root[dir="rtl"] & {
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0; border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
border-right: 1px solid var(--default-border-color); border-right: 1px solid var(--default-border-color);
border-left: 0; border-left: 0;
} }
@ -55,11 +55,11 @@
letter-spacing: 0.4px; letter-spacing: 0.4px;
:root[dir="ltr"] & { :root[dir="ltr"] & {
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0; border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
} }
:root[dir="rtl"] & { :root[dir="rtl"] & {
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg); border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
border-left: 1px solid var(--default-border-color); border-left: 1px solid var(--default-border-color);
border-right: 0; border-right: 0;
} }

@ -0,0 +1,72 @@
.exc-stats {
width: 204px;
position: absolute;
top: 60px;
font-size: 12px;
z-index: var(--zIndex-layerUI);
pointer-events: var(--ui-pointerEvents);
:root[dir="rtl"] & {
left: 12px;
right: initial;
}
h2 {
font-size: 1.5em;
margin-block-start: 0.83em;
margin-block-end: 0.83em;
font-weight: bold;
}
h3 {
white-space: nowrap;
font-size: 1.17em;
margin: 0;
font-weight: bold;
}
&__rows {
display: flex;
flex-direction: column;
gap: 0.3125rem;
}
&__row {
display: flex;
justify-content: space-between;
align-items: center;
display: grid;
gap: 4px;
div + div {
text-align: right;
}
}
&__row--heading {
text-align: center;
font-weight: bold;
margin: 0.25rem 0;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
h2 {
margin: 0;
}
}
.close {
height: 16px;
width: 16px;
cursor: pointer;
svg {
width: 100%;
height: 100%;
}
}
}

@ -8,7 +8,6 @@ import { Island } from "../Island";
import { throttle } from "lodash"; import { throttle } from "lodash";
import Dimension from "./Dimension"; import Dimension from "./Dimension";
import Angle from "./Angle"; import Angle from "./Angle";
import FontSize from "./FontSize"; import FontSize from "./FontSize";
import MultiDimension from "./MultiDimension"; import MultiDimension from "./MultiDimension";
import { elementsAreInSameGroup } from "../../groups"; import { elementsAreInSameGroup } from "../../groups";
@ -22,6 +21,9 @@ import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
import { getAtomicUnits } from "./utils"; import { getAtomicUnits } from "./utils";
import { STATS_PANELS } from "../../constants"; import { STATS_PANELS } from "../../constants";
import { isElbowArrow } from "../../element/typeChecks"; import { isElbowArrow } from "../../element/typeChecks";
import clsx from "clsx";
import "./Stats.scss";
interface StatsProps { interface StatsProps {
scene: Scene; scene: Scene;
@ -49,6 +51,50 @@ export const Stats = (props: StatsProps) => {
); );
}; };
const StatsRow = ({
children,
columns = 1,
heading,
style,
...rest
}: {
children: React.ReactNode;
columns?: number;
heading?: boolean;
style?: React.CSSProperties;
} & React.HTMLAttributes<HTMLDivElement>) => (
<div
className={clsx("exc-stats__row", { "exc-stats__row--heading": heading })}
style={{
gridTemplateColumns: `repeat(${columns}, 1fr)`,
...style,
}}
{...rest}
>
{children}
</div>
);
StatsRow.displayName = "StatsRow";
const StatsRows = ({
children,
order,
style,
...rest
}: {
children: React.ReactNode;
order?: number;
style?: React.CSSProperties;
} & React.HTMLAttributes<HTMLDivElement>) => (
<div className="exc-stats__rows" style={{ order, ...style }} {...rest}>
{children}
</div>
);
StatsRows.displayName = "StatsRows";
Stats.StatsRow = StatsRow;
Stats.StatsRows = StatsRows;
export const StatsInner = memo( export const StatsInner = memo(
({ ({
scene, scene,
@ -106,7 +152,7 @@ export const StatsInner = memo(
}, [selectedElements, appState]); }, [selectedElements, appState]);
return ( return (
<div className="Stats"> <div className="exc-stats">
<Island padding={3}> <Island padding={3}>
<div className="title"> <div className="title">
<h2>{t("stats.title")}</h2> <h2>{t("stats.title")}</h2>
@ -121,7 +167,6 @@ export const StatsInner = memo(
openTrigger={() => openTrigger={() =>
setAppState((state) => { setAppState((state) => {
return { return {
...state,
stats: { stats: {
open: true, open: true,
panels: state.stats.panels ^ STATS_PANELS.generalStats, panels: state.stats.panels ^ STATS_PANELS.generalStats,
@ -130,26 +175,23 @@ export const StatsInner = memo(
}) })
} }
> >
<table> <StatsRows>
<tbody> <StatsRow heading>{t("stats.scene")}</StatsRow>
<tr> <StatsRow columns={2}>
<th colSpan={2}>{t("stats.scene")}</th> <div>{t("stats.shapes")}</div>
</tr> <div>{elements.length}</div>
<tr> </StatsRow>
<td>{t("stats.elements")}</td> <StatsRow columns={2}>
<td>{elements.length}</td> <div>{t("stats.width")}</div>
</tr> <div>{sceneDimension.width}</div>
<tr> </StatsRow>
<td>{t("stats.width")}</td> <StatsRow columns={2}>
<td>{sceneDimension.width}</td> <div>{t("stats.height")}</div>
</tr> <div>{sceneDimension.height}</div>
<tr> </StatsRow>
<td>{t("stats.height")}</td> </StatsRows>
<td>{sceneDimension.height}</td>
</tr> {renderCustomStats?.(elements, appState)}
{renderCustomStats?.(elements, appState)}
</tbody>
</table>
</Collapsible> </Collapsible>
{selectedElements.length > 0 && ( {selectedElements.length > 0 && (
@ -167,7 +209,6 @@ export const StatsInner = memo(
openTrigger={() => openTrigger={() =>
setAppState((state) => { setAppState((state) => {
return { return {
...state,
stats: { stats: {
open: true, open: true,
panels: panels:
@ -177,117 +218,139 @@ export const StatsInner = memo(
}) })
} }
> >
{singleElement && ( <StatsRows>
<div className="sectionContent"> {singleElement && (
<div className="elementType"> <>
{t(`element.${singleElement.type}`)} <StatsRow heading data-testid="stats-element-type">
</div> {t(`element.${singleElement.type}`)}
</StatsRow>
<div className="statsItem"> <StatsRow>
<Position <Position
element={singleElement} element={singleElement}
property="x" property="x"
elementsMap={elementsMap} elementsMap={elementsMap}
scene={scene} scene={scene}
appState={appState} appState={appState}
/> />
<Position </StatsRow>
element={singleElement} <StatsRow>
property="y" <Position
elementsMap={elementsMap} element={singleElement}
scene={scene} property="y"
appState={appState} elementsMap={elementsMap}
/> scene={scene}
<Dimension appState={appState}
property="width" />
element={singleElement} </StatsRow>
scene={scene} <StatsRow>
appState={appState} <Dimension
/> property="width"
<Dimension
property="height"
element={singleElement}
scene={scene}
appState={appState}
/>
{!isElbowArrow(singleElement) && (
<Angle
property="angle"
element={singleElement} element={singleElement}
scene={scene} scene={scene}
appState={appState} appState={appState}
/> />
</StatsRow>
<StatsRow>
<Dimension
property="height"
element={singleElement}
scene={scene}
appState={appState}
/>
</StatsRow>
{!isElbowArrow(singleElement) && (
<StatsRow>
<Angle
property="angle"
element={singleElement}
scene={scene}
appState={appState}
/>
</StatsRow>
)} )}
<FontSize <StatsRow>
property="fontSize" <FontSize
element={singleElement} property="fontSize"
scene={scene} element={singleElement}
appState={appState} scene={scene}
/> appState={appState}
</div> />
</div> </StatsRow>
)} </>
)}
{multipleElements && ( {multipleElements && (
<div className="sectionContent"> <>
{elementsAreInSameGroup(multipleElements) && ( {elementsAreInSameGroup(multipleElements) && (
<div className="elementType">{t("element.group")}</div> <StatsRow heading>{t("element.group")}</StatsRow>
)} )}
<div className="elementsCount"> <StatsRow columns={2} style={{ margin: "0.3125rem 0" }}>
<div>{t("stats.elements")}</div> <div>{t("stats.shapes")}</div>
<div>{selectedElements.length}</div> <div>{selectedElements.length}</div>
</div> </StatsRow>
<div className="statsItem"> <StatsRow>
<MultiPosition <MultiPosition
property="x" property="x"
elements={multipleElements} elements={multipleElements}
elementsMap={elementsMap} elementsMap={elementsMap}
atomicUnits={atomicUnits} atomicUnits={atomicUnits}
scene={scene} scene={scene}
appState={appState} appState={appState}
/> />
<MultiPosition </StatsRow>
property="y" <StatsRow>
elements={multipleElements} <MultiPosition
elementsMap={elementsMap} property="y"
atomicUnits={atomicUnits} elements={multipleElements}
scene={scene} elementsMap={elementsMap}
appState={appState} atomicUnits={atomicUnits}
/> scene={scene}
<MultiDimension appState={appState}
property="width" />
elements={multipleElements} </StatsRow>
elementsMap={elementsMap} <StatsRow>
atomicUnits={atomicUnits} <MultiDimension
scene={scene} property="width"
appState={appState} elements={multipleElements}
/> elementsMap={elementsMap}
<MultiDimension atomicUnits={atomicUnits}
property="height" scene={scene}
elements={multipleElements} appState={appState}
elementsMap={elementsMap} />
atomicUnits={atomicUnits} </StatsRow>
scene={scene} <StatsRow>
appState={appState} <MultiDimension
/> property="height"
<MultiAngle elements={multipleElements}
property="angle" elementsMap={elementsMap}
elements={multipleElements} atomicUnits={atomicUnits}
scene={scene} scene={scene}
appState={appState} appState={appState}
/> />
<MultiFontSize </StatsRow>
property="fontSize" <StatsRow>
elements={multipleElements} <MultiAngle
scene={scene} property="angle"
appState={appState} elements={multipleElements}
elementsMap={elementsMap} scene={scene}
/> appState={appState}
</div> />
</div> </StatsRow>
)} <StatsRow>
<MultiFontSize
property="fontSize"
elements={multipleElements}
scene={scene}
appState={appState}
elementsMap={elementsMap}
/>
</StatsRow>
</>
)}
</StatsRows>
</Collapsible> </Collapsible>
</div> </div>
)} )}

@ -32,21 +32,6 @@ const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
let stats: HTMLElement | null = null; let stats: HTMLElement | null = null;
let elementStats: HTMLElement | null | undefined = null; let elementStats: HTMLElement | null | undefined = null;
const getStatsProperty = (label: string) => {
const elementStats = UI.queryStats()?.querySelector("#elementStats");
if (elementStats) {
const properties = elementStats?.querySelector(".statsItem");
return (
properties?.querySelector?.(
`.drag-input-container[data-testid="${label}"]`,
) || null
);
}
return null;
};
const testInputProperty = ( const testInputProperty = (
element: ExcalidrawElement, element: ExcalidrawElement,
property: "x" | "y" | "width" | "height" | "angle" | "fontSize", property: "x" | "y" | "width" | "height" | "angle" | "fontSize",
@ -54,7 +39,7 @@ const testInputProperty = (
initialValue: number, initialValue: number,
nextValue: number, nextValue: number,
) => { ) => {
const input = getStatsProperty(label)?.querySelector( const input = UI.queryStatsProperty(label)?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(input).toBeDefined(); expect(input).toBeDefined();
@ -136,7 +121,7 @@ describe("binding with linear elements", () => {
it("should remain bound to linear element on small position change", async () => { it("should remain bound to linear element on small position change", async () => {
const linear = h.elements[1] as ExcalidrawLinearElement; const linear = h.elements[1] as ExcalidrawLinearElement;
const inputX = getStatsProperty("X")?.querySelector( const inputX = UI.queryStatsProperty("X")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
@ -148,7 +133,7 @@ describe("binding with linear elements", () => {
it("should remain bound to linear element on small angle change", async () => { it("should remain bound to linear element on small angle change", async () => {
const linear = h.elements[1] as ExcalidrawLinearElement; const linear = h.elements[1] as ExcalidrawLinearElement;
const inputAngle = getStatsProperty("A")?.querySelector( const inputAngle = UI.queryStatsProperty("A")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
@ -159,7 +144,7 @@ describe("binding with linear elements", () => {
it("should unbind linear element on large position change", async () => { it("should unbind linear element on large position change", async () => {
const linear = h.elements[1] as ExcalidrawLinearElement; const linear = h.elements[1] as ExcalidrawLinearElement;
const inputX = getStatsProperty("X")?.querySelector( const inputX = UI.queryStatsProperty("X")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
@ -171,7 +156,7 @@ describe("binding with linear elements", () => {
it("should remain bound to linear element on small angle change", async () => { it("should remain bound to linear element on small angle change", async () => {
const linear = h.elements[1] as ExcalidrawLinearElement; const linear = h.elements[1] as ExcalidrawLinearElement;
const inputAngle = getStatsProperty("A")?.querySelector( const inputAngle = UI.queryStatsProperty("A")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
@ -225,18 +210,14 @@ describe("stats for a generic element", () => {
expect(title?.lastChild?.nodeValue)?.toBe(t("stats.elementProperties")); expect(title?.lastChild?.nodeValue)?.toBe(t("stats.elementProperties"));
// element type // element type
const elementType = elementStats?.querySelector(".elementType"); const elementType = queryByTestId(elementStats!, "stats-element-type");
expect(elementType).toBeDefined(); expect(elementType).toBeDefined();
expect(elementType?.lastChild?.nodeValue).toBe(t("element.rectangle")); expect(elementType?.lastChild?.nodeValue).toBe(t("element.rectangle"));
// properties // properties
const properties = elementStats?.querySelector(".statsItem");
expect(properties?.childNodes).toBeDefined();
["X", "Y", "W", "H", "A"].forEach((label) => () => { ["X", "Y", "W", "H", "A"].forEach((label) => () => {
expect( expect(
properties?.querySelector?.( stats!.querySelector?.(`.drag-input-container[data-testid="${label}"]`),
`.drag-input-container[data-testid="${label}"]`,
),
).toBeDefined(); ).toBeDefined();
}); });
}); });
@ -257,7 +238,7 @@ describe("stats for a generic element", () => {
const rectangle = h.elements[0]; const rectangle = h.elements[0];
const rectangleId = rectangle.id; const rectangleId = rectangle.id;
const input = getStatsProperty("W")?.querySelector( const input = UI.queryStatsProperty("W")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(input).toBeDefined(); expect(input).toBeDefined();
@ -287,11 +268,11 @@ describe("stats for a generic element", () => {
rectangle.angle, rectangle.angle,
); );
const xInput = getStatsProperty("X")?.querySelector( const xInput = UI.queryStatsProperty("X")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
const yInput = getStatsProperty("Y")?.querySelector( const yInput = UI.queryStatsProperty("Y")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
@ -417,7 +398,7 @@ describe("stats for a non-generic element", () => {
elementStats = stats?.querySelector("#elementStats"); elementStats = stats?.querySelector("#elementStats");
// can change font size // can change font size
const input = getStatsProperty("F")?.querySelector( const input = UI.queryStatsProperty("F")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(input).toBeDefined(); expect(input).toBeDefined();
@ -426,9 +407,9 @@ describe("stats for a non-generic element", () => {
expect(text.fontSize).toBe(36); expect(text.fontSize).toBe(36);
// cannot change width or height // cannot change width or height
const width = getStatsProperty("W")?.querySelector(".drag-input"); const width = UI.queryStatsProperty("W")?.querySelector(".drag-input");
expect(width).toBeUndefined(); expect(width).toBeUndefined();
const height = getStatsProperty("H")?.querySelector(".drag-input"); const height = UI.queryStatsProperty("H")?.querySelector(".drag-input");
expect(height).toBeUndefined(); expect(height).toBeUndefined();
// min font size is 4 // min font size is 4
@ -456,7 +437,7 @@ describe("stats for a non-generic element", () => {
expect(elementStats).toBeDefined(); expect(elementStats).toBeDefined();
// cannot change angle // cannot change angle
const angle = getStatsProperty("A")?.querySelector(".drag-input"); const angle = UI.queryStatsProperty("A")?.querySelector(".drag-input");
expect(angle).toBeUndefined(); expect(angle).toBeUndefined();
// can change width or height // can change width or height
@ -506,7 +487,7 @@ describe("stats for a non-generic element", () => {
API.setElements([container, text]); API.setElements([container, text]);
API.setSelectedElements([container]); API.setSelectedElements([container]);
const fontSize = getStatsProperty("F")?.querySelector( const fontSize = UI.queryStatsProperty("F")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(fontSize).toBeDefined(); expect(fontSize).toBeDefined();
@ -570,15 +551,15 @@ describe("stats for multiple elements", () => {
elementStats = stats?.querySelector("#elementStats"); elementStats = stats?.querySelector("#elementStats");
const width = getStatsProperty("W")?.querySelector( const width = UI.queryStatsProperty("W")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(width?.value).toBe("Mixed"); expect(width?.value).toBe("Mixed");
const height = getStatsProperty("H")?.querySelector( const height = UI.queryStatsProperty("H")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(height?.value).toBe("Mixed"); expect(height?.value).toBe("Mixed");
const angle = getStatsProperty("A")?.querySelector( const angle = UI.queryStatsProperty("A")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(angle.value).toBe("0"); expect(angle.value).toBe("0");
@ -629,25 +610,25 @@ describe("stats for multiple elements", () => {
elementStats = stats?.querySelector("#elementStats"); elementStats = stats?.querySelector("#elementStats");
const width = getStatsProperty("W")?.querySelector( const width = UI.queryStatsProperty("W")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(width).toBeDefined(); expect(width).toBeDefined();
expect(width.value).toBe("Mixed"); expect(width.value).toBe("Mixed");
const height = getStatsProperty("H")?.querySelector( const height = UI.queryStatsProperty("H")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(height).toBeDefined(); expect(height).toBeDefined();
expect(height.value).toBe("Mixed"); expect(height.value).toBe("Mixed");
const angle = getStatsProperty("A")?.querySelector( const angle = UI.queryStatsProperty("A")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(angle).toBeDefined(); expect(angle).toBeDefined();
expect(angle.value).toBe("0"); expect(angle.value).toBe("0");
const fontSize = getStatsProperty("F")?.querySelector( const fontSize = UI.queryStatsProperty("F")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(fontSize).toBeDefined(); expect(fontSize).toBeDefined();
@ -692,7 +673,7 @@ describe("stats for multiple elements", () => {
elementStats = stats?.querySelector("#elementStats"); elementStats = stats?.querySelector("#elementStats");
const x = getStatsProperty("X")?.querySelector( const x = UI.queryStatsProperty("X")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
@ -705,7 +686,7 @@ describe("stats for multiple elements", () => {
expect(h.elements[1].x).toBe(400); expect(h.elements[1].x).toBe(400);
expect(x.value).toBe("300"); expect(x.value).toBe("300");
const y = getStatsProperty("Y")?.querySelector( const y = UI.queryStatsProperty("Y")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
@ -718,13 +699,13 @@ describe("stats for multiple elements", () => {
expect(h.elements[1].y).toBe(300); expect(h.elements[1].y).toBe(300);
expect(y.value).toBe("200"); expect(y.value).toBe("200");
const width = getStatsProperty("W")?.querySelector( const width = UI.queryStatsProperty("W")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(width).toBeDefined(); expect(width).toBeDefined();
expect(Number(width.value)).toBe(200); expect(Number(width.value)).toBe(200);
const height = getStatsProperty("H")?.querySelector( const height = UI.queryStatsProperty("H")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
expect(height).toBeDefined(); expect(height).toBeDefined();

@ -22,21 +22,6 @@ const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
const getStatsProperty = (label: string) => {
const elementStats = UI.queryStats()?.querySelector("#elementStats");
if (elementStats) {
const properties = elementStats?.querySelector(".statsItem");
return (
properties?.querySelector?.(
`.drag-input-container[data-testid="${label}"]`,
) || null
);
}
return null;
};
describe("elbow arrow routing", () => { describe("elbow arrow routing", () => {
it("can properly generate orthogonal arrow points", () => { it("can properly generate orthogonal arrow points", () => {
const scene = new Scene(); const scene = new Scene();
@ -193,7 +178,7 @@ describe("elbow arrow ui", () => {
mouse.click(51, 51); mouse.click(51, 51);
const inputAngle = getStatsProperty("A")?.querySelector( const inputAngle = UI.queryStatsProperty("A")?.querySelector(
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
UI.updateInput(inputAngle, String("40")); UI.updateInput(inputAngle, String("40"));

@ -271,6 +271,7 @@ export { MainMenu };
export { useDevice } from "./components/App"; export { useDevice } from "./components/App";
export { WelcomeScreen }; export { WelcomeScreen };
export { LiveCollaborationTrigger }; export { LiveCollaborationTrigger };
export { Stats } from "./components/Stats";
export { DefaultSidebar } from "./components/DefaultSidebar"; export { DefaultSidebar } from "./components/DefaultSidebar";
export { TTDDialog } from "./components/TTDDialog/TTDDialog"; export { TTDDialog } from "./components/TTDDialog/TTDDialog";

@ -462,16 +462,15 @@
}, },
"stats": { "stats": {
"angle": "Angle", "angle": "Angle",
"element": "Element", "shapes": "Shapes",
"elements": "Elements",
"height": "Height", "height": "Height",
"scene": "Scene", "scene": "Scene",
"selected": "Selected", "selected": "Selected",
"storage": "Storage", "storage": "Storage",
"fullTitle": "Stats & Element properties", "fullTitle": "Canvas & Shape properties",
"title": "Stats", "title": "Properties",
"generalStats": "General stats", "generalStats": "General",
"elementProperties": "Element properties", "elementProperties": "Shape properties",
"total": "Total", "total": "Total",
"version": "Version", "version": "Version",
"versionCopy": "Click to copy", "versionCopy": "Click to copy",

@ -579,7 +579,23 @@ export class UI {
static queryStats = () => { static queryStats = () => {
return GlobalTestState.renderResult.container.querySelector( return GlobalTestState.renderResult.container.querySelector(
".Stats", ".exc-stats",
) as HTMLElement | null; ) as HTMLElement | null;
}; };
static queryStatsProperty = (label: string) => {
const elementStats = UI.queryStats()?.querySelector("#elementStats");
expect(elementStats).not.toBeNull();
if (elementStats) {
return (
elementStats?.querySelector(
`.exc-stats__row .drag-input-container[data-testid="${label}"]`,
) || null
);
}
return null;
};
} }