Fix column resize handle (#14559)

This PR fixes the column resize handle issues.

Fixes in https://github.com/twentyhq/core-team-issues/issues/1490 : 
- We see the pointer blink when resizing => Already here before
refactor, this is because the mouse enter / leave events shouldn't be
triggered while resizing.
- Resizing handle sometimes cannot be clicked anymore / does not appear
anymore (see if another listener doesn't prevent this)

The code will need to be cleaned after all fixes have been done though
has it duplicates logic between the different header components.
This commit is contained in:
Lucas Bordeau
2025-09-17 14:39:44 +02:00
committed by GitHub
parent 24204b2928
commit 6e47c1b5c6
6 changed files with 77 additions and 17 deletions
@@ -5,7 +5,9 @@ import { HIDDEN_TABLE_COLUMN_DROPDOWN_ID } from '@/object-record/record-table/co
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidth';
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidthClassName';
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent';
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
@@ -87,6 +89,8 @@ export const RecordTableHeaderAddColumnButton = () => {
const shouldDisplayBorderBottom =
hasRecordGroups || !isFirstRowActiveOrFocused || isScrolledVertically;
const { visibleRecordFields } = useRecordTableContextOrThrow();
return (
<StyledPlusIconHeaderCell
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
@@ -95,6 +99,10 @@ export const RecordTableHeaderAddColumnButton = () => {
RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME,
)}
>
<RecordTableHeaderResizeHandler
recordFieldIndex={visibleRecordFields.length}
position="left"
/>
<StyledDropdownContainer>
<Dropdown
dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID}
@@ -9,10 +9,12 @@ import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/reco
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { cx } from '@linaria/core';
import { isDefined } from 'twenty-shared/utils';
type RecordTableHeaderCellProps = {
recordField: RecordField;
@@ -50,6 +52,12 @@ export const RecordTableHeaderCell = ({
hasRecordGroupsComponentSelector,
);
const resizedFieldMetadataItemId = useRecoilComponentValue(
resizedFieldMetadataIdComponentState,
);
const isResizingAnyColumn = isDefined(resizedFieldMetadataItemId);
const shouldDisplayBorderBottom =
hasRecordGroups || !isFirstRowActiveOrFocused || isScrolledVertically;
@@ -61,12 +69,20 @@ export const RecordTableHeaderCell = ({
)}
key={recordField.fieldMetadataItemId}
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
isResizing={isResizingAnyColumn}
>
<RecordTableHeaderResizeHandler
recordFieldIndex={recordFieldIndex}
position="left"
/>
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
<RecordTableHeaderResizeHandler recordField={recordField} />
<RecordTableHeaderResizeHandler
recordFieldIndex={recordFieldIndex}
position="right"
/>
</RecordTableHeaderCellContainer>
);
};
@@ -4,6 +4,7 @@ import styled from '@emotion/styled';
const StyledHeaderCell = styled.div<{
zIndex?: number;
shouldDisplayBorderBottom: boolean;
isResizing: boolean;
}>`
color: ${({ theme }) => theme.font.color.tertiary};
padding: 0;
@@ -23,7 +24,11 @@ const StyledHeaderCell = styled.div<{
: 'none'};
user-select: none;
${({ theme }) => {
${({ theme, isResizing }) => {
if (isResizing) {
return '';
}
return `
&:hover {
background: ${theme.background.secondary};
@@ -34,7 +39,7 @@ const StyledHeaderCell = styled.div<{
`;
}};
cursor: pointer;
cursor: ${({ isResizing }) => (isResizing ? 'col-resize' : 'pointer')};
z-index: ${({ zIndex }) => zIndex ?? 'auto'};
`;
@@ -13,11 +13,12 @@ import { isRecordTableRowFocusActiveComponentState } from '@/object-record/recor
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { cx } from '@linaria/core';
import { filterOutByProperty } from 'twenty-shared/utils';
import { filterOutByProperty, isDefined } from 'twenty-shared/utils';
export const RecordTableHeaderFirstScrollableCell = () => {
const { objectMetadataItem, visibleRecordFields } =
@@ -94,6 +95,12 @@ export const RecordTableHeaderFirstScrollableCell = () => {
const shouldDisplayBorderBottom =
hasRecordGroups || !isFirstRowActiveOrFocused || isScrolledVertically;
const resizedFieldMetadataItemId = useRecoilComponentValue(
resizedFieldMetadataIdComponentState,
);
const isResizingAnyColumn = isDefined(resizedFieldMetadataItemId);
if (!recordField) {
return <></>;
}
@@ -104,12 +111,14 @@ export const RecordTableHeaderFirstScrollableCell = () => {
key={recordField.fieldMetadataItemId}
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
zIndex={zIndex}
isResizing={isResizingAnyColumn}
>
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="left" />
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
<RecordTableHeaderResizeHandler recordField={recordField} />
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="right" />
</RecordTableHeaderCellContainer>
);
};
@@ -13,12 +13,13 @@ import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/reco
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { cx } from '@linaria/core';
import { useState } from 'react';
import { findByProperty } from 'twenty-shared/utils';
import { findByProperty, isDefined } from 'twenty-shared/utils';
const StyledColumnHeadContainer = styled.div`
cursor: pointer;
@@ -66,6 +67,12 @@ export const RecordTableHeaderLabelIdentifierCell = () => {
hasRecordGroupsComponentSelector,
);
const resizedFieldMetadataItemId = useRecoilComponentValue(
resizedFieldMetadataIdComponentState,
);
const isResizingAnyColumn = isDefined(resizedFieldMetadataItemId);
const shouldDisplayBorderBottom =
hasRecordGroups || !isFirstRowActiveOrFocused || isScrolledVertically;
@@ -80,6 +87,7 @@ export const RecordTableHeaderLabelIdentifierCell = () => {
onMouseEnter={() => setIconIsVisible(true)}
onMouseLeave={() => setIconIsVisible(false)}
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
isResizing={isResizingAnyColumn}
>
<StyledColumnHeadContainer>
<RecordTableColumnHeadWithDropdown
@@ -88,7 +96,7 @@ export const RecordTableHeaderLabelIdentifierCell = () => {
/>
{iconIsVisible && <RecordTableHeaderLabelIdentifierCellPlusButton />}
</StyledColumnHeadContainer>
<RecordTableHeaderResizeHandler recordField={recordField} />
<RecordTableHeaderResizeHandler recordFieldIndex={0} position="right" />
</RecordTableHeaderCellContainer>
);
};
@@ -1,21 +1,24 @@
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import styled from '@emotion/styled';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledResizeHandler = styled.div<{ isResizing: boolean }>`
const StyledResizeHandler = styled.div<{
isResizing: boolean;
position: 'left' | 'right';
}>`
bottom: 0;
cursor: col-resize;
padding: 0 ${({ theme }) => theme.spacing(2)};
position: absolute;
right: -9px;
${({ position }) => (position === 'left' ? 'left: -1px;' : 'right: -1px;')}
top: 0;
width: 3px;
width: 10px;
z-index: 1;
${({ isResizing, theme }) => {
${({ isResizing, theme, position }) => {
if (isResizing === true) {
return `&:after {
background-color: ${theme.color.blue};
@@ -23,7 +26,8 @@ const StyledResizeHandler = styled.div<{ isResizing: boolean }>`
content: '';
display: block;
position: absolute;
right: 8px;
${position === 'left' ? 'left: -1px;' : 'right: -1px;'}
top: 0;
width: 2px;
}`;
@@ -32,10 +36,19 @@ const StyledResizeHandler = styled.div<{ isResizing: boolean }>`
`;
export const RecordTableHeaderResizeHandler = ({
recordField,
recordFieldIndex,
position,
}: {
recordField: RecordField;
recordFieldIndex: number;
position: 'left' | 'right';
}) => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const recordField: RecordField | undefined =
position === 'left'
? visibleRecordFields[recordFieldIndex - 1]
: visibleRecordFields[recordFieldIndex];
const isMobile = useIsMobile();
const columnResizeDisabled = isMobile;
@@ -44,13 +57,13 @@ export const RecordTableHeaderResizeHandler = ({
useRecoilComponentState(resizedFieldMetadataIdComponentState);
const isResizing =
recordField.fieldMetadataItemId === resizedFieldMetadataItemId;
recordField?.fieldMetadataItemId === resizedFieldMetadataItemId;
const { setDragSelectionStartEnabled } = useDragSelect();
const handlePointerDown = () => {
setDragSelectionStartEnabled(false);
setResizedFieldMetadataItemId(recordField.fieldMetadataItemId);
setResizedFieldMetadataItemId(recordField?.fieldMetadataItemId);
};
return (
@@ -60,6 +73,7 @@ export const RecordTableHeaderResizeHandler = ({
role="separator"
onPointerDown={handlePointerDown}
isResizing={isResizing}
position={position}
/>
)
);