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:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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';
|
||||
|
||||
@ -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.
|
||||
|
||||
Reference in New Issue
Block a user