FIX[app.ironcalc.com]: Clean up code for the title

This commit is contained in:
Nicolás Hatcher
2025-03-05 22:57:45 +01:00
committed by Nicolás Hatcher Andrés
parent cde6f0e49f
commit e07fdd2091
2 changed files with 122 additions and 71 deletions

View File

@@ -1,7 +1,7 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import type { Model } from "@ironcalc/workbook"; import type { Model } from "@ironcalc/workbook";
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook"; import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
import { useRef, useState } from "react"; import { useLayoutEffect, useRef, useState } from "react";
import { FileMenu } from "./FileMenu"; import { FileMenu } from "./FileMenu";
import { ShareButton } from "./ShareButton"; import { ShareButton } from "./ShareButton";
import ShareWorkbookDialog from "./ShareWorkbookDialog"; import ShareWorkbookDialog from "./ShareWorkbookDialog";
@@ -9,6 +9,20 @@ import { WorkbookTitle } from "./WorkbookTitle";
import { downloadModel } from "./rpc"; import { downloadModel } from "./rpc";
import { updateNameSelectedWorkbook } from "./storage"; import { updateNameSelectedWorkbook } from "./storage";
// This hook is used to get the width of the window
function useWindowWidth() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
function updateWidth() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", updateWidth);
updateWidth();
return () => window.removeEventListener("resize", updateWidth);
}, []);
return width;
}
export function FileBar(properties: { export function FileBar(properties: {
model: Model; model: Model;
newModel: () => void; newModel: () => void;
@@ -16,8 +30,19 @@ export function FileBar(properties: {
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>; onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
onDelete: () => void; onDelete: () => void;
}) { }) {
const hiddenInputRef = useRef<HTMLInputElement>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const spacerRef = useRef<HTMLDivElement>(null);
const [maxTitleWidth, setMaxTitleWidth] = useState(0);
const width = useWindowWidth();
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to update the maxTitleWidth when the width changes
useLayoutEffect(() => {
const el = spacerRef.current;
if (el) {
const bb = el.getBoundingClientRect();
setMaxTitleWidth(bb.right - bb.left - 50);
}
}, [width]);
return ( return (
<FileBarWrapper> <FileBarWrapper>
@@ -41,19 +66,17 @@ export function FileBar(properties: {
> >
Help Help
</HelpButton> </HelpButton>
<WorkbookTitle <WorkbookTitleWrapper>
name={properties.model.getName()} <WorkbookTitle
onNameChange={(name) => { name={properties.model.getName()}
properties.model.setName(name); onNameChange={(name) => {
updateNameSelectedWorkbook(properties.model, name); properties.model.setName(name);
}} updateNameSelectedWorkbook(properties.model, name);
/> }}
<input maxWidth={maxTitleWidth}
ref={hiddenInputRef} />
type="text" </WorkbookTitleWrapper>
style={{ position: "absolute", left: -9999, top: -9999 }} <Spacer ref={spacerRef} />
/>
<div style={{ marginLeft: "auto" }} />
<DialogContainer> <DialogContainer>
<ShareButton onClick={() => setIsDialogOpen(true)} /> <ShareButton onClick={() => setIsDialogOpen(true)} />
{isDialogOpen && ( {isDialogOpen && (
@@ -68,6 +91,19 @@ export function FileBar(properties: {
); );
} }
// We want the workbook title to be exactly an the center of the page,
// so we need an absolute position
const WorkbookTitleWrapper = styled("div")`
position: absolute;
left: 50%;
transform: translateX(-50%);
`;
// The "Spacer" component occupies as much space as possible between the menu and the share button
const Spacer = styled("div")`
flex-grow: 1;
`;
const StyledDesktopLogo = styled(IronCalcLogo)` const StyledDesktopLogo = styled(IronCalcLogo)`
width: 120px; width: 120px;
margin-left: 12px; margin-left: 12px;
@@ -103,14 +139,15 @@ const Divider = styled("div")`
border-left: 1px solid #e0e0e0; border-left: 1px solid #e0e0e0;
`; `;
// The container must be relative positioned so we can position the title absolutely
const FileBarWrapper = styled("div")` const FileBarWrapper = styled("div")`
position: relative;
height: 60px; height: 60px;
width: 100%; width: 100%;
background: #fff; background: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
position: relative;
justify-content: space-between; justify-content: space-between;
`; `;

View File

@@ -1,89 +1,105 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { type ChangeEvent, useEffect, useRef, useState } from "react"; import {
type ChangeEvent,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
export function WorkbookTitle(props: { // This element has a in situ editable text
// We use a virtual element to compute the size of the input
export function WorkbookTitle(properties: {
name: string; name: string;
onNameChange: (name: string) => void; onNameChange: (name: string) => void;
maxWidth: number;
}) { }) {
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
const [value, setValue] = useState(props.name); const [name, setName] = useState(properties.name);
const mirrorDivRef = useRef<HTMLDivElement>(null); const mirrorDivRef = useRef<HTMLDivElement>(null);
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value); setName(event.target.value);
if (mirrorDivRef.current) { if (mirrorDivRef.current) {
setWidth(mirrorDivRef.current.scrollWidth); setWidth(mirrorDivRef.current.scrollWidth);
} }
}; };
useEffect(() => { useEffect(() => {
setName(properties.name);
}, [properties.name]);
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to change the width with every value change
useLayoutEffect(() => {
if (mirrorDivRef.current) { if (mirrorDivRef.current) {
setWidth(mirrorDivRef.current.scrollWidth); setWidth(mirrorDivRef.current.scrollWidth);
} }
}, []); }, [name]);
useEffect(() => {
setValue(props.name);
}, [props.name]);
return ( return (
<div <Container
style={{ style={{
position: "absolute", width: Math.min(width, properties.maxWidth),
left: "50%",
textAlign: "center",
transform: "translateX(-50%)",
// height: "60px",
// lineHeight: "60px",
padding: "8px",
fontSize: "14px",
fontWeight: "700",
fontFamily: "Inter",
width,
}} }}
> >
<TitleWrapper <TitleInput
value={value} value={name}
rows={1} onInput={handleChange}
onChange={handleChange}
onBlur={(event) => { onBlur={(event) => {
props.onNameChange(event.target.value); properties.onNameChange(event.target.value);
}} }}
style={{ width: width }} onKeyDown={(event) => {
switch (event.key) {
case "Enter": {
// If we hit "Enter" finish editing
event.currentTarget.blur();
break;
}
case "Escape": {
// revert changes
setName(properties.name);
break;
}
}
}}
style={{ width: Math.min(width, properties.maxWidth) }}
spellCheck="false" spellCheck="false"
> />
{value} <MirrorDiv ref={mirrorDivRef}>{name}</MirrorDiv>
</TitleWrapper> </Container>
<div
ref={mirrorDivRef}
style={{
position: "absolute",
top: "-9999px",
left: "-9999px",
whiteSpace: "pre-wrap",
textWrap: "nowrap",
visibility: "hidden",
fontFamily: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
padding: "inherit",
border: "inherit",
}}
>
{value}
</div>
</div>
); );
} }
const TitleWrapper = styled("textarea")` const Container = styled("div")`
text-align: center;
padding: 8px;
font-size: 14px;
font-weight: 700;
font-family: Inter;
`;
const MirrorDiv = styled("div")`
position: absolute;
top: -9999px;
left: -9999px;
white-space: pre-wrap;
text-wrap: nowrap;
visibility: hidden;
font-family: inherit;
font-size: inherit;
line-height: inherit;
padding: inherit;
border: inherit;
`;
const TitleInput = styled("input")`
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
height: 20px; height: 20px;
line-height: 20px; line-height: 20px;
border-radius: 4px; border-radius: 4px;
padding: inherit; padding: inherit;
overflow: hidden;
outline: none; outline: none;
resize: none; resize: none;
text-wrap: nowrap; text-wrap: nowrap;
@@ -97,8 +113,6 @@ const TitleWrapper = styled("textarea")`
font-weight: inherit; font-weight: inherit;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
max-width: 520px; overflow: ellipsis;
overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis;
`; `;