update: allow to edit sheet anems directly from tab buttons

This commit is contained in:
Daniel
2025-11-19 01:27:56 +01:00
committed by Nicolás Hatcher Andrés
parent 6b60b339d6
commit 19c115b32f
3 changed files with 153 additions and 188 deletions

View File

@@ -1,158 +0,0 @@
import { Dialog, styled, TextField } from "@mui/material";
import { Check, X } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
interface SheetRenameDialogProps {
open: boolean;
onClose: () => void;
onNameChanged: (name: string) => void;
defaultName: string;
}
const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
const { t } = useTranslation();
const [name, setName] = useState(properties.defaultName);
const handleClose = () => {
properties.onClose();
};
return (
<Dialog open={properties.open} onClose={properties.onClose}>
<StyledDialogTitle>
{t("sheet_rename.title")}
<Cross
onClick={handleClose}
title={t("sheet_rename.close")}
tabIndex={0}
onKeyDown={(event) => event.key === "Enter" && properties.onClose()}
>
<X />
</Cross>
</StyledDialogTitle>
<StyledDialogContent>
<StyledTextField
autoFocus
defaultValue={properties.defaultName}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === "Enter") {
properties.onNameChanged(name);
properties.onClose();
} else if (event.key === "Escape") {
properties.onClose();
}
}}
onChange={(event) => {
setName(event.target.value);
}}
spellCheck="false"
onPaste={(event) => event.stopPropagation()}
onCopy={(event) => event.stopPropagation()}
onCut={(event) => event.stopPropagation()}
/>
</StyledDialogContent>
<DialogFooter>
<StyledButton
onClick={() => {
properties.onNameChanged(name);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
properties.onNameChanged(name);
properties.onClose();
}
}}
tabIndex={0}
>
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
{t("sheet_rename.rename")}
</StyledButton>
</DialogFooter>
</Dialog>
);
};
const StyledDialogTitle = styled("div")`
display: flex;
align-items: center;
height: 44px;
font-size: 14px;
font-weight: 500;
font-family: Inter;
padding: 0px 12px;
justify-content: space-between;
border-bottom: 1px solid ${theme.palette.grey["300"]};
`;
const Cross = styled("div")`
&:hover {
background-color: ${theme.palette.grey["50"]};
}
display: flex;
border-radius: 4px;
height: 24px;
width: 24px;
cursor: pointer;
align-items: center;
justify-content: center;
svg {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
`;
const StyledDialogContent = styled("div")`
font-size: 12px;
margin: 12px;
`;
const StyledTextField = styled(TextField)`
width: 100%;
border-radius: 4px;
overflow: hidden;
& .MuiInputBase-input {
font-size: 14px;
padding: 10px;
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 4px;
color: ${theme.palette.common.black};
background-color: ${theme.palette.common.white};
}
&:hover .MuiInputBase-input {
border: 1px solid ${theme.palette.grey["500"]};
}
`;
const DialogFooter = styled("div")`
color: #757575;
display: flex;
align-items: center;
border-top: 1px solid ${theme.palette.grey["300"]};
font-family: Inter;
justify-content: flex-end;
padding: 12px;
`;
const StyledButton = styled("div")`
cursor: pointer;
color: #ffffff;
background: #f2994a;
padding: 0px 10px;
height: 36px;
line-height: 36px;
border-radius: 4px;
display: flex;
align-items: center;
font-family: "Inter";
font-size: 14px;
&:hover {
background: #d68742;
}
`;
export default SheetRenameDialog;

View File

