diff --git a/webapp/app.ironcalc.com/frontend/src/components/FileBar.tsx b/webapp/app.ironcalc.com/frontend/src/components/FileBar.tsx index 6a296c3..924ca18 100644 --- a/webapp/app.ironcalc.com/frontend/src/components/FileBar.tsx +++ b/webapp/app.ironcalc.com/frontend/src/components/FileBar.tsx @@ -1,7 +1,7 @@ import styled from "@emotion/styled"; import type { Model } 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 { ShareButton } from "./ShareButton"; import ShareWorkbookDialog from "./ShareWorkbookDialog"; @@ -9,6 +9,20 @@ import { WorkbookTitle } from "./WorkbookTitle"; import { downloadModel } from "./rpc"; 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: { model: Model; newModel: () => void; @@ -16,8 +30,19 @@ export function FileBar(properties: { onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise; onDelete: () => void; }) { - const hiddenInputRef = useRef(null); const [isDialogOpen, setIsDialogOpen] = useState(false); + const spacerRef = useRef(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 ( @@ -41,19 +66,17 @@ export function FileBar(properties: { > Help - { - properties.model.setName(name); - updateNameSelectedWorkbook(properties.model, name); - }} - /> - -
+ + { + properties.model.setName(name); + updateNameSelectedWorkbook(properties.model, name); + }} + maxWidth={maxTitleWidth} + /> + + setIsDialogOpen(true)} /> {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)` width: 120px; margin-left: 12px; @@ -103,14 +139,15 @@ const Divider = styled("div")` border-left: 1px solid #e0e0e0; `; +// The container must be relative positioned so we can position the title absolutely const FileBarWrapper = styled("div")` + position: relative; height: 60px; width: 100%; background: #fff; display: flex; align-items: center; border-bottom: 1px solid #e0e0e0; - position: relative; justify-content: space-between; `; diff --git a/webapp/app.ironcalc.com/frontend/src/components/WorkbookTitle.tsx b/webapp/app.ironcalc.com/frontend/src/components/WorkbookTitle.tsx index 1836c9d..1a151d4 100644 --- a/webapp/app.ironcalc.com/frontend/src/components/WorkbookTitle.tsx +++ b/webapp/app.ironcalc.com/frontend/src/components/WorkbookTitle.tsx @@ -1,89 +1,105 @@ 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; onNameChange: (name: string) => void; + maxWidth: number; }) { const [width, setWidth] = useState(0); - const [value, setValue] = useState(props.name); + const [name, setName] = useState(properties.name); const mirrorDivRef = useRef(null); - const handleChange = (event: ChangeEvent) => { - setValue(event.target.value); + const handleChange = (event: ChangeEvent) => { + setName(event.target.value); if (mirrorDivRef.current) { setWidth(mirrorDivRef.current.scrollWidth); } }; 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) { setWidth(mirrorDivRef.current.scrollWidth); } - }, []); - - useEffect(() => { - setValue(props.name); - }, [props.name]); + }, [name]); return ( -
- { - 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" - > - {value} - -
- {value} -
-
+ /> + {name} + ); } -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; text-align: center; height: 20px; line-height: 20px; border-radius: 4px; padding: inherit; - overflow: hidden; outline: none; resize: none; text-wrap: nowrap; @@ -97,8 +113,6 @@ const TitleWrapper = styled("textarea")` font-weight: inherit; font-family: inherit; font-size: inherit; - max-width: 520px; - overflow: hidden; + overflow: ellipsis; white-space: nowrap; - text-overflow: ellipsis; `;