Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: define container for drag selection #1481

Merged
merged 4 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 27 additions & 20 deletions src/components/item/FolderContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import {
SelectionContextProvider,
useSelectionContext,
} from '../main/list/SelectionContext';
import { useDragSelection } from '../main/list/useDragSelection';
import {
DragContainerStack,
useDragSelection,
} from '../main/list/useDragSelection';
import { DesktopMap } from '../map/DesktopMap';
import NoItemFilters from '../pages/NoItemFilters';
import { OutletType } from '../pages/item/type';
Expand All @@ -49,6 +52,8 @@ type Props = {
canWrite?: boolean;
};

const CONTAINER_ID = 'items-container-id';

const Content = ({
item,
searchText,
Expand All @@ -60,7 +65,7 @@ const Content = ({
const { itemTypes } = useFilterItemsContext();
const { selectedIds, clearSelection, toggleSelection } =
useSelectionContext();
const DragSelection = useDragSelection();
const DragSelection = useDragSelection({ containerId: CONTAINER_ID });

if (mode === ItemLayoutMode.Map) {
return (
Expand All @@ -73,24 +78,26 @@ const Content = ({
if (items?.length) {
return (
<>
<ItemsTable
selectedIds={selectedIds}
enableMoveInBetween={sortBy === SortingOptionsForFolder.Order}
id={buildItemsTableId(item.id)}
items={items ?? []}
onCardClick={toggleSelection}
onMove={clearSelection}
/>
{Boolean(canWrite && !searchText && !itemTypes?.length) && (
<Stack alignItems="center" mb={2}>
<NewItemButton
type="icon"
key="newButton"
// add new items at the end of the list
previousItemId={items ? items[items.length - 1]?.id : undefined}
/>
</Stack>
)}
<DragContainerStack id={CONTAINER_ID}>
<ItemsTable
selectedIds={selectedIds}
enableMoveInBetween={sortBy === SortingOptionsForFolder.Order}
id={buildItemsTableId(item.id)}
items={items ?? []}
onCardClick={toggleSelection}
onMove={clearSelection}
/>
{Boolean(canWrite && !searchText && !itemTypes?.length) && (
<Stack alignItems="center" mb={2}>
<NewItemButton
type="icon"
key="newButton"
// add new items at the end of the list
previousItemId={items ? items[items.length - 1]?.id : undefined}
/>
</Stack>
)}
</DragContainerStack>
{DragSelection}
</>
);
Expand Down
11 changes: 8 additions & 3 deletions src/components/main/list/ItemsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,16 @@ const ItemsTable = ({
return;
}

// silent error, happens when you want to cancel the operation
if (movedItem.id === targetItem.id) {
console.error('cannot move target into itself');
return;
}

// cannot move item into itself, or target cannot be part of selection if moving selection
if (
movedItem.id === targetItem.id ||
(selectedIds.includes(movedItem?.id) &&
selectedIds.includes(targetItem.id))
selectedIds.includes(movedItem?.id) &&
selectedIds.includes(targetItem.id)
) {
toast.error(translateMessage(FAILURE_MESSAGES.INVALID_MOVE_TARGET));
return;
Expand Down
6 changes: 5 additions & 1 deletion src/components/main/list/SelectionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
useState,
} from 'react';

import { Stack } from '@mui/material';

type SelectionContextValue = {
selectedIds: string[];
toggleSelection: (id: string) => void;
Expand Down Expand Up @@ -74,7 +76,9 @@ export const SelectionContextProvider = ({

return (
<SelectionContext.Provider value={value}>
<div ref={elementsContainerRef}>{children}</div>
<Stack height="100%" ref={elementsContainerRef}>
{children}
</Stack>
</SelectionContext.Provider>
);
};
Expand Down
39 changes: 36 additions & 3 deletions src/components/main/list/useDragSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState } from 'react';
import { ReactNode, useState } from 'react';

import { Stack } from '@mui/material';

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

Expand All @@ -12,9 +14,35 @@ import { ITEM_CARD_CLASS } from '@/config/selectors';

import { useSelectionContext } from './SelectionContext';

export const DragContainerStack = ({
gap,
id,
children,
}: {
gap?: number;
id: string;
children: ReactNode;
}): JSX.Element => (
<Stack
// this is a hack to allow selection dragging from margin
// 100 is artbitrary big
mx={-100}
px={100}
height="100%"
id={id}
gap={gap}
>
{children}
</Stack>
);

export const useDragSelection = ({
elementClass = ITEM_CARD_CLASS,
} = {}): JSX.Element => {
containerId,
}: {
containerId: string;
elementClass?: string;
}): JSX.Element => {
const { addToSelection, clearSelection } = useSelectionContext();
const [boundingBox, setBoundingBox] = useState<null | {
top: number;
Expand Down Expand Up @@ -52,8 +80,13 @@ export const useDragSelection = ({
);
},
shouldStartSelecting: (e) => {
// does not trigger drag selection if mousedown on card
if (e instanceof HTMLElement || e instanceof SVGElement) {
// does not trigger if click is outside of container
if (!e.closest(`#${containerId}`)) {
return false;
}

// does not trigger drag selection if mousedown on card
return !e?.closest(`.${elementClass}`);
}
return true;
Expand Down
25 changes: 18 additions & 7 deletions src/components/pages/PageWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,25 @@ const PageWrapper = ({
<Helmet>
<title>{title}</title>
</Helmet>
<Container id={id} sx={{ my: 2 }}>
<Stack mb={2} direction="row" justifyContent="space-between" spacing={1}>
<Typography variant="h2" component="h1" sx={{ wordWrap: 'break-word' }}>
{title}
</Typography>
{options}
<Container id={id} sx={{ pt: 1, height: '100%' }}>
<Stack height="100%">
<Stack
mb={2}
direction="row"
justifyContent="space-between"
spacing={1}
>
<Typography
variant="h2"
component="h1"
sx={{ wordWrap: 'break-word' }}
>
{title}
</Typography>
{options}
</Stack>
{children}
</Stack>
{children}
</Container>
</>
);
Expand Down
65 changes: 36 additions & 29 deletions src/components/pages/RecycledItemsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ import {
SelectionContextProvider,
useSelectionContext,
} from '../main/list/SelectionContext';
import { useDragSelection } from '../main/list/useDragSelection';
import {
DragContainerStack,
useDragSelection,
} from '../main/list/useDragSelection';
import ItemCard from '../table/ItemCard';
import SortingSelect from '../table/SortingSelect';
import { SortingOptions } from '../table/types';
import { useSorting, useTranslatedSortingOptions } from '../table/useSorting';
import PageWrapper from './PageWrapper';
import RecycleBinToolbar from './recycleBin/RecycleBinSelectionToolbar';

const CONTAINER_ID = 'recycle-items-container';

const RecycledItemsScreenContent = ({
searchText,
}: {
Expand All @@ -52,14 +57,14 @@ const RecycledItemsScreenContent = ({
?.sort(sortFn);
const { selectedIds, toggleSelection } = useSelectionContext();

const DragSelection = useDragSelection();
const DragSelection = useDragSelection({ containerId: CONTAINER_ID });

// render this when there is data from the query
if (recycledItems?.length) {
const hasSelection = selectedIds.length && filteredData?.length;
return (
<>
<Stack gap={1}>
<Stack gap={1} height="100%">
<Stack
alignItems="space-between"
direction="column"
Expand Down Expand Up @@ -91,32 +96,34 @@ const RecycledItemsScreenContent = ({
</Stack>
)}
</Stack>
{
// render the filtered data and when it is empty display that nothing matches the search
filteredData?.length ? (
filteredData.map((item) => (
<ItemCard
item={item}
onThumbnailClick={() => toggleSelection(item.id)}
isSelected={selectedIds.includes(item.id)}
showThumbnail={false}
allowNavigation={false}
footer={
<Stack justifyContent="right" direction="row">
<RestoreButton itemIds={[item.id]} />
<DeleteButton items={[item]} />
</Stack>
}
/>
))
) : (
<Alert severity="info">
{translateBuilder(BUILDER.TRASH_NO_ITEM_SEARCH, {
search: searchText,
})}
</Alert>
)
}
<DragContainerStack id={CONTAINER_ID}>
{
// render the filtered data and when it is empty display that nothing matches the search
filteredData?.length ? (
filteredData.map((item) => (
<ItemCard
item={item}
onThumbnailClick={() => toggleSelection(item.id)}
isSelected={selectedIds.includes(item.id)}
showThumbnail={false}
allowNavigation={false}
footer={
<Stack justifyContent="right" direction="row">
<RestoreButton itemIds={[item.id]} />
<DeleteButton items={[item]} />
</Stack>
}
/>
))
) : (
<Alert severity="info">
{translateBuilder(BUILDER.TRASH_NO_ITEM_SEARCH, {
search: searchText,
})}
</Alert>
)
}
</DragContainerStack>
</Stack>
{DragSelection}
</>
Expand Down
15 changes: 10 additions & 5 deletions src/components/pages/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
SelectionContextProvider,
useSelectionContext,
} from '@/components/main/list/SelectionContext';
import { useDragSelection } from '@/components/main/list/useDragSelection';
import {
DragContainerStack,
useDragSelection,
} from '@/components/main/list/useDragSelection';
import { ITEM_PAGE_SIZE } from '@/config/constants';
import { ShowOnlyMeChangeType } from '@/config/types';
import { ItemLayoutMode, Ordering } from '@/enums';
Expand Down Expand Up @@ -46,6 +49,8 @@ import NoItemFilters from '../NoItemFilters';
import PageWrapper from '../PageWrapper';
import HomeSelectionToolbar from './HomeSelectionToolbar';

const CONTAINER_ID = 'home-items-container';

const HomeScreenContent = ({ searchText }: { searchText: string }) => {
const { t: translateBuilder } = useBuilderTranslation();
const { t: translateEnums } = useEnumsTranslation();
Expand Down Expand Up @@ -75,7 +80,7 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
{ pageSize: ITEM_PAGE_SIZE },
);

const DragSelection = useDragSelection();
const DragSelection = useDragSelection({ containerId: CONTAINER_ID });

const onShowOnlyMeChange: ShowOnlyMeChangeType = (checked) => {
setShowOnlyMe(checked);
Expand Down Expand Up @@ -105,7 +110,7 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
? data.pages.map(({ data: d }) => d.length).reduce((a, b) => a + b, 0)
: 0;
content = (
<>
<DragContainerStack id={CONTAINER_ID}>
<ItemsTable
canMove={!searchText}
id={ACCESSIBLE_ITEMS_TABLE_ID}
Expand All @@ -128,7 +133,7 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
<NewItemButton type="icon" />
</Stack>
)}
</>
</DragContainerStack>
);
} else if (itemTypes.length || searchText) {
content = <NoItemFilters searchText={searchText} />;
Expand Down Expand Up @@ -182,7 +187,7 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
</Stack>
)}
</Stack>
<Stack>
<Stack height="100%">
{content}
{data && isFetching && (
<Box sx={{ width: '100%' }}>
Expand Down