From 58e5d2426141076fe1802cb798abbb3296573a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Mon, 31 Jul 2023 05:38:38 +0200 Subject: [PATCH] feat: add column resizing (#975) * feat: add column resizing Closes #817 * Use mouse up and down instead of dragging --------- Co-authored-by: Charles Bochet --- .../ui/table/components/ColumnHead.tsx | 9 +- .../ui/table/components/EntityTable.tsx | 14 ++- .../ui/table/components/EntityTableHeader.tsx | 117 ++++++++++++++++-- 3 files changed, 126 insertions(+), 14 deletions(-) diff --git a/front/src/modules/ui/table/components/ColumnHead.tsx b/front/src/modules/ui/table/components/ColumnHead.tsx index 3309caccb0..b0f15c4d75 100644 --- a/front/src/modules/ui/table/components/ColumnHead.tsx +++ b/front/src/modules/ui/table/components/ColumnHead.tsx @@ -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 ( {viewIcon} - {viewName} + {viewName} ); } diff --git a/front/src/modules/ui/table/components/EntityTable.tsx b/front/src/modules/ui/table/components/EntityTable.tsx index 18b5fb6d7c..6f9bcb5655 100644 --- a/front/src/modules/ui/table/components/EntityTable.tsx +++ b/front/src/modules/ui/table/components/EntityTable.tsx @@ -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({ onSortsUpdate, useUpdateEntityMutation, }: OwnProps) { + const viewFields = useRecoilValue(viewFieldsFamilyState); + const tableBodyRef = React.useRef(null); useMapKeyboardToSoftFocus(); @@ -123,10 +127,12 @@ export function EntityTable({ onSortsUpdate={onSortsUpdate} /> - - - - + {viewFields.length && ( + + + + + )} diff --git a/front/src/modules/ui/table/components/EntityTableHeader.tsx b/front/src/modules/ui/table/components/EntityTableHeader.tsx index a217059918..53ebf7dff2 100644 --- a/front/src/modules/ui/table/components/EntityTableHeader.tsx +++ b/front/src/modules/ui/table/components/EntityTableHeader.tsx @@ -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[]; +}; + +export function EntityTableHeader({ viewFields }: OwnProps) { + const initialColumnWidths = viewFields.reduce>( + (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(null); + const [offset, setOffset] = useState(0); + + const handleResizeHandlerDragStart = useCallback( + (event: PointerEvent, fieldId: string) => { + setIsResizing(true); + setResizedFieldId(fieldId); + setInitialPointerPositionX(event.clientX); + }, + [setIsResizing, setResizedFieldId, setInitialPointerPositionX], + ); + + const handleResizeHandlerDrag = useCallback( + (event: PointerEvent) => { + 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 ( @@ -20,20 +105,34 @@ export function EntityTableHeader() { > + {viewFields.map((viewField) => ( - - + + handleResizeHandlerDragStart(event, viewField.id) + } + onPointerMove={handleResizeHandlerDrag} + onPointerOut={handleResizeHandlerDragEnd} + onPointerUp={handleResizeHandlerDragEnd} + /> + ))}