Skip to content

Commit

Permalink
Unselect record table records on table body click (#8306)
Browse files Browse the repository at this point in the history
We have previously fixed the unselection of table records on click
outside. However, the ref was mispositioned as it selected the full
height table. In the case of low record numbers, we also want the
unselection to happen on table body click
  • Loading branch information
charlesBochet authored Nov 4, 2024
1 parent 6959918 commit 52e5f7d
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import { isNonEmptyString, isNull } from '@sniptt/guards';

import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect';
import { RecordTableBodyUnselectEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRef } from 'react';

const StyledTable = styled.table`
border-radius: ${({ theme }) => theme.border.radius.sm};
Expand All @@ -32,11 +38,17 @@ export const RecordTable = ({
objectNameSingular,
onColumnsChange,
}: RecordTableProps) => {
const tableBodyRef = useRef<HTMLTableElement>(null);

const isRecordTableInitialLoading = useRecoilComponentValueV2(
isRecordTableInitialLoadingComponentState,
recordTableId,
);

const { toggleClickOutsideListener } = useClickOutsideListener(
RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
);

const tableRowIds = useRecoilComponentValueV2(
tableRowIdsComponentState,
recordTableId,
Expand All @@ -47,6 +59,10 @@ export const RecordTable = ({
recordTableId,
);

const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId,
});

const recordTableIsEmpty =
!isRecordTableInitialLoading &&
tableRowIds.length === 0 &&
Expand All @@ -67,15 +83,32 @@ export const RecordTable = ({
viewBarId={viewBarId}
>
<RecordTableBodyEffect />
<RecordTableBodyUnselectEffect
tableBodyRef={tableBodyRef}
recordTableId={recordTableId}
/>
{recordTableIsEmpty ? (
<RecordTableEmptyState />
) : (
<StyledTable className="entity-table-cell">
<RecordTableHeader
objectMetadataNameSingular={objectNameSingular}
<>
<StyledTable className="entity-table-cell" ref={tableBodyRef}>
<RecordTableHeader
objectMetadataNameSingular={objectNameSingular}
/>
<RecordTableBody />
</StyledTable>
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={() => {
resetTableRowSelection();
toggleClickOutsideListener(false);
}}
onDragSelectionChange={setRowSelected}
onDragSelectionEnd={() => {
toggleClickOutsideListener(true);
}}
/>
<RecordTableBody />
</StyledTable>
</>
)}
</RecordTableContextProvider>
</RecordTableComponentInstance>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import styled from '@emotion/styled';
import { useRef } from 'react';
import { useRecoilCallback } from 'recoil';

import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordTable } from '@/object-record/record-table/components/RecordTable';
import { EntityDeleteContext } from '@/object-record/record-table/contexts/EntityDeleteHookContext';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';

import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable';

import { RecordTableInternalEffect } from './RecordTableInternalEffect';

const StyledTableWithHeader = styled.div`
height: 100%;
Expand Down Expand Up @@ -45,12 +40,6 @@ export const RecordTableWithWrappers = ({
recordTableId,
viewBarId,
}: RecordTableWithWrappersProps) => {
const tableBodyRef = useRef<HTMLDivElement>(null);

const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId,
});

const { saveViewFields } = useSaveCurrentViewFields(viewBarId);

