Improve mouse tracking (#1061)
* Improve mouse tracking * Fix lint * Fix regression on Filters * Fix according to review
This commit is contained in:
@ -1,15 +1,12 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
||||
import { useUpdateViewFieldMutation } from '~/generated/graphql';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/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';
|
||||
@ -102,11 +99,6 @@ export function EntityTable<SortField>({
|
||||
onSortsUpdate,
|
||||
useUpdateEntityMutation,
|
||||
}: OwnProps<SortField>) {
|
||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
||||
const setViewFields = useSetRecoilState(viewFieldsFamilyState);
|
||||
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
|
||||
const tableBodyRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useMapKeyboardToSoftFocus();
|
||||
@ -120,25 +112,6 @@ export function EntityTable<SortField>({
|
||||
},
|
||||
});
|
||||
|
||||
const handleColumnResize = useCallback(
|
||||
(resizedFieldId: string, width: number) => {
|
||||
setViewFields((previousViewFields) =>
|
||||
previousViewFields.map((viewField) =>
|
||||
viewField.id === resizedFieldId
|
||||
? { ...viewField, columnSize: width }
|
||||
: viewField,
|
||||
),
|
||||
);
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: { sizeInPx: width },
|
||||
where: { id: resizedFieldId },
|
||||
},
|
||||
});
|
||||
},
|
||||
[setViewFields, updateViewFieldMutation],
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityUpdateMutationHookContext.Provider value={useUpdateEntityMutation}>
|
||||
<StyledTableWithHeader>
|
||||
@ -150,15 +123,10 @@ export function EntityTable<SortField>({
|
||||
onSortsUpdate={onSortsUpdate}
|
||||
/>
|
||||
<StyledTableWrapper>
|
||||
{viewFields.length > 0 && (
|
||||
<StyledTable>
|
||||
<EntityTableHeader
|
||||
onColumnResize={handleColumnResize}
|
||||
viewFields={viewFields}
|
||||
/>
|
||||
<EntityTableBody />
|
||||
</StyledTable>
|
||||
)}
|
||||
<StyledTable>
|
||||
<EntityTableHeader />
|
||||
<EntityTableBody />
|
||||
</StyledTable>
|
||||
</StyledTableWrapper>
|
||||
</StyledTableContainer>
|
||||
</StyledTableWithHeader>
|
||||
|
||||
@ -34,14 +34,7 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
|
||||
return (
|
||||
<RecoilScope>
|
||||
<ColumnIndexContext.Provider value={cellIndex}>
|
||||
<td
|
||||
onContextMenu={(event) => handleContextMenu(event)}
|
||||
style={{
|
||||
width: viewField.columnSize,
|
||||
minWidth: viewField.columnSize,
|
||||
maxWidth: viewField.columnSize,
|
||||
}}
|
||||
>
|
||||
<td onContextMenu={(event) => handleContextMenu(event)}>
|
||||
<GenericEditableCell viewField={viewField} />
|
||||
</td>
|
||||
</ColumnIndexContext.Provider>
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
import { PointerEvent, useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
useRecoilCallback,
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
} from 'recoil';
|
||||
|
||||
import type {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '../types/ViewField';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { useUpdateViewFieldMutation } from '~/generated/graphql';
|
||||
|
||||
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
|
||||
import { viewFieldsState } from '../states/viewFieldsState';
|
||||
|
||||
import { ColumnHead } from './ColumnHead';
|
||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||
@ -42,12 +49,11 @@ const StyledResizeHandler = styled.div`
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
onColumnResize: (resizedFieldId: string, width: number) => void;
|
||||
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
|
||||
};
|
||||
export function EntityTableHeader() {
|
||||
const viewFields = useRecoilValue(viewFieldsState);
|
||||
const setViewFields = useSetRecoilState(viewFieldsState);
|
||||
|
||||
export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) {
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
const columnWidths = useMemo(
|
||||
() =>
|
||||
viewFields.reduce<Record<string, number>>(
|
||||
@ -59,46 +65,75 @@ export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) {
|
||||
),
|
||||
[viewFields],
|
||||
);
|
||||
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 [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
|
||||
|
||||
const handleResizeHandlerDragStart = useCallback(
|
||||
(event: PointerEvent<HTMLDivElement>, fieldId: string) => {
|
||||
setIsResizing(true);
|
||||
setResizedFieldId(fieldId);
|
||||
setInitialPointerPositionX(event.clientX);
|
||||
const handleColumnResize = useCallback(
|
||||
(resizedFieldId: string, width: number) => {
|
||||
setViewFields((previousViewFields) =>
|
||||
previousViewFields.map((viewField) =>
|
||||
viewField.id === resizedFieldId
|
||||
? { ...viewField, columnSize: width }
|
||||
: viewField,
|
||||
),
|
||||
);
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: { sizeInPx: width },
|
||||
where: { id: resizedFieldId },
|
||||
},
|
||||
});
|
||||
},
|
||||
[setIsResizing, setResizedFieldId, setInitialPointerPositionX],
|
||||
[setViewFields, updateViewFieldMutation],
|
||||
);
|
||||
|
||||
const handleResizeHandlerDrag = useCallback(
|
||||
(event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!isResizing || initialPointerPositionX === null) return;
|
||||
|
||||
setOffset(event.clientX - initialPointerPositionX);
|
||||
const handleResizeHandlerStart = useCallback(
|
||||
(positionX: number, _: number) => {
|
||||
setInitialPointerPositionX(positionX);
|
||||
},
|
||||
[isResizing, initialPointerPositionX],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleResizeHandlerDragEnd = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
if (!resizedFieldId) return;
|
||||
const handleResizeHandlerMove = useCallback(
|
||||
(positionX: number, _positionY: number) => {
|
||||
if (!initialPointerPositionX) return;
|
||||
setOffset(positionX - initialPointerPositionX);
|
||||
},
|
||||
[setOffset, initialPointerPositionX],
|
||||
);
|
||||
|
||||
const nextWidth = Math.round(
|
||||
Math.max(columnWidths[resizedFieldId] + offset, COLUMN_MIN_WIDTH),
|
||||
);
|
||||
const handleResizeHandlerEnd = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(_positionX: number, _positionY: number) => {
|
||||
if (!resizedFieldId) return;
|
||||
const nextWidth = Math.round(
|
||||
Math.max(
|
||||
columnWidths[resizedFieldId] +
|
||||
snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(),
|
||||
COLUMN_MIN_WIDTH,
|
||||
),
|
||||
);
|
||||
|
||||
if (nextWidth !== columnWidths[resizedFieldId]) {
|
||||
onColumnResize(resizedFieldId, nextWidth);
|
||||
}
|
||||
if (nextWidth !== columnWidths[resizedFieldId]) {
|
||||
handleColumnResize(resizedFieldId, nextWidth);
|
||||
}
|
||||
set(resizeFieldOffsetState, 0);
|
||||
setInitialPointerPositionX(null);
|
||||
setResizedFieldId(null);
|
||||
},
|
||||
[resizedFieldId, columnWidths, setResizedFieldId, handleColumnResize],
|
||||
);
|
||||
|
||||
setOffset(0);
|
||||
}, [resizedFieldId, columnWidths, offset, onColumnResize]);
|
||||
useTrackPointer({
|
||||
shouldTrackPointer: resizedFieldId !== null,
|
||||
onMouseDown: handleResizeHandlerStart,
|
||||
onMouseMove: handleResizeHandlerMove,
|
||||
onMouseUp: handleResizeHandlerEnd,
|
||||
});
|
||||
|
||||
return (
|
||||
<thead>
|
||||
@ -116,7 +151,7 @@ export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) {
|
||||
{viewFields.map((viewField) => (
|
||||
<StyledColumnHeaderCell
|
||||
key={viewField.columnOrder.toString()}
|
||||
isResizing={isResizing && resizedFieldId === viewField.id}
|
||||
isResizing={resizedFieldId === viewField.id}
|
||||
style={{
|
||||
width: Math.max(
|
||||
columnWidths[viewField.id] +
|
||||
@ -132,12 +167,9 @@ export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) {
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={(event) =>
|
||||
handleResizeHandlerDragStart(event, viewField.id)
|
||||
}
|
||||
onPointerMove={handleResizeHandlerDrag}
|
||||
onPointerOut={handleResizeHandlerDragEnd}
|
||||
onPointerUp={handleResizeHandlerDragEnd}
|
||||
onPointerDown={() => {
|
||||
setResizedFieldId(viewField.id);
|
||||
}}
|
||||
/>
|
||||
</StyledColumnHeaderCell>
|
||||
))}
|
||||
|
||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ViewFieldContext } from '../states/ViewFieldContext';
|
||||
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
||||
import { viewFieldsState } from '../states/viewFieldsState';
|
||||
|
||||
import { CheckboxCell } from './CheckboxCell';
|
||||
import { EntityTableCell } from './EntityTableCell';
|
||||
@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
`;
|
||||
|
||||
export function EntityTableRow({ rowId }: { rowId: string }) {
|
||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
||||
const viewFields = useRecoilValue(viewFieldsState);
|
||||
|
||||
return (
|
||||
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>
|
||||
|
||||
@ -33,7 +33,6 @@ export function GenericEntityTableData({
|
||||
variables: { orderBy, where: whereFilters },
|
||||
onCompleted: (data: any) => {
|
||||
const entities = data[getRequestResultKey] ?? [];
|
||||
|
||||
setEntityTableData(entities, filterDefinitionArray);
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
@ -4,8 +4,8 @@ import { Key } from 'ts-key-enum';
|
||||
|
||||
import { DateInputEdit } from '@/ui/input/date/components/DateInputEdit';
|
||||
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
|
||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
import { useEditableCell } from '../../hooks/useEditableCell';
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
|
||||
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
||||
import { viewFieldsState } from '../states/viewFieldsState';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
@ -32,7 +32,7 @@ export const useLoadView = ({
|
||||
const setEntityTableDimensions = useSetRecoilState(
|
||||
entityTableDimensionsState,
|
||||
);
|
||||
const setViewFields = useSetRecoilState(viewFieldsFamilyState);
|
||||
const setViewFields = useSetRecoilState(viewFieldsState);
|
||||
|
||||
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
|
||||
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const resizeFieldOffsetState = atom<number>({
|
||||
key: 'resizeFieldOffsetState',
|
||||
default: 0,
|
||||
});
|
||||
@ -2,9 +2,7 @@ import { atom } from 'recoil';
|
||||
|
||||
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
|
||||
|
||||
export const viewFieldsFamilyState = atom<
|
||||
ViewFieldDefinition<ViewFieldMetadata>[]
|
||||
>({
|
||||
key: 'viewFieldsFamilyState',
|
||||
export const viewFieldsState = atom<ViewFieldDefinition<ViewFieldMetadata>[]>({
|
||||
key: 'viewFieldsState',
|
||||
default: [],
|
||||
});
|
||||
|
||||
@ -64,18 +64,20 @@ export function TableHeader<SortField>({
|
||||
</>
|
||||
}
|
||||
displayBottomBorder={false}
|
||||
rightComponents={[
|
||||
<FilterDropdownButton
|
||||
context={TableContext}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>,
|
||||
<SortDropdownButton<SortField>
|
||||
isSortSelected={sorts.length > 0}
|
||||
availableSorts={availableSorts || []}
|
||||
onSortSelect={sortSelect}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>,
|
||||
]}
|
||||
rightComponent={
|
||||
<>
|
||||
<FilterDropdownButton
|
||||
context={TableContext}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
<SortDropdownButton<SortField>
|
||||
isSortSelected={sorts.length > 0}
|
||||
availableSorts={availableSorts || []}
|
||||
onSortSelect={sortSelect}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
bottomComponent={
|
||||
<SortAndFilterBar
|
||||
context={TableContext}
|
||||
|
||||
Reference in New Issue
Block a user