@@ -1,5 +1,5 @@
import type { MenuItemProps } from "@mui/material"; import type { MenuItemProps } from "@mui/material";
import { Button, Menu, MenuItem, styled } from "@mui/material"; import { Button, Input, Menu, MenuItem, styled } from "@mui/material";
import { import {
ChevronDown, ChevronDown,
EyeOff, EyeOff,
@@ -7,14 +7,13 @@ import {
TextCursorInput, TextCursorInput,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { useRef, useState } from "react"; import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { theme } from "../../theme"; import { theme } from "../../theme";
import ColorPicker from "../ColorPicker/ColorPicker"; import ColorPicker from "../ColorPicker/ColorPicker";
import { isInReferenceMode } from "../Editor/util"; import { isInReferenceMode } from "../Editor/util";
import type { WorkbookState } from "../workbookState"; import type { WorkbookState } from "../workbookState";
import SheetDeleteDialog from "./SheetDeleteDialog"; import SheetDeleteDialog from "./SheetDeleteDialog";
import SheetRenameDialog from "./SheetRenameDialog";
interface SheetTabProps { interface SheetTabProps {
name: string; name: string;
@@ -48,14 +47,6 @@ function SheetTab(props: SheetTabProps) {
onSelected(); onSelected();
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}; };
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const handleCloseRenameDialog = () => {
setRenameDialogOpen(false);
};
const handleOpenRenameDialog = () => {
setRenameDialogOpen(true);
};
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -66,19 +57,70 @@ function SheetTab(props: SheetTabProps) {
const handleCloseDeleteDialog = () => { const handleCloseDeleteDialog = () => {
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
}; };
const [isEditing, setIsEditing] = useState(false);
const [editingName, setEditingName] = useState(name);
const inputRef = useRef<HTMLInputElement>(null);
const measureRef = useRef<HTMLSpanElement>(null);
const [inputWidth, setInputWidth] = useState<number>(0);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
useEffect(() => {
if (!isEditing) {
setEditingName(name);
}
}, [name, isEditing]);
useLayoutEffect(() => {
if (isEditing && measureRef.current) {
void editingName;
const width = measureRef.current.offsetWidth;
setInputWidth(Math.max(width + 8, 6));
}
}, [editingName, isEditing]);
const handleStartEditing = () => {
setEditingName(name);
setInputWidth(Math.max(name.length * 7 + 8, 6));
setIsEditing(true);
};
const handleSave = () => {
if (editingName.trim() !== "") {
props.onRenamed(editingName.trim());
}
setIsEditing(false);
};
const handleCancel = () => {
setEditingName(name);
setIsEditing(false);
};
return ( return (
<> <>
<TabWrapper <TabWrapper
$color={color} $color={color}
$selected={selected} $selected={selected}
onClick={(event: React.MouseEvent) => { onClick={(event: React.MouseEvent) => {
if (!isEditing) {
onSelected(); onSelected();
}
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
}} }}
onDoubleClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
handleStartEditing();
}}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onPointerDown={(event: React.PointerEvent) => { onPointerDown={(event: React.PointerEvent) => {
// If it is in browse mode stop he event
const cell = workbookState.getEditingCell(); const cell = workbookState.getEditingCell();
if (cell && isInReferenceMode(cell.text, cell.cursorStart)) { if (cell && isInReferenceMode(cell.text, cell.cursorStart)) {
event.stopPropagation(); event.stopPropagation();
@@ -87,10 +129,46 @@ function SheetTab(props: SheetTabProps) {
}} }}
ref={colorButton} ref={colorButton}
> >
<Name onDoubleClick={handleOpenRenameDialog}>{name}</Name> {isEditing ? (
<StyledButton onClick={handleOpen} disableRipple={true} $active={open}> <>
<HiddenMeasure ref={measureRef}>{editingName || " "}</HiddenMeasure>
<StyledInput
inputRef={inputRef}
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
style={{ width: `${inputWidth}px` }}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSave();
} else if (e.key === "Escape") {
e.preventDefault();
handleCancel();
}
e.stopPropagation();
}}
onBlur={() => {
handleSave();
}}
onClick={(e) => e.stopPropagation()}
spellCheck={false}
/>
<StyledButton disableRipple={true} disabled={true} $active={false}>
<ChevronDown /> <ChevronDown />
</StyledButton> </StyledButton>
</>
) : (
<>
<Name>{name}</Name>
<StyledButton
onClick={handleOpen}
disableRipple={true}
$active={open}
>
<ChevronDown />
</StyledButton>
</>
)}
</TabWrapper> </TabWrapper>
<StyledMenu <StyledMenu
anchorEl={anchorEl} anchorEl={anchorEl}
@@ -107,7 +185,7 @@ function SheetTab(props: SheetTabProps) {
> >
<StyledMenuItem <StyledMenuItem
onClick={() => { onClick={() => {
handleOpenRenameDialog(); handleStartEditing();
handleClose(); handleClose();
}} }}
> >
@@ -145,15 +223,6 @@ function SheetTab(props: SheetTabProps) {
{t("sheet_tab.delete")} {t("sheet_tab.delete")}
</DeleteButton> </DeleteButton>
</StyledMenu> </StyledMenu>
<SheetRenameDialog
open={renameDialogOpen}
onClose={handleCloseRenameDialog}
defaultName={name}
onNameChanged={(newName) => {
props.onRenamed(newName);
setRenameDialogOpen(false);
}}
/>
<ColorPicker <ColorPicker
color={color} color={color}
defaultColor="#FFFFFF" defaultColor="#FFFFFF"
@@ -220,15 +289,19 @@ const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>`
margin-right: 12px; margin-right: 12px;
border-bottom: 3px solid ${(props) => props.$color}; border-bottom: 3px solid ${(props) => props.$color};
line-height: 37px; line-height: 37px;
padding: 0px 4px; padding: 0px 4px 0px 6px;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
min-width: 40px;
font-weight: ${(props) => (props.$selected ? 600 : 400)}; font-weight: ${(props) => (props.$selected ? 600 : 400)};
background-color: ${(props) => background-color: ${(props) =>
props.$selected ? `${theme.palette.grey[50]}80` : "transparent"}; props.$selected ? `${theme.palette.grey[50]}` : "transparent"};
&:hover {
background-color: ${theme.palette.grey[50]}80;
}
`; `;
const StyledButton = styled(Button)<{ $active?: boolean }>` const StyledButton = styled(Button)<{ $active: boolean }>`
width: 16px; width: 16px;
height: 16px; height: 16px;
min-width: 0px; min-width: 0px;
@@ -236,6 +309,7 @@ const StyledButton = styled(Button)<{ $active?: boolean }>`
color: inherit; color: inherit;
font-weight: inherit; font-weight: inherit;
border-radius: 4px; border-radius: 4px;
flex-shrink: 0;
background-color: ${(props) => background-color: ${(props) =>
props.$active ? `${theme.palette.grey[300]}` : "transparent"}; props.$active ? `${theme.palette.grey[300]}` : "transparent"};
&:hover { &:hover {
@@ -244,6 +318,9 @@ const StyledButton = styled(Button)<{ $active?: boolean }>`
&:active { &:active {
background-color: ${theme.palette.grey[300]}; background-color: ${theme.palette.grey[300]};
} }
&:disabled {
pointer-events: none;
}
svg { svg {
width: 14px; width: 14px;
height: 14px; height: 14px;
@@ -255,6 +332,51 @@ const Name = styled("div")`
margin-right: 5px; margin-right: 5px;
text-wrap: nowrap; text-wrap: nowrap;
user-select: none; user-select: none;
width: 100%;
text-align: center;
`;
const HiddenMeasure = styled("span")`
position: absolute;
visibility: hidden;
white-space: pre;
font-size: 12px;
font-family: Inter;
font-weight: inherit;
padding: 0;
margin: 0;
height: 100%;
overflow: hidden;
pointer-events: none;
`;
const StyledInput = styled(Input)`
font-size: 12px;
font-family: Inter;
font-weight: inherit;
min-width: 6px;
margin-right: 2px;
outline-offset: 1px;
min-height: 100%;
flex-grow: 1;
& .MuiInputBase-input {
font-family: Inter;
font-weight: inherit;
padding: 6px 0px;
outline: 1px solid ${theme.palette.primary.main};
border-radius: 2px;
color: ${theme.palette.common.black};
text-align: center;
will-change: width;
&:focus {
border-color: ${theme.palette.primary.main};
}
}
&::before,
&::after {
display: none;
}
`; `;
const MenuDivider = styled("div")` const MenuDivider = styled("div")`

View File

@@ -137,6 +137,7 @@ const Sheets = styled("div")`
padding-left: 12px; padding-left: 12px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100%;
`; `;
const SheetInner = styled("div")` const SheetInner = styled("div")`