Fix autogrowing input glitch (#10224)

- Removed unnecessary resize observer
- Removed unused component
- Fixed autogrowing input glitch

# Before


https://github.com/user-attachments/assets/a7de71d5-bc6e-495f-851c-18df596749dd

# After


https://github.com/user-attachments/assets/63588d0e-1122-43fe-b685-3f3a4ec4114e
This commit is contained in:
Raphaël Bosi
2025-02-17 11:10:39 +01:00
committed by GitHub
parent 0cd6e29bff
commit 9ef6486b06
7 changed files with 52 additions and 237 deletions

View File

@ -1,90 +0,0 @@
import styled from '@emotion/styled';
import { ChangeEvent } from 'react';
import { StyledTextInput as UIStyledTextInput } from '@/ui/field/input/components/TextInput';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { ComputeNodeDimensions } from 'twenty-ui';
import { InputHotkeyScope } from '../types/InputHotkeyScope';
export type EntityTitleDoubleTextInputProps = {
firstValue: string;
secondValue: string;
firstValuePlaceholder: string;
secondValuePlaceholder: string;
onChange: (firstValue: string, secondValue: string) => void;
className?: string;
};
const StyledDoubleTextContainer = styled.div`
align-items: center;
display: flex;
justify-content: center;
text-align: center;
`;
const StyledTextInput = styled(UIStyledTextInput)`
margin: 0 ${({ theme }) => theme.spacing(0.5)};
padding: 0;
width: ${({ width }) => (width ? `${width}px` : 'auto')};
&:hover:not(:focus) {
background-color: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
padding: 0 ${({ theme }) => theme.spacing(1)};
}
`;
export const EntityTitleDoubleTextInput = ({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
onChange,
className,
}: EntityTitleDoubleTextInputProps) => {
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleFocus = () => {
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
};
const handleBlur = () => {
goBackToPreviousHotkeyScope();
};
return (
<StyledDoubleTextContainer className={className}>
<ComputeNodeDimensions node={firstValue || firstValuePlaceholder}>
{(nodeDimensions) => (
<StyledTextInput
width={nodeDimensions?.width}
placeholder={firstValuePlaceholder}
value={firstValue}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value, secondValue);
}}
/>
)}
</ComputeNodeDimensions>
<ComputeNodeDimensions node={secondValue || secondValuePlaceholder}>
{(nodeDimensions) => (
<StyledTextInput
width={nodeDimensions?.width}
autoComplete="off"
placeholder={secondValuePlaceholder}
value={secondValue}
onFocus={handleFocus}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(firstValue, event.target.value);
}}
/>
)}
</ComputeNodeDimensions>
</StyledDoubleTextContainer>
);
};

View File

