Skip to content

Commit

Permalink
feat: add column resizing (twentyhq#975)
Browse files Browse the repository at this point in the history
* feat: add column resizing

Closes twentyhq#817

* Use mouse up and down instead of dragging

---------

Co-authored-by: Charles Bochet <[email protected]>
  • Loading branch information
2 people authored and AdityaPimpalkar committed Aug 3, 2023
1 parent 582876a commit c7f0ae1
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 14 deletions.
9 changes: 8 additions & 1 deletion front/src/modules/ui/table/components/ColumnHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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>
);
}
14 changes: 10 additions & 4 deletions front/src/modules/ui/table/components/EntityTable.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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';

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';
Expand Down Expand Up @@ -99,6 +101,8 @@ export function EntityTable<SortField>({
onSortsUpdate,
useUpdateEntityMutation,
}: OwnProps<SortField>) {
const viewFields = useRecoilValue(viewFieldsFamilyState);

const tableBodyRef = React.useRef<HTMLDivElement>(null);

useMapKeyboardToSoftFocus();
Expand All @@ -123,10 +127,12 @@ export function EntityTable<SortField>({
onSortsUpdate={onSortsUpdate}
/>
<StyledTableWrapper>
<StyledTable>
<EntityTableHeader />
<EntityTableBody />
</StyledTable>
{viewFields.length && (
<StyledTable>
<EntityTableHeader viewFields={viewFields} />
<EntityTableBody />
</StyledTable>
)}
</StyledTableWrapper>
</StyledTableContainer>
</StyledTableWithHeader>
Expand Down
117 changes: 108 additions & 9 deletions front/src/modules/ui/table/components/EntityTableHeader.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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>
Expand Down

0 comments on commit c7f0ae1

Please sign in to comment.