unselect all cards using esc key or click (#1393)
* unselect all cards using esc key or click * useScopedHotKeys * useListenClickByClassName * rules are rules * smoothing out || cursor-boxing-selection compliant * replenished activeCardIds * setRecoilState
This commit is contained in:
@ -7,12 +7,15 @@ import { useRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
|
import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
|
||||||
import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress';
|
import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress';
|
||||||
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { BoardHeader } from '@/ui/board/components/BoardHeader';
|
import { BoardHeader } from '@/ui/board/components/BoardHeader';
|
||||||
import { StyledBoard } from '@/ui/board/components/StyledBoard';
|
import { StyledBoard } from '@/ui/board/components/StyledBoard';
|
||||||
import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext';
|
import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext';
|
||||||
import { SelectedSortType } from '@/ui/filter-n-sort/types/interface';
|
import { SelectedSortType } from '@/ui/filter-n-sort/types/interface';
|
||||||
import { IconList } from '@/ui/icon';
|
import { IconList } from '@/ui/icon';
|
||||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
import {
|
import {
|
||||||
@ -22,6 +25,7 @@ import {
|
|||||||
useUpdateOnePipelineProgressStageMutation,
|
useUpdateOnePipelineProgressStageMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
|
import { useCurrentCardSelected } from '../hooks/useCurrentCardSelected';
|
||||||
import { useSetCardSelected } from '../hooks/useSetCardSelected';
|
import { useSetCardSelected } from '../hooks/useSetCardSelected';
|
||||||
import { useUpdateBoardCardIds } from '../hooks/useUpdateBoardCardIds';
|
import { useUpdateBoardCardIds } from '../hooks/useUpdateBoardCardIds';
|
||||||
import { boardColumnsState } from '../states/boardColumnsState';
|
import { boardColumnsState } from '../states/boardColumnsState';
|
||||||
@ -54,6 +58,8 @@ export function EntityBoard({
|
|||||||
const [updatePipelineProgressStage] =
|
const [updatePipelineProgressStage] =
|
||||||
useUpdateOnePipelineProgressStageMutation();
|
useUpdateOnePipelineProgressStageMutation();
|
||||||
|
|
||||||
|
const { unselectAllActiveCards } = useCurrentCardSelected();
|
||||||
|
|
||||||
const updatePipelineProgressStageInDB = useCallback(
|
const updatePipelineProgressStageInDB = useCallback(
|
||||||
async (
|
async (
|
||||||
pipelineProgressId: NonNullable<PipelineProgress['id']>,
|
pipelineProgressId: NonNullable<PipelineProgress['id']>,
|
||||||
@ -70,6 +76,11 @@ export function EntityBoard({
|
|||||||
[updatePipelineProgressStage],
|
[updatePipelineProgressStage],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useListenClickOutsideByClassName({
|
||||||
|
className: 'entity-board-card',
|
||||||
|
callback: unselectAllActiveCards,
|
||||||
|
});
|
||||||
|
|
||||||
const updateBoardCardIds = useUpdateBoardCardIds();
|
const updateBoardCardIds = useUpdateBoardCardIds();
|
||||||
|
|
||||||
const onDragEnd: OnDragEndResponder = useCallback(
|
const onDragEnd: OnDragEndResponder = useCallback(
|
||||||
@ -106,6 +117,12 @@ export function EntityBoard({
|
|||||||
|
|
||||||
const boardRef = useRef<HTMLDivElement>(null);
|
const boardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
'escape',
|
||||||
|
unselectAllActiveCards,
|
||||||
|
PageHotkeyScope.OpportunitiesPage,
|
||||||
|
);
|
||||||
|
|
||||||
return (boardColumns?.length ?? 0) > 0 ? (
|
return (boardColumns?.length ?? 0) > 0 ? (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export function EntityBoardCard({
|
|||||||
ref={draggableProvided?.innerRef}
|
ref={draggableProvided?.innerRef}
|
||||||
{...draggableProvided?.dragHandleProps}
|
{...draggableProvided?.dragHandleProps}
|
||||||
{...draggableProvided?.draggableProps}
|
{...draggableProvided?.draggableProps}
|
||||||
|
className="entity-board-card"
|
||||||
data-selectable-id={cardId}
|
data-selectable-id={cardId}
|
||||||
data-select-disable
|
data-select-disable
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { actionBarOpenState } from '@/ui/action-bar/states/actionBarIsOpenState';
|
import { actionBarOpenState } from '@/ui/action-bar/states/actionBarIsOpenState';
|
||||||
|
|
||||||
import { BoardCardIdContext } from '../contexts/BoardCardIdContext';
|
import { BoardCardIdContext } from '../contexts/BoardCardIdContext';
|
||||||
|
import { activeCardIdsState } from '../states/activeCardIdsState';
|
||||||
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
|
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
|
||||||
|
|
||||||
export function useCurrentCardSelected() {
|
export function useCurrentCardSelected() {
|
||||||
@ -13,19 +14,48 @@ export function useCurrentCardSelected() {
|
|||||||
isCardSelectedFamilyState(currentCardId ?? ''),
|
isCardSelectedFamilyState(currentCardId ?? ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setActiveCardIds = useSetRecoilState(activeCardIdsState);
|
||||||
|
|
||||||
const setCurrentCardSelected = useRecoilCallback(
|
const setCurrentCardSelected = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
(selected: boolean) => {
|
(selected: boolean) => {
|
||||||
if (!currentCardId) return;
|
if (!currentCardId) return;
|
||||||
|
|
||||||
set(isCardSelectedFamilyState(currentCardId), selected);
|
set(isCardSelectedFamilyState(currentCardId), selected);
|
||||||
set(actionBarOpenState, true);
|
set(actionBarOpenState, selected);
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
setActiveCardIds((prevActiveCardIds) => [
|
||||||
|
...prevActiveCardIds,
|
||||||
|
currentCardId,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setActiveCardIds((prevActiveCardIds) =>
|
||||||
|
prevActiveCardIds.filter((id) => id !== currentCardId),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[currentCardId],
|
[currentCardId, setActiveCardIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const unselectAllActiveCards = useRecoilCallback(
|
||||||
|
({ set, snapshot }) =>
|
||||||
|
() => {
|
||||||
|
const activeCardIds = snapshot.getLoadable(activeCardIdsState).contents;
|
||||||
|
|
||||||
|
activeCardIds.forEach((cardId: string) => {
|
||||||
|
set(isCardSelectedFamilyState(cardId), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
set(activeCardIdsState, []);
|
||||||
|
set(actionBarOpenState, false);
|
||||||
|
},
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentCardSelected: isCardSelected,
|
currentCardSelected: isCardSelected,
|
||||||
setCurrentCardSelected,
|
setCurrentCardSelected,
|
||||||
|
unselectAllActiveCards,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,28 @@ import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { actionBarOpenState } from '@/ui/action-bar/states/actionBarIsOpenState';
|
import { actionBarOpenState } from '@/ui/action-bar/states/actionBarIsOpenState';
|
||||||
|
|
||||||
|
import { activeCardIdsState } from '../states/activeCardIdsState';
|
||||||
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
|
import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState';
|
||||||
|
|
||||||
export function useSetCardSelected() {
|
export function useSetCardSelected() {
|
||||||
const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
|
const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
|
||||||
|
|
||||||
return useRecoilCallback(({ set }) => (cardId: string, selected: boolean) => {
|
return useRecoilCallback(
|
||||||
set(isCardSelectedFamilyState(cardId), selected);
|
({ set, snapshot }) =>
|
||||||
setActionBarOpenState(true);
|
(cardId: string, selected: boolean) => {
|
||||||
});
|
const activeCardIds = snapshot.getLoadable(activeCardIdsState).contents;
|
||||||
|
|
||||||
|
set(isCardSelectedFamilyState(cardId), selected);
|
||||||
|
setActionBarOpenState(selected || activeCardIds.length > 0);
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
set(activeCardIdsState, [...activeCardIds, cardId]);
|
||||||
|
} else {
|
||||||
|
set(
|
||||||
|
activeCardIdsState,
|
||||||
|
activeCardIds.filter((id: string) => id !== cardId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
6
front/src/modules/ui/board/states/activeCardIdsState.ts
Normal file
6
front/src/modules/ui/board/states/activeCardIdsState.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const activeCardIdsState = atom<string[]>({
|
||||||
|
key: 'activeCardIdsState',
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
@ -76,3 +76,38 @@ export function useListenClickOutside<T extends Element>({
|
|||||||
};
|
};
|
||||||
}, [refs, callback, mode]);
|
}, [refs, callback, mode]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useListenClickOutsideByClassName = ({
|
||||||
|
className,
|
||||||
|
callback,
|
||||||
|
}: {
|
||||||
|
className: string;
|
||||||
|
callback: () => void;
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const clickedElement = event.target as HTMLElement;
|
||||||
|
let isClickedInside = false;
|
||||||
|
let currentElement: HTMLElement | null = clickedElement;
|
||||||
|
|
||||||
|
// Check if the clicked element or any of its parent elements have the specified class
|
||||||
|
while (currentElement) {
|
||||||
|
if (currentElement.classList.contains(className)) {
|
||||||
|
isClickedInside = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentElement = currentElement.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isClickedInside) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [callback, className]);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user