isDragging returning true after drop. End callback not being fired immediately after drop
yosef-ian-kurniadi opened this issue · comments
Describe the bug
End callback not being called immediately after dropping a draggable object outside of the drop zone. IsDragging still returns true after the drop, shown by the faded opacity of the card. To get the end callback firing, I need to drop it in the drop zone which accepts the card. In the sandbox however, the end callback would be fired regardless of the position of the drop. I expect the end callback to be invoked like in the sandbox I link here. This is only possible on my PC (not on the sandbox) if and only if I drop it legally.
Reproduction
My code looks similar to this which is what I am trying to reproduce but I am having another bug here which is not related to the title. Yeah I am having difficulties with this difficulties apparently. But yeah I was able to get an almost perfectly working kanban on my work here in PC but not on the sandbox.
https://codesandbox.io/s/loving-wood-pjhc9q
Steps to reproduce the behavior:
I did not get a minified of the version working on the sandbox., despite reusing most of the code there. I may get back to it on another day since I have not finished what I did at the drop callback. But that is something
Expected behavior
It should behave like a vertical kanban board
Screenshots
End callback and isDragging bug
https://github.com/react-dnd/react-dnd/assets/126670492/5f56966c-2a87-4d2c-9be5-0d994c4d7ac4
It works fine when I drop it in its respective drop zone.
End.callback.working.mp4
Here are my relevant code snippets for that
FavoritesCard.tsx
type FavoritesCardType = FavoriteItemFragment & {
index: number
draggable?: boolean
cardType: ItemTypes
moveCard?: MoveCardSignature
dropCard?: DropCardSignature
loading: boolean
}
export type Payload = {
item: FavoriteItemFragment
index: number
cardType: ItemTypes
originalCardType: ItemTypes
}
export const FavoritesCard: React.FC<FavoritesCardType> = ({
index,
cardType,
moveCard,
dropCard,
loading,
draggable = true,
...fav
}) => {
const methods = useUpdateFavoriteForm(fav)
const setHovered = useSetHoveredFavorite()
const onHover = useCallback(() => {
setHovered(fav.pk)
}, [fav.pk, setHovered])
const canEdit = useCanEdit(fav)
const loadingRef = useRef(loading)
loadingRef.current = loading
const [{ isDragging }, drag] = useDrag<
Payload,
unknown,
{ isDragging: boolean }
>(
() => ({
type: cardType,
item: {
item: fav,
index,
cardType: cardType,
originalCardType: cardType,
},
collect: monitor => ({
isDragging: monitor.getItem()?.item?.pk === fav.pk,
}),
canDrag: () => {
return !loadingRef.current && draggable
},
end: () => {
console.log("End Callback called")
},
}),
[index, cardType]
)
const ref = useRef(null)
const [{ handlerId }, drop] = useDrop<
Payload,
unknown,
{ handlerId: string | symbol }
>({
accept: [ItemTypes.OwnFavorites, ItemTypes.SharedFavorites],
collect: monitor => {
return {
handlerId: monitor.getHandlerId(),
}
},
drop: ({ item, originalCardType }) => {
dropCard(item, originalCardType, cardType)
},
hover: (payload, monitor) => {
if (!ref.current) {
return payload.index
}
const dragIndex = payload.index
const dropIndex = index
// Don't replace items with themselves
if (dragIndex === dropIndex && payload.cardType === cardType) return
// Determine the drop zone bounding rect on the client screen
const dropBoundingRect = ref.current?.getBoundingClientRect()
// Get vertical middle of the drop zone
const dropMiddleY = (dropBoundingRect.bottom - dropBoundingRect.top) / 2
// Determine mouse position. It has nothing to do with offset I believe, you can see this as absolute values starting from upper left of the client's screen
const clientOffset = monitor.getClientOffset()
// Distance to the top of the drop zone
const hoverClientY = clientOffset.y - dropBoundingRect.top
//Dragged item (down direction) has not surpassed the middle of the drop zone from above
if (dragIndex < dropIndex && hoverClientY < dropMiddleY) return
//Dragged item (up direction) has not surpassed the middle of the drop zone from below
if (dragIndex > dropIndex && hoverClientY > dropMiddleY) return
// Time to actually perform the action
moveCard(
payload,
{ index: payload.index, cardType: payload.cardType },
{ index, cardType }
)
},
})
drag(drop(ref))
return (
<NLink href={ROUTES.NEWS.MAP} params={{ favoriteId: fav.pk }}>
<Box opacity={isDragging ? 0.5 : 1}>
<Flex
ref={ref}
p="20px"
bgColor="gray.50"
boxShadow="lg"
borderRadius="6px"
borderLeft="4px solid"
borderLeftColor={color}
onMouseEnter={onHover}
cursor="pointer"
_hover={{
boxShadow: "xl",
}}
transform="translate3d(0, 0, 0)"
data-handler-id={handlerId}
my="3"
FavoriteCardList.tsx
export type ListPosition = { index: number; cardType: ItemTypes }
export type MoveCardSignature = (
payload: Payload,
from: ListPosition,
to: ListPosition
) => void
export type DropCardSignature = (
fav: FavoriteItemFragment,
previousCardType: ItemTypes,
currentCardType: ItemTypes
) => void
export const FavoriteCardList: FC = () => {
const { myFavorites, sharedFavorites, moveCard, dropCard, loading } =
useSortedFavoriteLists()
const { data } = useGetMeQuery()
const pk = data?.me?.account?.pk
return (
<NewCheckProvider>
<Flex flexDir="column" maxH="calc(100vh - 108px - 70px)" width="60%">
<THeading
tKey="NEWS.FAVORITES.TITLE"
fontSize="xl"
fontWeight="bold"
lineHeight={7}
mb="57px"
/>
<Flex
gap="0"
flexDir="column"
css={scrollbarStyle.onHoverVisible}
height="100%"
pr={2}
position="relative"
overflowX="hidden"
>
<DndProvider backend={HTML5Backend}>
{!sharedFavorites?.length && !myFavorites?.length && (
<TText tKey="NEWS.FAVORITES.NO_FAVORITES" />
)}
{!!sharedFavorites.length && (
<>
<THeading
tKey="NEWS.FAVORITES.OWN"
size="large"
position="sticky"
top="0"
bg="white"
zIndex="150"
width="110%"
pb={3}
/>
{!myFavorites?.length && (
<DropEmpty
moveCard={moveCard}
sharedZone={false}
dropCard={dropCard}
/>
)}
</>
)}
{myFavorites?.map((card, index) => (
<FavoritesCard
{...{
index,
cardType: ItemTypes.OwnFavorites,
moveCard,
dropCard,
loading,
...card,
}}
key={card.pk}
/>
))}
{!!sharedFavorites.length && (
<>
<THeading
tKey="NEWS.FAVORITES.SHARED"
size="large"
pb={3}
mt={6}
position="sticky"
top={0}
bg="white"
width="110%"
/>
{sharedFavorites?.map((card, index) => (
<FavoritesCard
{...{
index,
cardType: ItemTypes.SharedFavorites,
draggable: pk === card.userId,
moveCard,
dropCard,
loading,
...card,
}}
key={card.pk}
/>
))}
</>
)}
{!sharedFavorites?.length && !!myFavorites?.length && (
<DropEmpty
moveCard={moveCard}
sharedZone={true}
dropCard={dropCard}
/>
)}
</DndProvider>
</Flex>
</Flex>
</NewCheckProvider>
)
}
const DropEmpty: React.FC<{
moveCard: MoveCardSignature
dropCard: DropCardSignature
sharedZone: boolean
}> = ({ moveCard, dropCard, sharedZone }) => {
const ref = useRef(null)
const [{ handlerId: handlerID }, dropRef] = useDrop<
Payload,
unknown,
{ handlerId: string | symbol }
>({
accept: [ItemTypes.OwnFavorites, ItemTypes.SharedFavorites],
collect: monitor => {
return {
handlerId: monitor.getHandlerId(),
}
},
drop: ({ item, originalCardType }) => {
dropCard(
item,
originalCardType,
sharedZone ? ItemTypes.SharedFavorites : ItemTypes.OwnFavorites
)
},
hover: payload => {
if (sharedZone) {
moveCard(
payload,
{ index: payload.index, cardType: payload.cardType },
{ index: 0, cardType: ItemTypes.SharedFavorites }
)
} else {
moveCard(
payload,
{ index: payload.index, cardType: payload.cardType },
{ index: 0, cardType: ItemTypes.OwnFavorites }
)
}
},
})
dropRef(ref)
return (
<Center border="1px dashed" mt="3" ref={ref} data-handler-id={handlerID}>
<TText tKey="NEWS.FAVORITES.NO_SHARED" />
</Center>
)
}
useSortedFavoriteLists.tsx
export const sharedFavsKey = "SharedPkOrderFavorites"
export const ownFavsKey = "OwnPkOrderFavorites"
const saveFavoritesOrder = (favOwnPKs: string[], favSharedPKs: string[]) => {
localStorage.setItem(ownFavsKey, JSON.stringify(favOwnPKs))
localStorage.setItem(sharedFavsKey, JSON.stringify(favSharedPKs))
}
export const useSortedFavoriteLists = () => {
const { myFavorites, sharedFavorites } = useFavoriteLists()
const [localMyFavorites, setMyFavorites] =
useState<FavoriteItemFragment[]>(myFavorites)
const [localSharedFavorites, setSharedFavorites] =
useState<FavoriteItemFragment[]>(sharedFavorites)
useEffect(() => {
setMyFavorites([...myFavorites])
setSharedFavorites([...sharedFavorites])
}, [myFavorites, sharedFavorites])
const [updateFavorite, { loading }] = useUpdateFavoriteMutation({
refetchQueries: [GetFavoritesDocument],
awaitRefetchQueries: true,
})
const dropCard: DropCardSignature = (
fav: FavoriteItemFragment,
previousCardType: ItemTypes,
currentCardType: ItemTypes
) => {
saveFavoritesOrder(
localMyFavorites.map(x => x.pk),
localSharedFavorites.map(x => x.pk)
)
if (previousCardType !== currentCardType) {
updateFavorite({
variables: {
favorite: cleanFavorite({
...fav,
shared: currentCardType === ItemTypes.SharedFavorites,
}),
},
})
}
}
const moveCard: MoveCardSignature = (payload, from, to) => {
if (
from.cardType === to.cardType &&
from.cardType === ItemTypes.OwnFavorites
) {
setMyFavorites(x => {
x[from.index] = x[to.index]
x[to.index] = payload.item
return [...x]
})
payload.index = to.index
} else if (
from.cardType === to.cardType &&
from.cardType === ItemTypes.SharedFavorites
) {
setSharedFavorites(x => {
x[from.index] = x[to.index]
x[to.index] = payload.item
return [...x]
})
payload.index = to.index
} else if (
from.cardType === ItemTypes.OwnFavorites &&
to.cardType === ItemTypes.SharedFavorites
) {
setMyFavorites(x => x.filter((_, index) => index !== from.index))
setSharedFavorites(x => {
x.unshift(payload.item)
return x
})
payload.index = 0
} else if (
from.cardType === ItemTypes.SharedFavorites &&
to.cardType === ItemTypes.OwnFavorites
) {
setMyFavorites(x => {
payload.index = x.push(payload.item) - 1
return x
})
setSharedFavorites(x => x.filter((_, index) => index !== from.index))
}
// Mutating the item parameter object makes my life whole lot easier
// Somehow I need to handle a special case for favorites migrating from SharedFavorites to OwnFavorites.
payload.cardType = to.cardType
}
return {
myFavorites: localMyFavorites,
sharedFavorites: localSharedFavorites,
moveCard,
dropCard,
loading,
}
}
useFavoriteLists.tsx
export const useFavoriteLists = (): TwoLists => {
const { data } = useGetFavoritesQuery()
const status = useFavoritesStatus()
return useMemo(() => {
console.log("refreshing favorites")
const allFavorites = data.news.favorites
if (status === FavoritesStatus.Individual) {
return {
myFavorites: allFavorites,
sharedFavorites: [],
}
}
const myFavorites = allFavorites
.filter(({ shared }) => !shared)
.sort(byLocalStorage(JSON.parse(localStorage.getItem(ownFavsKey))))
const sharedFavorites = allFavorites
.filter(({ shared }) => shared)
.sort(byLocalStorage(JSON.parse(localStorage.getItem(sharedFavsKey))))
return {
myFavorites,
sharedFavorites,
}
}, [data.news.favorites, status])
}
type TwoLists = {
myFavorites: FavoriteItemFragment[]
sharedFavorites: FavoriteItemFragment[]
}
const byLocalStorage =
(order: string[]) => (a: { pk: string }, b: { pk: string }) =>
order?.findIndex(x => x.valueOf() === a.pk.valueOf()) -
order?.findIndex(x => x.valueOf() === b.pk.valueOf()) || 0
Desktop (please complete the following information):
- Windows 10, Chrome,
- Browser : Chrome
- Version : 116
Additional context
I am using Chakra UI, Next.js, React, Apollo for the backend. I am a relatively new front end developer working for a company and thus the code is not open source. I have asked permission however to record the application and also post some code.
You can contact my more experienced colleague@david-morris for more information