react-dnd / react-dnd

Drag and Drop for React

Home Page:http://react-dnd.github.io/react-dnd

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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