Skip to content

Commit

Permalink
fix: prevent modal calls on selection (#1387)
Browse files Browse the repository at this point in the history
* fix: prevent modal calls on selection

* refactor: use custom overlay for selection
  • Loading branch information
pyphilia committed Jul 31, 2024
1 parent 4ac1f83 commit f78bb18
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 124 deletions.
3 changes: 3 additions & 0 deletions src/components/item/FolderContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
SelectionContextProvider,
useSelectionContext,
} from '../main/list/SelectionContext';
import { useDragSelection } from '../main/list/useDragSelection';
import { DesktopMap } from '../map/DesktopMap';
import NoItemFilters from '../pages/NoItemFilters';
import SortingSelect from '../table/SortingSelect';
Expand All @@ -49,6 +50,7 @@ const Content = ({ item, searchText, items, sortBy }: Props) => {
const { itemTypes } = useFilterItemsContext();
const { selectedIds, clearSelection, toggleSelection } =
useSelectionContext();
const DragSelection = useDragSelection();

const enableEditing = item.permission
? PermissionLevelCompare.lte(PermissionLevel.Write, item.permission)
Expand Down Expand Up @@ -86,6 +88,7 @@ const Content = ({ item, searchText, items, sortBy }: Props) => {
/>
</Stack>
)}
{DragSelection}
</>
);
}
Expand Down
7 changes: 6 additions & 1 deletion src/components/item/copy/CopyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const CopyModal = ({
open: boolean;
onClose: () => void;
itemIds: DiscriminatedItem['id'][];
}): JSX.Element => {
}): JSX.Element | null => {
const { mutate: copyItems } = mutations.useCopyItems();
const { t: translateBuilder } = useBuilderTranslation();

Expand All @@ -36,6 +36,11 @@ export const CopyModal = ({
name,
});

// prevent loading if not opened
if (!open) {
return null;
}

return (
<ItemSelectionModal
titleKey={BUILDER.COPY_ITEM_MODAL_TITLE}
Expand Down
5 changes: 5 additions & 0 deletions src/components/item/move/MoveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export const MoveModal = ({
name,
});

// prevent loading if not opened
if (!open) {
return null;
}

return items ? (
<ItemSelectionModal
titleKey={BUILDER.MOVE_ITEM_MODAL_TITLE}
Expand Down
81 changes: 17 additions & 64 deletions src/components/main/list/SelectionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,24 @@ import {
useState,
} from 'react';

import { PRIMARY_COLOR } from '@graasp/ui';

import {
Box,
boxesIntersect,
useSelectionContainer,
} from '@air/react-drag-to-select';

import { ITEM_CARD_CLASS } from '@/config/selectors';

type SelectionContextValue = {
selectedIds: string[];
toggleSelection: (id: string) => void;
addToSelection: (id: string) => void;
clearSelection: () => void;
};

export const SelectionContext = createContext<SelectionContextValue>({
selectedIds: [],
toggleSelection: () => {},
addToSelection: () => {},
clearSelection: () => {},
});

export const SelectionContextProvider = ({
children,
elementClass = ITEM_CARD_CLASS,
}: {
children: JSX.Element;
elementClass?: string;
}): JSX.Element => {
const [selection, setSelection] = useState(new Set<string>());
const elementsContainerRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -55,72 +45,35 @@ export const SelectionContextProvider = ({
[selection],
);

const { DragSelection } = useSelectionContainer({
eventsElement: document.getElementById('root'),
onSelectionChange: (box) => {
/**
* Here we make sure to adjust the box's left and top with the scroll position of the window
* @see https://github.com/AirLabsTeam/react-drag-to-select/#scrolling
*/
const scrollAwareBox: Box = {
...box,
top: box.top + window.scrollY,
left: box.left + window.scrollX,
};

Array.from(document.getElementsByClassName(elementClass)).forEach(
(item) => {
const bb = item.getBoundingClientRect();
if (
boxesIntersect(scrollAwareBox, bb) &&
item.parentNode instanceof HTMLElement
) {
const itemId = item.parentNode.dataset.id;
if (itemId) {
selection.add(itemId);
}
}
},
);

setSelection(new Set(selection));
},
shouldStartSelecting: (e) => {
// does not trigger drag selection if mousedown on card
if (e instanceof HTMLElement) {
return !e?.closest(`.${ITEM_CARD_CLASS}`);
const addToSelection = useCallback(
(id: string) => {
if (!selection.has(id)) {
selection.add(id);
setSelection(new Set(selection));
}
return true;
},
onSelectionStart: () => {
// clear selection on new dragging action
clearSelection();
},
onSelectionEnd: () => {},
selectionProps: {
style: {
border: `2px dashed ${PRIMARY_COLOR}`,
borderRadius: 4,
backgroundColor: 'lightblue',
opacity: 0.5,
},
},
isEnabled: true,
});
[selection],
);

const value: SelectionContextValue = useMemo(
() => ({
selectedIds: [...selection.values()],
toggleSelection,
clearSelection,
elementsContainerRef,
addToSelection,
}),
[selection, toggleSelection, clearSelection, elementsContainerRef],
[
selection,
toggleSelection,
clearSelection,
addToSelection,
elementsContainerRef,
],
);

return (
<SelectionContext.Provider value={value}>
<DragSelection />
<div ref={elementsContainerRef}>{children}</div>
</SelectionContext.Provider>
);
Expand Down
98 changes: 98 additions & 0 deletions src/components/main/list/useDragSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useState } from 'react';

import { PRIMARY_COLOR } from '@graasp/ui';

import {
Box,
boxesIntersect,
useSelectionContainer,
} from '@air/react-drag-to-select';

import { ITEM_CARD_CLASS } from '@/config/selectors';

import { useSelectionContext } from './SelectionContext';

export const useDragSelection = ({
elementClass = ITEM_CARD_CLASS,
} = {}): JSX.Element => {
const { addToSelection, clearSelection } = useSelectionContext();
const [boundingBox, setBoundingBox] = useState<null | {
top: number;
left: number;
}>(null);

const { DragSelection: HookComponent } = useSelectionContainer({
eventsElement: document.getElementById('root'),
onSelectionChange: (box) => {
/**
* Here we make sure to adjust the box's left and top with the scroll position of the window
* @see https://github.com/AirLabsTeam/react-drag-to-select/#scrolling
*/
const scrollAwareBox: Box = {
...box,
top: box.top + window.scrollY,
left: box.left + window.scrollX,
};

setBoundingBox(scrollAwareBox);

Array.from(document.getElementsByClassName(elementClass)).forEach(
(item) => {
const bb = item.getBoundingClientRect();
if (
boxesIntersect(scrollAwareBox, bb) &&
item.parentNode instanceof HTMLElement
) {
const itemId = item.parentNode.dataset.id;
if (itemId) {
addToSelection(itemId);
}
}
},
);
},
shouldStartSelecting: (e) => {
// does not trigger drag selection if mousedown on card
if (e instanceof HTMLElement) {
return !e?.closest(`.${elementClass}`);
}
return true;
},
onSelectionStart: () => {
// clear selection on new dragging action
clearSelection();
setBoundingBox(null);
},
onSelectionEnd: () => {
setBoundingBox(null);
},
selectionProps: {
style: {
display: 'none',
},
},
isEnabled: true,
});

// we don't use native overlay because it's bound to the wrapper
// https://github.com/AirLabsTeam/react-drag-to-select/issues/30
const DragSelection = (
<>
<div
style={{
position: 'fixed',
border: `2px dashed ${PRIMARY_COLOR}`,
borderRadius: 4,
backgroundColor: 'lightblue',
opacity: 0.5,
zIndex: 999,
display: boundingBox?.top ? 'block' : 'none',
...boundingBox,
}}
/>
<HookComponent />
</>
);

return DragSelection;
};
Loading

0 comments on commit f78bb18

Please sign in to comment.