const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular });
Expand All @@ -72,25 +61,14 @@ export const RecordTableWithWrappers = ({
<RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>
<StyledTableInternalContainer ref={tableBodyRef}>
<StyledTableInternalContainer>
<RecordTable
viewBarId={viewBarId}
recordTableId={recordTableId}
objectNameSingular={objectNameSingular}
onColumnsChange={handleColumnsChange}
/>
<DragSelect
dragSelectable={tableBodyRef}
onDragSelectionStart={() => {
resetTableRowSelection();
}}
onDragSelectionChange={setRowSelected}
/>
</StyledTableInternalContainer>
<RecordTableInternalEffect
tableBodyRef={tableBodyRef}
recordTableId={recordTableId}
/>
</StyledTableContainer>
</StyledTableWithHeader>
</RecordUpdateContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkey
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';

type RecordTableInternalEffectProps = {
recordTableId: string;
type RecordTableBodyUnselectEffectProps = {
tableBodyRef: React.RefObject<HTMLDivElement>;
recordTableId: string;
};

export const RecordTableInternalEffect = ({
recordTableId,
export const RecordTableBodyUnselectEffect = ({
tableBodyRef,
}: RecordTableInternalEffectProps) => {
const leaveTableFocus = useLeaveTableFocus(recordTableId);
recordTableId,
}: RecordTableBodyUnselectEffectProps) => {
const leaveTableFocus = useLeaveTableFocus();

const { resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({
recordTableId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { RefObject } from 'react';
import {
boxesIntersect,
useSelectionContainer,
} from '@air/react-drag-to-select';
import { useTheme } from '@emotion/react';
import { RefObject } from 'react';
import { RGBA } from 'twenty-ui';

import { useDragSelect } from '../hooks/useDragSelect';

type DragSelectProps = {
dragSelectable: RefObject<HTMLElement>;
onDragSelectionChange: (id: string, selected: boolean) => void;
onDragSelectionStart?: () => void;
onDragSelectionStart?: (event: MouseEvent) => void;
onDragSelectionEnd?: (event: MouseEvent) => void;
};

export const DragSelect = ({
dragSelectable,
onDragSelectionChange,
onDragSelectionStart,
onDragSelectionEnd,
}: DragSelectProps) => {
const theme = useTheme();
const { isDragSelectionStartEnabled } = useDragSelect();
Expand All @@ -37,6 +39,7 @@ export const DragSelect = ({
return true;
},
onSelectionStart: onDragSelectionStart,
onSelectionEnd: onDragSelectionEnd,
onSelectionChange: (box) => {
const scrollAwareBox = {
...box,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { clickOutsideListenerCallbacksComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerCallbacksComponentState';
import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState';
import { clickOutsideListenerIsMouseDownInsideComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsMouseDownInsideComponentState';
import { lockedListenerIdState } from '@/ui/utilities/pointer-event/states/lockedListenerIdState';
import { clickOutsideListenerMouseDownHappenedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerMouseDownHappenedComponentState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';

Expand All @@ -22,6 +22,9 @@ export const useClickOustideListenerStates = (componentId: string) => {
clickOutsideListenerIsActivatedComponentState,
scopeId,
),
lockedListenerIdState,
getClickOutsideListenerMouseDownHappenedState: extractComponentState(
clickOutsideListenerMouseDownHappenedComponentState,
scopeId,
),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ import {
useListenClickOutsideV2,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { toSpliced } from '~/utils/array/toSpliced';
import { isDefined } from '~/utils/isDefined';

export const useClickOutsideListener = (componentId: string) => {
// TODO: improve typing
const scopeId = getScopeIdFromComponentId(componentId) ?? '';

const {
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerCallbacksState,
getClickOutsideListenerMouseDownHappenedState,
} = useClickOustideListenerStates(componentId);

const useListenClickOutside = <T extends Element>({
Expand Down Expand Up @@ -53,8 +50,15 @@ export const useClickOutsideListener = (componentId: string) => {
({ set }) =>
(activated: boolean) => {
set(getClickOutsideListenerIsActivatedState, activated);

if (!activated) {
set(getClickOutsideListenerMouseDownHappenedState, false);
}
},
[getClickOutsideListenerIsActivatedState],
[
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerMouseDownHappenedState,
],
);

const registerOnClickOutsideCallback = useRecoilCallback(
Expand Down Expand Up @@ -148,7 +152,6 @@ export const useClickOutsideListener = (componentId: string) => {
};

return {
scopeId,
useListenClickOutside,
toggleClickOutsideListener,
useRegisterClickOutsideListenerCallback,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const useListenClickOutsideV2 = <T extends Element>({
const {
getClickOutsideListenerIsMouseDownInsideState,
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerMouseDownHappenedState,
} = useClickOustideListenerStates(listenerId);

const handleMouseDown = useRecoilCallback(
Expand All @@ -37,6 +38,8 @@ export const useListenClickOutsideV2 = <T extends Element>({
.getLoadable(getClickOutsideListenerIsActivatedState)
.getValue();

set(getClickOutsideListenerMouseDownHappenedState, true);

const isListening = clickOutsideListenerIsActivated && enabled;

if (!isListening) {
Expand Down Expand Up @@ -92,21 +95,32 @@ export const useListenClickOutsideV2 = <T extends Element>({
}
},
[
getClickOutsideListenerIsActivatedState,
enabled,
mode,
refs,
getClickOutsideListenerIsMouseDownInsideState,
enabled,
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerMouseDownHappenedState,
],
);

const handleClickOutside = useRecoilCallback(
({ snapshot }) =>
(event: MouseEvent | TouchEvent) => {
const clickOutsideListenerIsActivated = snapshot
.getLoadable(getClickOutsideListenerIsActivatedState)
.getValue();

const isListening = clickOutsideListenerIsActivated && enabled;

const isMouseDownInside = snapshot
.getLoadable(getClickOutsideListenerIsMouseDownInsideState)
.getValue();

const hasMouseDownHappened = snapshot
.getLoadable(getClickOutsideListenerMouseDownHappenedState)
.getValue();

if (mode === ClickOutsideMode.compareHTMLRef) {
const clickedElement = event.target as HTMLElement;
let isClickedOnExcluded = false;
Expand All @@ -132,6 +146,8 @@ export const useListenClickOutsideV2 = <T extends Element>({
.some((ref) => ref.current?.contains(event.target as Node));

if (
isListening &&
hasMouseDownHappened &&
!clickedOnAtLeastOneRef &&
!isMouseDownInside &&
!isClickedOnExcluded
Expand Down Expand Up @@ -171,13 +187,21 @@ export const useListenClickOutsideV2 = <T extends Element>({
return true;
});

if (!clickedOnAtLeastOneRef && !isMouseDownInside) {
if (
!clickedOnAtLeastOneRef &&
!isMouseDownInside &&
isListening &&
hasMouseDownHappened
) {
callback(event);
}
}
},
[
getClickOutsideListenerIsActivatedState,
enabled,
getClickOutsideListenerIsMouseDownInsideState,
getClickOutsideListenerMouseDownHappenedState,
mode,
refs,
excludeClassNames,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';

export const clickOutsideListenerMouseDownHappenedComponentState =
createComponentState<boolean>({
key: 'clickOutsideListenerMouseDownHappenedComponentState',
defaultValue: false,
});

This file was deleted.

0 comments on commit 52e5f7d

Please sign in to comment.