@ -11,12 +11,7 @@ import {
useRef,
useState,
} from 'react';
import {
ComputeNodeDimensions,
IconComponent,
IconEye,
IconEyeOff,
} from 'twenty-ui';
import { AutogrowWrapper, IconComponent, IconEye, IconEyeOff } from 'twenty-ui';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
@ -64,6 +59,8 @@ const StyledInput = styled.input<
inheritFontStyles ? 'inherit' : theme.font.weight.regular};
height: ${({ sizeVariant }) =>
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
line-height: ${({ sizeVariant }) =>
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
outline: none;
padding: ${({ theme, sizeVariant, autoGrow }) =>
autoGrow
@ -80,7 +77,6 @@ const StyledInput = styled.input<
width: ${({ theme, width }) =>
width ? `calc(${width}px + ${theme.spacing(0.5)})` : '100%'};
max-width: ${({ autoGrow }) => (autoGrow ? '100%' : 'none')};
&::placeholder,
&::-webkit-input-placeholder {
color: ${({ theme }) => theme.font.color.light};
@ -296,13 +292,13 @@ const TextInputV2Component = forwardRef<
},
);
const StyledComputeNodeDimensions = styled(ComputeNodeDimensions)<{
const StyledAutogrowWrapper = styled(AutogrowWrapper)<{
sizeVariant?: TextInputV2Size;
}>`
border: 1px solid transparent;
height: ${({ sizeVariant }) =>
sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'};
padding: 0 ${({ theme }) => theme.spacing(1)};
padding: 0 ${({ theme }) => theme.spacing(1.25)};
box-sizing: border-box;
`;
@ -313,19 +309,17 @@ const TextInputV2WithAutoGrowWrapper = forwardRef<
return (
<>
{props.autoGrow ? (
<StyledComputeNodeDimensions
<StyledAutogrowWrapper
sizeVariant={props.sizeVariant}
node={props.value || props.placeholder}
>
{(nodeDimensions) => (
<TextInputV2Component
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
width={nodeDimensions?.width}
/>
)}
</StyledComputeNodeDimensions>
<TextInputV2Component
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
fullWidth={true}
/>
</StyledAutogrowWrapper>
) : (
<TextInputV2Component
// eslint-disable-next-line react/jsx-props-no-spreading

View File

@ -3,7 +3,6 @@ import { ThemeType } from 'twenty-ui';
export { ThemeProvider } from '@emotion/react';
export * from 'twenty-ui';
export * from './src/modules/ui/input/components/AutosizeTextInput';
export * from './src/modules/ui/input/components/EntityTitleDoubleTextInput';
export * from './src/modules/ui/input/components/IconPicker';
export * from './src/modules/ui/input/components/ImageInput';
export * from './src/modules/ui/input/components/Select';

View File

@ -0,0 +1,38 @@
import styled from '@emotion/styled';
import { ReactNode } from 'react';
type AutogrowWrapperProps = {
children: ReactNode;
node?: ReactNode;
className?: string;
};
const StyledNodeWrapper = styled.span`
pointer-events: none;
visibility: hidden;
`;
const StyledContainer = styled.div`
max-width: 100%;
position: relative;
`;
const StyledChildWrapper = styled.div`
left: 0;
top: 0;
position: absolute;
width: 100%;
`;
export const AutogrowWrapper = ({
children,
node = children,
className,
}: AutogrowWrapperProps) => {
return (
<StyledContainer className={className}>
<StyledNodeWrapper>{node}</StyledNodeWrapper>
<StyledChildWrapper>{children}</StyledChildWrapper>
</StyledContainer>
);
};

View File

@ -1,67 +0,0 @@
import styled from '@emotion/styled';
import { ReactNode, useLayoutEffect, useRef, useState } from 'react';
import { isDefined } from 'twenty-shared';
type ComputeNodeDimensionsProps = {
children: (
dimensions: { height: number; width: number } | undefined,
) => ReactNode;
node?: ReactNode;
className?: string;
};
const StyledNodeWrapper = styled.span`
pointer-events: none;
visibility: hidden;
`;
const StyledDiv = styled.div`
max-width: 100%;
position: relative;
`;
const StyledChildWrapper = styled.div`
left: 0;
position: absolute;
top: 0;
`;
export const ComputeNodeDimensions = ({
children,
node = children(undefined),
className,
}: ComputeNodeDimensionsProps) => {
const nodeWrapperRef = useRef<HTMLDivElement>(null);
const [nodeDimensions, setNodeDimensions] = useState<
| {
width: number;
height: number;
}
| undefined
>(undefined);
useLayoutEffect(() => {
if (!nodeWrapperRef.current) {
return;
}
const resizeObserver = new ResizeObserver(() => {
if (isDefined(nodeWrapperRef.current)) {
setNodeDimensions({
width: nodeWrapperRef.current.offsetWidth,
height: nodeWrapperRef.current.offsetHeight,
});
}
});
resizeObserver.observe(nodeWrapperRef.current);
return () => resizeObserver.disconnect();
}, [nodeWrapperRef]);
return (
<StyledDiv ref={nodeWrapperRef} className={className}>
<StyledNodeWrapper>{node}</StyledNodeWrapper>
<StyledChildWrapper>
{nodeDimensions && children(nodeDimensions)}
</StyledChildWrapper>
</StyledDiv>
);
};

View File

@ -9,7 +9,7 @@ export * from './color/utils/stringToHslColor';
export * from './device/getOsControlSymbol';
export * from './device/getOsShortcutSeparator';
export * from './device/getUserDevice';
export * from './dimensions/components/ComputeNodeDimensions';
export * from './dimensions/components/AutogrowWrapper';
export * from './responsive/hooks/useIsMobile';
export * from './screen-size/hooks/useScreenSize';
export * from './state/utils/createState';

View File

@ -112,65 +112,6 @@ export const MyComponent = () => {
</ArticleTabs>
## Entity Title Double Text Input
Displays a pair of text inputs side by side, allowing the user to edit two related values simultaneously.
<ArticleTabs label1="Usage" label2="Props">
<ArticleTab>
<SandpackEditor content={`import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { EntityTitleDoubleTextInput } from "@/ui/input/components/EntityTitleDoubleTextInput";
export const MyComponent = () => {
const [firstValue, setFirstValue] = useState(
"First Value"
);
const [secondValue, setSecondValue] = useState(
"Second Value"
);
const handleInputChange = (newFirstValue, newSecondValue) => {
setFirstValue(newFirstValue);
setSecondValue(newSecondValue);
};
return (
<RecoilRoot>
<EntityTitleDoubleTextInput
firstValue={firstValue}
secondValue={secondValue}
firstValuePlaceholder="Enter First Value"
secondValuePlaceholder="Enter Second Value"
onChange={handleInputChange}
/>
</RecoilRoot>
);
};`} />
</ArticleTab>
<ArticleTab>
<ArticlePropsTable options={[
['firstValue', 'string', 'The value for the first text input'],
['secondValue', 'string', 'The value for the second text input'],
['firstValuePlaceholder', 'string', 'Placeholder text for the first text input, displayed when the input is empty'],
['secondValuePlaceholder', 'string', 'Placeholder text for the second text input, displayed when the input is empty'],
['onChange', 'function', 'The callback function you want to trigger when the text input changes']
]} />
</ArticleTab>
</ArticleTabs>
## Text Area
Allows you to create multi-line text inputs.