feat: add column resizing (#975)

* feat: add column resizing

Closes #817

* Use mouse up and down instead of dragging

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thaïs 2023-07-31 05:38:38 +02:00 committed by GitHub
parent ade5e52e55
commit 58e5d24261
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 126 additions and 14 deletions

View File

@ -14,6 +14,7 @@ const StyledTitle = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(8)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledIcon = styled.div`
@ -25,11 +26,17 @@ const StyledIcon = styled.div`
}
`;
const StyledText = styled.span`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export function ColumnHead({ viewName, viewIcon }: OwnProps) {
return (
<StyledTitle>
<StyledIcon>{viewIcon}</StyledIcon>
{viewName}
<StyledText>{viewName}</StyledText>
</StyledTitle>
);
}

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
@ -7,6 +8,7 @@ import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useLis
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
import { EntityUpdateMutationHookContext } from '../states/EntityUpdateMutationHookContext';
import { viewFieldsFamilyState } from '../states/viewFieldsState';
import { TableHeader } from '../table-header/components/TableHeader';
import { EntityTableBody } from './EntityTableBody';
@ -99,6 +101,8 @@ export function EntityTable<SortField>({
onSortsUpdate,
useUpdateEntityMutation,
}: OwnProps<SortField>) {
const viewFields = useRecoilValue(viewFieldsFamilyState);
const tableBodyRef = React.useRef<HTMLDivElement>(null);
useMapKeyboardToSoftFocus();
@ -123,10 +127,12 @@ export function EntityTable<SortField>({
onSortsUpdate={onSortsUpdate}
/>
<StyledTableWrapper>
{viewFields.length && (
<StyledTable>
<EntityTableHeader />
<EntityTableHeader viewFields={viewFields} />
<EntityTableBody />
</StyledTable>
)}
</StyledTableWrapper>
</StyledTableContainer>
</StyledTableWithHeader>

View File

@ -1,12 +1,97 @@
import { useRecoilValue } from 'recoil';
import { PointerEvent, useCallback, useState } from 'react';
import styled from '@emotion/styled';
import { viewFieldsFamilyState } from '../states/viewFieldsState';
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
import { ColumnHead } from './ColumnHead';
import { SelectAllCheckbox } from './SelectAllCheckbox';
export function EntityTableHeader() {
const viewFields = useRecoilValue(viewFieldsFamilyState);
const COLUMN_MIN_WIDTH = 75;
const StyledColumnHeaderCell = styled.th<{ isResizing?: boolean }>`
min-width: ${COLUMN_MIN_WIDTH}px;
position: relative;
user-select: none;
${({ isResizing, theme }) => {
if (isResizing) {
return `&:after {
background-color: ${theme.color.blue};
bottom: 0;
content: '';
display: block;
position: absolute;
right: -1px;
top: 0;
width: 2px;
}`;
}
}};
`;
const StyledResizeHandler = styled.div`
bottom: 0;
cursor: col-resize;
padding: 0 ${({ theme }) => theme.spacing(2)};
position: absolute;
right: -9px;
top: 0;
width: 3px;
z-index: 1;
`;
type OwnProps = {
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
};
export function EntityTableHeader({ viewFields }: OwnProps) {
const initialColumnWidths = viewFields.reduce<Record<string, number>>(
(result, viewField) => ({
...result,
[viewField.id]: viewField.columnSize,
}),
{},
);
const [columnWidths, setColumnWidths] = useState(initialColumnWidths);
const [isResizing, setIsResizing] = useState(false);
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
number | null
>(null);
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
const [offset, setOffset] = useState(0);
const handleResizeHandlerDragStart = useCallback(
(event: PointerEvent<HTMLDivElement>, fieldId: string) => {
setIsResizing(true);
setResizedFieldId(fieldId);
setInitialPointerPositionX(event.clientX);
},
[setIsResizing, setResizedFieldId, setInitialPointerPositionX],
);
const handleResizeHandlerDrag = useCallback(
(event: PointerEvent<HTMLDivElement>) => {
if (!isResizing || initialPointerPositionX === null) return;
setOffset(event.clientX - initialPointerPositionX);
},
[isResizing, initialPointerPositionX],
);
const handleResizeHandlerDragEnd = useCallback(() => {
setIsResizing(false);
if (!resizedFieldId) return;
const newColumnWidths = {
...columnWidths,
[resizedFieldId]: Math.max(
columnWidths[resizedFieldId] + offset,
COLUMN_MIN_WIDTH,
),
};
setColumnWidths(newColumnWidths);
setOffset(0);
}, [offset, setIsResizing, columnWidths, resizedFieldId]);
return (
<thead>
@ -20,20 +105,34 @@ export function EntityTableHeader() {
>
<SelectAllCheckbox />
</th>
{viewFields.map((viewField) => (
<th
<StyledColumnHeaderCell
key={viewField.columnOrder.toString()}
isResizing={isResizing && resizedFieldId === viewField.id}
style={{
width: viewField.columnSize,
minWidth: viewField.columnSize,
maxWidth: viewField.columnSize,
width: Math.max(
columnWidths[viewField.id] +
(resizedFieldId === viewField.id ? offset : 0),
COLUMN_MIN_WIDTH,
),
}}
>
<ColumnHead
viewName={viewField.columnLabel}
viewIcon={viewField.columnIcon}
/>
</th>
<StyledResizeHandler
className="cursor-col-resize"
role="separator"
onPointerDown={(event) =>
handleResizeHandlerDragStart(event, viewField.id)
}
onPointerMove={handleResizeHandlerDrag}
onPointerOut={handleResizeHandlerDragEnd}
onPointerUp={handleResizeHandlerDragEnd}
/>
</StyledColumnHeaderCell>
))}
<th></th>
</tr>