Unselect record table records on table body click (#8306)

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
This commit is contained in:
Charles Bochet
2024-11-04 17:44:50 +01:00
committed by GitHub
parent 695991881f
commit 52e5f7daeb
9 changed files with 98 additions and 53 deletions

View File

@@ -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};
@@ -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,
@@ -47,6 +59,10 @@ export const RecordTable = ({
recordTableId,
);
const { resetTableRowSelection, setRowSelected } = useRecordTable({
recordTableId,
});
const recordTableIsEmpty =
!isRecordTableInitialLoading &&
tableRowIds.length === 0 &&
@@ -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>

View File

@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { useRef } from 'react';
import { useRecoilCallback } from 'recoil';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
@@ -7,15 +6,11 @@ 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%;
@@ -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 });
@@ -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>

View File

@@ -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,

View File

@@ -1,9 +1,9 @@
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';
@@ -11,13 +11,15 @@ 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();
@@ -37,6 +39,7 @@ export const DragSelect = ({
return true;
},
onSelectionStart: onDragSelectionStart,
onSelectionEnd: onDragSelectionEnd,
onSelectionChange: (box) => {
const scrollAwareBox = {
...box,

View File

@@ -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';
@@ -22,6 +22,9 @@ export const useClickOustideListenerStates = (componentId: string) => {
clickOutsideListenerIsActivatedComponentState,
scopeId,
),
lockedListenerIdState,
getClickOutsideListenerMouseDownHappenedState: extractComponentState(
clickOutsideListenerMouseDownHappenedComponentState,
scopeId,
),
};
};

View File

@@ -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>({
@@ -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(
@@ -148,7 +152,6 @@ export const useClickOutsideListener = (componentId: string) => {
};
return {
scopeId,
useListenClickOutside,
toggleClickOutsideListener,
useRegisterClickOutsideListenerCallback,

View File

@@ -28,6 +28,7 @@ export const useListenClickOutsideV2 = <T extends Element>({
const {
getClickOutsideListenerIsMouseDownInsideState,
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerMouseDownHappenedState,
} = useClickOustideListenerStates(listenerId);
const handleMouseDown = useRecoilCallback(
@@ -37,6 +38,8 @@ export const useListenClickOutsideV2 = <T extends Element>({
.getLoadable(getClickOutsideListenerIsActivatedState)
.getValue();
set(getClickOutsideListenerMouseDownHappenedState, true);
const isListening = clickOutsideListenerIsActivated && enabled;
if (!isListening) {
@@ -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;
@@ -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
@@ -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,

View File

@@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const clickOutsideListenerMouseDownHappenedComponentState =
createComponentState<boolean>({
key: 'clickOutsideListenerMouseDownHappenedComponentState',
defaultValue: false,
});

View File

@@ -1,6 +0,0 @@
import { createState } from 'twenty-ui';
export const lockedListenerIdState = createState<string | null>({
key: 'lockedListenerIdState',
defaultValue: null,
});