react-use-gesture#useGesture TypeScript Examples

The following examples show how to use react-use-gesture#useGesture. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: drag-handle.component.tsx    From react-sortful with MIT License 6 votes vote down vote up
DragHandleComponent = (props: Props) => {
  const itemContext = React.useContext(ItemContext);

  // Checks `props.children` has one React node.
  React.useEffect(() => {
    React.Children.only(props.children);
  }, [props.children]);

  const draggableBinder = useGesture({
    onDragStart: (state: any) => {
      if (itemContext.isLocked) return;

      const event: React.SyntheticEvent = state.event;
      event.persist();
      event.stopPropagation();

      itemContext.dragHandlers.onDragStart();
    },
    onDrag: ({ down, movement }) => {
      if (itemContext.isLocked) return;

      itemContext.dragHandlers.onDrag(down, movement);
    },
    onDragEnd: () => {
      if (itemContext.isLocked) return;

      itemContext.dragHandlers.onDragEnd();
    },
  });

  return (
    <div className={props.className} {...draggableBinder()}>
      {props.children}
    </div>
  );
}
Example #2
Source File: LeftBar.tsx    From binaural-meet with GNU General Public License v3.0 5 votes vote down vote up
LeftBar: React.FC<BMProps> = (props) => {
  const classes = styleForSplit()
  const [scale, doSetScale] = useState<number>(1)
  const setScale = (scale:number) => {
    Object.assign(textLineStyle, defaultTextLineHeight)
    textLineStyle.fontSize *= scale
    textLineStyle.lineHeight *= scale
    doSetScale(scale)
  }

  const bind = useGesture(
    {
      onPinch: ({da: [d, a], origin, event, memo}) => {
        if (memo === undefined) {
          return [d, a]
        }
        const [md] = memo

        const MIN_D = 10
        const scaleChange = d > MIN_D ? d / md : d <  -MIN_D ? md / d : (1 + (d - md) / MIN_D)
        setScale(limitScale(scale, scaleChange))
        //  console.log(`Pinch: da:${[d, a]} origin:${origin}  memo:${memo}  scale:${scale}`)

        return [d, a]
      },
    },
    {
      eventOptions:{passive:false}, //  This prevents default zoom by browser when pinch.
    },
  )


  return (
    <div {...bind()}>
      <SplitPane split="horizontal" defaultSize="80%" resizerClassName = {classes.resizerHorizontal}
        paneStyle = {{overflowY: 'auto', overflowX: 'hidden', width:'100%'}} >
        <SplitPane split="horizontal" defaultSize="50%" resizerClassName = {classes.resizerHorizontal}
          paneStyle = {{overflowY: 'auto', overflowX: 'hidden', width:'100%'}} >
          <ParticipantList {...props} {...textLineStyle} />
          <ContentList {...props}  {...textLineStyle} />
        </SplitPane >
        <ChatInBar {...props}  {...textLineStyle} />
      </SplitPane >
    </div>
  )
}
Example #3
Source File: index.tsx    From cuiswap with GNU General Public License v3.0 5 votes vote down vote up
export default function Modal({
  isOpen,
  onDismiss,
  minHeight = false,
  maxHeight = 50,
  initialFocusRef = null,
  children
}: ModalProps) {
  const fadeTransition = useTransition(isOpen, null, {
    config: { duration: 200 },
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 }
  })

  const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
  const bind = useGesture({
    onDrag: state => {
      set({
        y: state.down ? state.movement[1] : 0
      })
      if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) {
        onDismiss()
      }
    }
  })

  return (
    <>
      {fadeTransition.map(
        ({ item, key, props }) =>
          item && (
            <StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
              <StyledDialogContent
                {...(isMobile
                  ? {
                      ...bind(),
                      style: { transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`) }
                    }
                  : {})}
                aria-label="dialog content"
                minHeight={minHeight}
                maxHeight={maxHeight}
                mobile={isMobile}
              >
                {/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
                {!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
                {children}
              </StyledDialogContent>
            </StyledDialogOverlay>
          )
      )}
    </>
  )
}
Example #4
Source File: index.tsx    From sybil-interface with GNU General Public License v3.0 5 votes vote down vote up
export default function Modal({
  isOpen,
  onDismiss,
  minHeight = false,
  maxHeight = 90,
  initialFocusRef,
  children,
}: ModalProps): JSX.Element {
  const fadeTransition = useTransition(isOpen, null, {
    config: { duration: 200 },
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 },
  })

  const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
  const bind = useGesture({
    onDrag: (state) => {
      set({
        y: state.down ? state.movement[1] : 0,
      })
      if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) {
        onDismiss()
      }
    },
  })

  return (
    <>
      {fadeTransition.map(
        ({ item, key, props }) =>
          item && (
            <StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
              <StyledDialogContent
                {...(isMobile
                  ? {
                      ...bind(),
                      style: { transform: y.interpolate((y: any) => `translateY(${y > 0 ? y : 0}px)`) },
                    }
                  : {})}
                aria-label="dialog content"
                minHeight={minHeight}
                maxHeight={maxHeight}
                mobile={isMobile}
              >
                {/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
                {!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
                {children}
              </StyledDialogContent>
            </StyledDialogOverlay>
          )
      )}
    </>
  )
}
Example #5
Source File: index.tsx    From dyp with Do What The F*ck You Want To Public License 5 votes vote down vote up
export default function Modal({
  isOpen,
  onDismiss,
  minHeight = false,
  maxHeight = 90,
  initialFocusRef,
  children
}: ModalProps) {
  const fadeTransition = useTransition(isOpen, null, {
    config: { duration: 200 },
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 }
  })

  const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
  const bind = useGesture({
    onDrag: state => {
      set({
        y: state.down ? state.movement[1] : 0
      })
      if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) {
        onDismiss()
      }
    }
  })

  return (
    <>
      {fadeTransition.map(
        ({ item, key, props }) =>
          item && (
            <StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
              <StyledDialogContent
                {...(isMobile
                  ? {
                      ...bind(),
                      style: { transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`) }
                    }
                  : {})}
                aria-label="dialog content"
                minHeight={minHeight}
                maxHeight={maxHeight}
                mobile={isMobile}
              >
                {/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
                {!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
                {children}
              </StyledDialogContent>
            </StyledDialogOverlay>
          )
      )}
    </>
  )
}
Example #6
Source File: index.tsx    From limit-orders-lib with GNU General Public License v3.0 5 votes vote down vote up
export default function Modal({
  isOpen,
  onDismiss,
  minHeight = false,
  maxHeight = 90,
  initialFocusRef,
  children,
}: ModalProps) {
  const fadeTransition = useTransition(isOpen, null, {
    config: { duration: 200 },
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 },
  });

  const [{ y }, set] = useSpring(() => ({
    y: 0,
    config: { mass: 1, tension: 210, friction: 20 },
  }));
  const bind = useGesture({
    onDrag: (state) => {
      set({
        y: state.down ? state.movement[1] : 0,
      });
      if (
        state.movement[1] > 300 ||
        (state.velocity > 3 && state.direction[1] > 0)
      ) {
        onDismiss();
      }
    },
  });

  return (
    <Fragment>
      {fadeTransition.map(
        ({ item, key, props }) =>
          item && (
            <StyledDialogOverlay
              key={key}
              style={props}
              onDismiss={onDismiss}
              initialFocusRef={initialFocusRef}
              unstable_lockFocusAcrossFrames={false}
            >
              <StyledDialogContent
                {...(isMobile
                  ? {
                      ...bind(),
                      style: {
                        transform: y.interpolate(
                          (y) => `translateY(${(y as number) > 0 ? y : 0}px)`
                        ),
                      },
                    }
                  : {})}
                aria-label="dialog content"
                minHeight={minHeight}
                maxHeight={maxHeight}
                mobile={isMobile}
              >
                {/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
                {!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
                {children}
              </StyledDialogContent>
            </StyledDialogOverlay>
          )
      )}
    </Fragment>
  );
}
Example #7
Source File: index.tsx    From sushiswap-exchange with GNU General Public License v3.0 5 votes vote down vote up
export default function Modal({
  isOpen,
  onDismiss,
  minHeight = false,
  maxHeight = 50,
  initialFocusRef,
  children
}: ModalProps) {
  const fadeTransition = useTransition(isOpen, null, {
    config: { duration: 200 },
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 }
  })

  const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
  const bind = useGesture({
    onDrag: state => {
      set({
        y: state.down ? state.movement[1] : 0
      })
      if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) {
        onDismiss()
      }
    }
  })

  return (
    <>
      {fadeTransition.map(
        ({ item, key, props }) =>
          item && (
            <StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
              <StyledDialogContent
                {...(isMobile
                  ? {
                      ...bind(),
                      style: { transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`) }
                    }
                  : {})}
                aria-label="dialog content"
                minHeight={minHeight}
                maxHeight={maxHeight}
                mobile={isMobile}
              >
                {/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
                {!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
                {children}
              </StyledDialogContent>
            </StyledDialogOverlay>
          )
      )}
    </>
  )
}
Example #8
Source File: index.tsx    From luaswap-interface with GNU General Public License v3.0 5 votes vote down vote up
export default function Modal({
  isOpen,
  onDismiss = () => {},
  minHeight = false,
  maxHeight = 90,
  initialFocusRef,
  children
}: ModalProps) {
  const fadeTransition = useTransition(isOpen, null, {
    config: { duration: 200 },
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 }
  })

  const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
  const bind = useGesture({
    onDrag: state => {
      set({
        y: state.down ? state.movement[1] : 0
      })
      if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) {
        onDismiss()
      }
    }
  })

  return (
    <>
      {fadeTransition.map(
        ({ item, key, props }) =>
          item && (
            <StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
              <StyledDialogContent
                {...(isMobile
                  ? {
                      ...bind(),
                      style: { transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`) }
                    }
                  : {})}
                aria-label="dialog content"
                minHeight={minHeight}
                maxHeight={maxHeight}
                mobile={isMobile}
              >
                {/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
                {!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
                {children}
              </StyledDialogContent>
            </StyledDialogOverlay>
          )
      )}
    </>
  )
}
Example #9
Source File: Base.tsx    From binaural-meet with GNU General Public License v3.0 4 votes vote down vote up
Base: React.FC<MapProps> = (props: MapProps) => {
  const {map, participants} = props.stores
  const matrix = useObserver(() => map.matrix)
  const container = useRef<HTMLDivElement>(null)
  const outer = useRef<HTMLDivElement>(null)
  function offset():[number, number] {
    return map.offset
  }
  const thirdPersonView = useObserver(() => participants.local.thirdPersonView)
  const memRef = useRef<BaseMember>(new BaseMember())
  const mem = memRef.current

  const center = transformPoint2D(matrix, participants.local.pose.position)
  if (thirdPersonView !== mem.prebThirdPersonView) {
    mem.prebThirdPersonView = thirdPersonView
    if (thirdPersonView) {
      const mapRot = radian2Degree(extractRotation(matrix))
      if (mapRot) {
        const newMatrix = rotateMap(-mapRot, center)
        map.setCommittedMatrix(newMatrix)
      }
    }else {
      const avatarRot = participants.local.pose.orientation
      const mapRot = radian2Degree(extractRotation(matrix))
      if (avatarRot + mapRot) {
        const newMatrix = rotateMap(-(avatarRot + mapRot), center)
        map.setCommittedMatrix(newMatrix)
      }
    }
  }

  //  utility
  function rotateMap(degree:number, center:[number, number]) {
    const changeMatrix = (new DOMMatrix()).rotateSelf(0, 0, degree)
    const newMatrix = transfromAt(center, changeMatrix, matrix)
    map.setMatrix(newMatrix)

    return newMatrix
  }

  //  Mouse and touch operations ----------------------------------------------
  const MOUSE_LEFT = 1
  const MOUSE_RIGHT = 2

  //  zoom by scrollwheel
  function wheelHandler(event:React.WheelEvent) {
    if (!event.ctrlKey) {
      /*  //  translate map
      const diff = mulV2(0.2, rotateVector2D(matrix.inverse(), [event.deltaX, event.deltaY]))
      const newMatrix = matrix.translate(-diff[0], -diff[1])
      map.setMatrix(newMatrix)*/

      //  zoom map
      let scale = Math.pow(1.2, event.deltaY / 100)
      scale = limitScale(extractScaleX(map.matrix), scale)
      //  console.log(`zoom scale:${scale}`)
      if (scale === 1){
        return
      }

      //  console.log(`Wheel: ${movement}  scale=${scale}`)
      const newMatrix = map.matrix.scale(scale, scale, 1,
        ...transformPoint2D(map.matrix.inverse(), map.mouse))
      map.setMatrix(newMatrix)
      map.setCommittedMatrix(newMatrix)
    }
  }
  function moveParticipant(move: boolean, givenTarget?:[number,number]) {
    const local = participants.local
    let target = givenTarget
    if (!target){ target = map.mouseOnMap }
    const diff = subV2(target, local.pose.position)
    if (normV(diff) > (givenTarget ? PARTICIPANT_SIZE*2 : PARTICIPANT_SIZE / 2)) {
      const dir = mulV2(20 / normV(diff), diff)
      local.pose.orientation = Math.atan2(dir[0], -dir[1]) * 180 / Math.PI
      if (move) {
        local.pose.position = addV2(local.pose.position, dir)
      }
      local.savePhysicsToStorage(false)
    }
  }
  /*
  function moveParticipantPeriodically(move: boolean, target?:[number,number]) {
    moveParticipant(move, target)
    const TIMER_INTERVAL = move ? 33 : 300
    setTimeout(() => {
      if (mem.mouseDown) {
        moveParticipantPeriodically(true)
      }
    }, TIMER_INTERVAL) //  move to mouse position
  }*/

  const bind = useGesture(
    {
      onDragStart: ({buttons}) => {
        document.body.focus()
        mem.dragging = true
        mem.mouseDown = true
        //  console.log('Base StartDrag:')
        if (buttons === MOUSE_LEFT) {
          //  It seems facing and moving to the mouse cursor induce unintended movements of the avatar.
          //  move participant to mouse position
          //  moveParticipantPeriodically(false)
        }
      },
      onDrag: ({down, delta, xy, buttons}) => {
        if (delta[0] || delta[1]) { mem.mouseDown = false }
        //  if (map.keyInputUsers.size) { return }
        if (mem.dragging && down && outer.current) {
          if (!thirdPersonView && buttons === MOUSE_RIGHT) {  // right mouse drag - rotate map
            const center = transformPoint2D(matrix, participants.local.pose.position)
            const target:[number, number] = addV2(xy, offset())
            const radius1 = subV2(target, center)
            const radius2 = subV2(radius1, delta)

            const cosAngle = crossProduct(radius1, radius2) / (vectorLength(radius1) * vectorLength(radius2))
            const flag = crossProduct(rotate90ClockWise(radius1), delta) > 0 ? -1 : 1
            const angle = Math.acos(cosAngle) * flag
            if (isNaN(angle)) {  // due to accuracy, angle might be NaN when cosAngle is larger than 1
              return  // no need to update matrix
            }

            const newMatrix = rotateMap(radian2Degree(angle), center)
            participants.local.pose.orientation = -radian2Degree(extractRotation(newMatrix))
          } else {
            // left mouse drag or touch screen drag - translate map
            const diff = rotateVector2D(matrix.inverse(), delta)
            const newMatrix = matrix.translate(...diff)
            map.setMatrix(newMatrix)
            //  rotate and direct participant to the mouse position.
            if (delta[0] || delta[1]){
              moveParticipant(false, map.centerOnMap)
            }
            //console.log('Base onDrag:', delta)
          }
        }
      },
      onDragEnd: () => {
        if (matrix.toString() !== map.committedMatrix.toString()) {
          map.setCommittedMatrix(matrix)
          moveParticipant(false, map.centerOnMap)
          //console.log(`Base onDragEnd: (${map.centerOnMap})`)
        }
        mem.dragging = false
        mem.mouseDown = false
      },
      onPinch: ({da: [d, a], origin, event, memo}) => {
        if (memo === undefined) {
          return [d, a]
        }

        const [md, ma] = memo

        const center = addV2(origin as [number, number], offset())

        const MIN_D = 10
        let scale = d > MIN_D ? d / md : d <  -MIN_D ? md / d : (1 + (d - md) / MIN_D)
        //console.log(`Pinch: da:${[d, a]} origin:${origin}  memo:${memo}  scale:${scale}`)

        scale = limitScale(extractScaleX(matrix), scale)

        const changeMatrix = thirdPersonView ?
          (new DOMMatrix()).scaleSelf(scale, scale, 1) :
          (new DOMMatrix()).scaleSelf(scale, scale, 1).rotateSelf(0, 0, a - ma)

        const newMatrix = transfromAt(center, changeMatrix, matrix)
        map.setMatrix(newMatrix)

        if (!thirdPersonView) {
          participants.local.pose.orientation = -radian2Degree(extractRotation(newMatrix))
        }

        return [d, a]
      },
      onPinchEnd: () => map.setCommittedMatrix(matrix),
      onMove:({xy}) => {
        map.setMouse(xy)
        participants.local.mouse.position = Object.assign({}, map.mouseOnMap)
      },
      onTouchStart:(ev) => {
        map.setMouse([ev.touches[0].clientX, ev.touches[0].clientY])
        participants.local.mouse.position = Object.assign({}, map.mouseOnMap)
      },
    },
    {
      eventOptions:{passive:false}, //  This prevents default zoom by browser when pinch.
    },
  )

  //  setClientRect of the outer.
  useEffect(
    () => {
      onResizeOuter()
    },
    // eslint-disable-next-line  react-hooks/exhaustive-deps
    [],
  )

  // Prevent browser's zoom
  useEffect(
    () => {
      function topWindowHandler(event:WheelEvent) {
        //console.log(event)
        if (event.ctrlKey) {
          if (window.visualViewport && window.visualViewport.scale > 1){
            if (event.deltaY < 0){
              event.preventDefault()
              //  console.log('prevent', event.deltaY)
            }else{
              //  console.log('through', event.deltaY)
            }
          }else{
            event.preventDefault()
          }
          //  console.log('CTRL + mouse wheel = zoom prevented.', event)
        }
      }


      window.document.body.addEventListener('wheel', topWindowHandler, {passive: false})

      return () => {
        window.document.body.removeEventListener('wheel', topWindowHandler)
      }
    },
    [],
  )
  /*  //  This has no effect for iframe and other cases can be handled by onMove. So this is useless
  //  preview mouse move on outer
  useEffect(
    () => {
      function handler(ev:MouseEvent) {
        map.setMouse([ev.clientX, ev.clientY])
      }
      if (outer.current) {
        outer.current.addEventListener('mousemove', handler, {capture:true})
      }

      return () => {
        if (outer.current) {
          outer.current.removeEventListener('mousemove', handler)
        }
      }
    },
    [outer])
  */
  //  Event handlers when use scroll ----------------------------------------------
  //  Move to center when root div is created.
  /*
  useEffect(
    () => {
      if (outer.current) {
        const elem = outer.current
        console.log('useEffect[outer] called')
        elem.scrollTo((MAP_SIZE - elem.clientWidth) * HALF, (MAP_SIZE - elem.clientHeight) *  HALF)
      }
    },
    [outer],
  )
  if (!showScrollbar) {
    const elem = outer.current
    if (elem) {
      elem.scrollTo((MAP_SIZE - elem.clientWidth) * HALF, (MAP_SIZE - elem.clientHeight) *  HALF)
    }
  }
  */
  // scroll range
  /*  useEffect(
    () => {
      const orgMat = new DOMMatrix(matrix.toString())
      setMatrix(orgMat)
    },
    [outer],
  )
  */
  //  update offset
  const onResizeOuter = useRef(
      () => {
      if (outer.current) {
        let cur = outer.current as HTMLElement
        let offsetLeft = 0
        while (cur) {
          offsetLeft += cur.offsetLeft
          cur = cur.offsetParent as HTMLElement
        }
        //  console.log(`sc:[${outer.current.clientWidth}, ${outer.current.clientHeight}] left:${offsetLeft}`)
        map.setScreenSize([outer.current.clientWidth, outer.current.clientHeight])
        map.setLeft(offsetLeft)
        // map.setOffset([outer.current.scrollLeft, outer.current.scrollTop])  //  when use scroll
      }
    }
  ).current

  const styleProps: StyleProps = {
    matrix,
  }
  const classes = useStyles(styleProps)

  return (
    <div className={classes.root} ref={outer} {...bind()}>
      <ResizeObserver onResize = { onResizeOuter } />
      <div className={classes.center} onWheel={wheelHandler}>
        <div id="map-transform" className={classes.transform} ref={container}>
            {props.children}
        </div>
      </div>
    </div>
  )
}
Example #10
Source File: RndContent.tsx    From binaural-meet with GNU General Public License v3.0 4 votes vote down vote up
RndContent: React.FC<RndContentProps> = (props:RndContentProps) => {
  /*
  function rotateG2C(gv: [number, number]) {
    const lv = mapData.rotateFromWindow(gv)
    const cv = rotateVector2DByDegree(-pose.orientation, lv)
    //  console.log('rotateG2C called ori', pose.orientation, ' tran:', transform.rotation)

    return cv
  }*/
  /*
  function rotateG2L(gv: [number, number]) {
    const lv = transform.rotateG2L(gv)

    return lv
  }
  function rotateC2G(cv: [number, number]) {
    const lv = rotateVector2DByDegree(pose.orientation, cv)
    const gv = transform.rotateL2G(lv)

    return gv
  }*/

  // states
  const [pose, setPose] = useState(props.content.pose)  //  pose of content
  const [size, setSize] = useState(props.content.size)  //  size of content
  const [resizeBase, setResizeBase] = useState(size)    //  size when resize start
  const [resizeBasePos, setResizeBasePos] = useState(pose.position)    //  position when resize start
  const [showTitle, setShowTitle] = useState(!props.autoHideTitle || !props.content.pinned)
  const [showForm, setShowForm] = useState(false)
  const [preciseOrientation, setPreciseOrientation] = useState(pose.orientation)
  const [dragging, setDragging] = useState(false)
  const rnd = useRef<Rnd>(null)                         //  ref to rnd to update position and size
  const {contents, map} = props.stores
  const editing = useObserver(() => contents.editing === props.content.id)
  const zoomed = useObserver(() => map.zoomed)
  function setEditing(flag: boolean) { contents.setEditing(flag ? props.content.id : '') }
  const memberRef = useRef<RndContentMember>(new RndContentMember())
  const member = memberRef.current

  useEffect(  //  update pose
    ()=> {
      member.dragCanceled = true
      if (!_.isEqual(size, props.content.size)) {
        setSize(_.cloneDeep(props.content.size))
      }
      if (!_.isEqual(pose, props.content.pose)) {
        setPose(_.cloneDeep(props.content.pose))
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [props.content],
  )

  function setPoseAndSizeToRnd(){
    if (rnd.current) { rnd.current.resizable.orientation = pose.orientation + map.rotation }
    const titleHeight = showTitle ? TITLE_HEIGHT : 0
    rnd.current?.updatePosition({x:pose.position[0], y:pose.position[1] - titleHeight})
    rnd.current?.updateSize({width:size[0], height:size[1] + titleHeight})
  }
  useLayoutEffect(  //  reflect pose etc. to rnd size
    () => {
      setPoseAndSizeToRnd()
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [pose, size, showTitle, map.rotation],
  )

  //  handlers
  function stop(ev:MouseOrTouch|React.PointerEvent) {
    ev.stopPropagation()
    ev.preventDefault()
  }
  function onClickShare(evt: MouseOrTouch) {
    stop(evt)
    props.onShare?.call(null, evt)
  }
  function onClickClose(evt: MouseOrTouch) {
    stop(evt)
    props.onClose?.call(null, evt)
  }
  function onClickEdit(evt: MouseOrTouch) {
    stop(evt)
    setEditing(!editing)
  }
  function onClickMoveToTop(evt: MouseOrTouch) {
    stop(evt)
    moveContentToTop(props.content)
    props.updateAndSend(props.content)
  }
  function onClickMoveToBottom(evt: MouseOrTouch) {
    stop(evt)
    moveContentToBottom(props.content)
    props.updateAndSend(props.content)
  }
  function onClickPin(evt: MouseOrTouch) {
    stop(evt)
    props.content.pinned = !props.content.pinned
    props.updateAndSend(props.content)
  }
  function onClickCopy(evt: MouseOrTouch){
    stop(evt)
    copyContentToClipboard(props.content)
  }
  function onClickMaximize(evt: MouseOrTouch){
    stop(evt)
    if (map.zoomed){
      map.restoreZoom()
    }else{
      map.zoomTo(props.content)
    }
  }
  function onClickMore(evt: MouseOrTouch){
    stop(evt)
    map.keyInputUsers.add('contentForm')
    setShowForm(true)
  }
  function onCloseForm(){
    setShowForm(false)
    if (props.content.pinned){
      setShowTitle(false)
    }
    map.keyInputUsers.delete('contentForm')
    props.updateAndSend(props.content)
  }
  function updateHandler() {
    if (JSON.stringify(pose) !== JSON.stringify(props.content.pose) ||
      JSON.stringify(size) !== JSON.stringify(props.content.size)) {
      props.content.size = [...size] //  Must be new object to compare the pose or size object.
      props.content.pose = {...pose} //  Must be new object to compare the pose or size object.
      props.updateAndSend(props.content)
    }
  }

  //  drag for title area
  function dragHandler(delta:[number, number], buttons:number, event:any) {
    if (member.dragCanceled){ return }
    const ROTATION_IN_DEGREE = 360
    const ROTATION_STEP = 15
    if (buttons === MOUSE_RIGHT) {
      setPreciseOrientation((preciseOrientation + delta[0] + delta[1]) % ROTATION_IN_DEGREE)
      let newOri
      if (event?.shiftKey || event?.ctrlKey) {
        newOri = preciseOrientation
      }else {
        newOri = preciseOrientation - preciseOrientation % ROTATION_STEP
      }
      //    mat.translateSelf(...addV2(props.pose.position, mulV(0.5, size)))
      const CENTER_IN_RATIO = 0.5
      const center = addV2(pose.position, mulV(CENTER_IN_RATIO, size))
      pose.position = addV2(pose.position,
                            subV2(rotateVector2DByDegree(pose.orientation - newOri, center), center))
      pose.orientation = newOri
      setPose(Object.assign({}, pose))
    }else {
      const lv = map.rotateFromWindow(delta)
      const cv = rotateVector2DByDegree(-pose.orientation, lv)
      pose.position = addV2(pose.position, cv)
      setPose(Object.assign({}, pose))
    }
  }

  const isFixed = (props.autoHideTitle && props.content.pinned)
  const handlerForTitle:UserHandlersPartial = {
    onDoubleClick: (evt)=>{
      if (isContentEditable(props.content)){
        stop(evt)
        setEditing(!editing)
      }
    },
    onDrag: ({down, delta, event, xy, buttons}) => {
      //  console.log('onDragTitle:', delta)
      if (isFixed) { return }
      event?.stopPropagation()
      if (down) {
        //  event?.preventDefault()
        dragHandler(delta, buttons, event)
      }
    },
    onDragStart: ({event, currentTarget, delta, buttons}) => {   // to detect click
      //  console.log(`dragStart delta=${delta}  buttons=${buttons}`)
      setDragging(true)
      member.buttons = buttons
      member.dragCanceled = false
      if (currentTarget instanceof Element && event instanceof PointerEvent){
        currentTarget.setPointerCapture(event?.pointerId)
      }
    },
    onDragEnd: ({event, currentTarget, delta, buttons}) => {
      //  console.log(`dragEnd delta=${delta}  buttons=${buttons}`)
      setDragging(false)
      if (!member.dragCanceled){ updateHandler() }
      member.dragCanceled = false

      if (currentTarget instanceof Element && event instanceof PointerEvent){
        currentTarget.releasePointerCapture(event?.pointerId)
      }
      if (!map.keyInputUsers.size && member.buttons === MOUSE_RIGHT){ //  right click
        setShowForm(true)
        map.keyInputUsers.add('contentForm')
      }
      member.buttons = 0
    },
    onPointerUp: (arg) => { if(editing) {arg.stopPropagation()} },
    onPointerDown: (arg) => { if(editing) {arg.stopPropagation()} },
    onMouseUp: (arg) => { if(editing) {arg.stopPropagation()} },
    onMouseDown: (arg) => { if(editing) {arg.stopPropagation()} },
    onTouchStart: (arg) => { if(editing) {arg.stopPropagation() }},
    onTouchEnd: (arg) => { if(editing) {arg.stopPropagation()} },
  }
  const handlerForContent:UserHandlersPartial = Object.assign({}, handlerForTitle)
  handlerForContent.onDrag = (args: FullGestureState<'drag'>) => {
    //  console.log('onDragBody:', args.delta)
    if (isFixed || map.keyInputUsers.has(props.content.id)) { return }
    handlerForTitle.onDrag?.call(this, args)
  }

  function onResizeStart(evt:React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>){
    member.dragCanceled = false
    evt.stopPropagation(); evt.preventDefault()
    setResizeBase(size)
    setResizeBasePos(pose.position)
  }
  function onResizeStop(){
    if (!member.dragCanceled){ updateHandler() }
    member.dragCanceled = false
  }
  function onResize(evt:MouseEvent | TouchEvent, dir: any, elem:HTMLDivElement, delta:any, pos:any) {
    evt.stopPropagation(); evt.preventDefault()
    //  console.log(`dragcancel:${member.dragCanceled}`)
    if (member.dragCanceled) {
      setPoseAndSizeToRnd()

      return
    }

    const scale = (extractScaleX(map.matrix) + extractScaleY(map.matrix)) / 2
    const cd:[number, number] = [delta.width / scale, delta.height / scale]
    // console.log('resize dir:', dir, ' delta:', delta, ' d:', d, ' pos:', pos)
    if (dir === 'left' || dir === 'right') {
      cd[1] = 0
    }
    if (dir === 'top' || dir === 'bottom') {
      cd[0] = 0
    }
    let posChange = false
    const deltaPos: [number, number] = [0, 0]
    if (dir === 'left' || dir === 'topLeft' || dir === 'bottomLeft') {
      deltaPos[0] = -cd[0]
      posChange = posChange || cd[0] !== 0
    }
    if (dir === 'top' || dir === 'topLeft' || dir === 'topRight') {
      deltaPos[1] = -cd[1]
      posChange = posChange || cd[1] !== 0
    }
    if (posChange) {
      pose.position = addV2(resizeBasePos, deltaPos)
      setPose(Object.assign({}, pose))
      //console.log(`setPose ${pose.position}`)
    }
    const newSize = addV2(resizeBase, cd)
    if (props.content.originalSize[0]) {
      const ratio = props.content.originalSize[0] / props.content.originalSize[1]
      if (newSize[0] > ratio * newSize[1]) { newSize[0] = ratio * newSize[1] }
      if (newSize[0] < ratio * newSize[1]) { newSize[1] = newSize[0] / ratio }
    }
    setSize(newSize)
  }


  const classes = useStyles({props, pose, size, showTitle, pinned:props.content.pinned, dragging, editing})
  //  console.log('render: TITLE_HEIGHT:', TITLE_HEIGHT)
  const contentRef = React.useRef<HTMLDivElement>(null)
  const formRef = React.useRef<HTMLDivElement>(null)
  const gestureForContent = useGesture(handlerForContent)
  const gestureForTitle = useGesture(handlerForTitle)
  const theContent =
    <div className={classes.rndContainer} {...gestureForContent()}>
      <div className={classes.titlePosition} {...gestureForTitle() /* title can be placed out of Rnd */}>
        <div className={classes.titleContainer}
            onMouseEnter = {() => { if (props.autoHideTitle) { setShowTitle(true) } }}
            onMouseLeave = {() => {
              if (props.autoHideTitle && !editing && props.content.pinned) { setShowTitle(false) } }}
            onTouchStart = {() => {
              if (props.autoHideTitle) {
                if (!showTitle) {
                  setShowTitle(true)
                }else if (props.content.pinned) {
                  setShowTitle(false)
                }
              }
            }}
            onContextMenu = {() => {
              setShowForm(true)
              map.keyInputUsers.add('contentForm')
            }}
            >
          <div className={classes.type}>
            {contentTypeIcons(props.content.type, TITLE_HEIGHT, TITLE_HEIGHT*1.1)}
          </div>
          <Tooltip placement="top" title={props.content.pinned ? t('ctUnpin') : t('ctPin')} >
          <div className={classes.pin} onClick={onClickPin} onTouchStart={stop}>
            <Icon icon={props.content.pinned ? pinIcon : pinOffIcon} height={TITLE_HEIGHT} width={TITLE_HEIGHT*1.1} />
          </div></Tooltip>
          <Tooltip placement="top" title={editButtonTip(editing, props.content)} >
            <div className={classes.edit} onClick={onClickEdit} onTouchStart={stop}>
             {
              editing ? <DoneIcon style={{fontSize:TITLE_HEIGHT}} />
                : <EditIcon style={{fontSize:TITLE_HEIGHT}} />}
            </div>
          </Tooltip>
          {props.content.pinned ? undefined :
            <Tooltip placement="top" title={t('ctMoveTop')} >
              <div className={classes.titleButton} onClick={onClickMoveToTop}
                onTouchStart={stop}><FlipToFrontIcon /></div></Tooltip>}
          {props.content.pinned ? undefined :
            <Tooltip placement="top" title={t('ctMoveBottom')} >
              <div className={classes.titleButton} onClick={onClickMoveToBottom}
                onTouchStart={stop}><FlipToBackIcon /></div></Tooltip>}

          {/*(props.content.pinned || !canContentBeAWallpaper(props.content)) ? undefined :
            <div className={classes.titleButton} onClick={onClickWallpaper}
              onTouchStart={stop}>
                <Tooltip placement="top" title={isContentWallpaper(props.content) ?
                  t('ctUnWallpaper') : t('ctWallpaper')}>
                  <div><WallpaperIcon />{isContentWallpaper(props.content) ?
                    <CloseRoundedIcon style={{marginLeft:'-1em'}} /> : undefined }</div>
                </Tooltip>
                  </div> */}
          <Tooltip placement="top" title={t('ctCopyToClipboard')} >
            <div className={classes.titleButton} onClick={onClickCopy}
              onTouchStart={stop}>
                <Icon icon={clipboardCopy} height={TITLE_HEIGHT}/>
            </div>
          </Tooltip>
          {isContentMaximizable(props.content) ?
            <Tooltip placement="top" title={zoomed ? t('ctUnMaximize') : t('ctMaximize')} >
              <div className={classes.titleButton} onClick={onClickMaximize}
                onTouchStart={stop}>
                  <Icon icon={zoomed ? minimizeIcon: maximizeIcon} height={TITLE_HEIGHT}/>
              </div>
            </Tooltip> : undefined}
          <div className={classes.titleButton} onClick={onClickMore} onTouchStart={stop} ref={formRef}>
              <MoreHorizIcon />
          </div>
          <SharedContentForm open={showForm} {...props} close={onCloseForm}
            anchorEl={contentRef.current} anchorOrigin={{vertical:'top', horizontal:'right'}}
          />
          <div className={classes.note} onClick={onClickShare} onTouchStart={stop}>Share</div>
          {props.content.playback ? <div className={classes.close} ><PlayArrowIcon htmlColor="#0C0" /></div> :
            (props.content.pinned || isContentWallpaper(props.content)) ? undefined :
              <div className={classes.close} onClick={onClickClose} onTouchStart={stop}>
                <CloseRoundedIcon /></div>}
        </div>
      </div>
      <div className={classes.content} ref={contentRef}
        onFocus={()=>{
          if (doseContentEditingUseKeyinput(props.content) && editing){
            map.keyInputUsers.add(props.content.id)
          }
        }}
        onBlur={()=>{
          if (doseContentEditingUseKeyinput(props.content) && editing){
            map.keyInputUsers.delete(props.content.id)
          }
        }}
      >
        <Content {...props}/>
      </div>
    </div>
  //  console.log('Rnd rendered.')


  return (
    <div className={classes.container} style={{zIndex:props.content.zIndex}} onContextMenu={
      (evt) => {
        evt.stopPropagation()
        evt.preventDefault()
      }
    }>
      <Rnd className={classes.rndCls} enableResizing={isFixed ? resizeDisable : resizeEnable}
        disableDragging={isFixed} ref={rnd}
        onResizeStart = {onResizeStart}
        onResize = {onResize}
        onResizeStop = {onResizeStop}
      >
        {theContent}
      </Rnd>
    </div >
  )
}
Example #11
Source File: item.component.tsx    From react-sortful with MIT License 4 votes vote down vote up
Item = <T extends ItemIdentifier>(props: Props<T>) => {
  const listContext = React.useContext(ListContext);
  const groupContext = React.useContext(GroupContext);

  const wrapperElementRef = React.useRef<HTMLDivElement>(null);

  const ancestorIdentifiers = [...groupContext.ancestorIdentifiers, props.identifier];
  const isGroup = props.isGroup ?? false;
  const isLocked = (listContext.isDisabled || props.isLocked) ?? false;
  const isLonley = props.isLonely ?? false;
  const isUsedCustomDragHandlers = props.isUsedCustomDragHandlers ?? false;

  // Registers an identifier to the group context.
  const childIdentifiersRef = React.useRef<Set<ItemIdentifier>>(new Set());
  React.useEffect(() => {
    groupContext.childIdentifiersRef.current.add(props.identifier);

    return () => {
      groupContext.childIdentifiersRef.current.delete(props.identifier);
    };
  }, []);

  // Clears timers.
  const clearingDraggingNodeTimeoutIdRef = React.useRef<number>();
  React.useEffect(
    () => () => {
      window.clearTimeout(clearingDraggingNodeTimeoutIdRef.current);
    },
    [],
  );

  const onDragStart = React.useCallback(() => {
    const element = wrapperElementRef.current;
    if (element == undefined) return;

    setBodyStyle(document.body, listContext.draggingCursorStyle);
    initializeGhostElementStyle(
      element,
      listContext.ghostWrapperElementRef.current ?? undefined,
      listContext.itemSpacing,
      listContext.direction,
    );

    // Sets contexts to values.
    const nodeMeta = getNodeMeta(element, props.identifier, groupContext.identifier, ancestorIdentifiers, props.index, isGroup);
    listContext.setDraggingNodeMeta(nodeMeta);

    // Calls callbacks.
    listContext.onDragStart?.({
      identifier: nodeMeta.identifier,
      groupIdentifier: nodeMeta.groupIdentifier,
      index: nodeMeta.index,
      isGroup: nodeMeta.isGroup,
    });
  }, [
    listContext.itemSpacing,
    listContext.direction,
    listContext.onDragStart,
    listContext.draggingCursorStyle,
    groupContext.identifier,
    props.identifier,
    props.index,
    ancestorIdentifiers,
    isGroup,
  ]);
  const onDrag = React.useCallback((isDown: boolean, absoluteXY: [number, number]) => {
    if (!isDown) return;

    moveGhostElement(listContext.ghostWrapperElementRef.current ?? undefined, absoluteXY);
  }, []);
  const onDragEnd = React.useCallback(() => {
    clearBodyStyle(document.body);
    clearGhostElementStyle(listContext.ghostWrapperElementRef.current ?? undefined);

    // Calls callbacks.
    const destinationMeta = listContext.destinationMetaRef.current;
    listContext.onDragEnd({
      identifier: props.identifier,
      groupIdentifier: groupContext.identifier,
      index: props.index,
      isGroup,
      nextGroupIdentifier: destinationMeta != undefined ? destinationMeta.groupIdentifier : groupContext.identifier,
      nextIndex: destinationMeta != undefined ? destinationMeta.index : props.index,
    });

    // Resets context values.
    listContext.setDraggingNodeMeta(undefined);
    listContext.setIsVisibleDropLineElement(false);
    listContext.setStackedGroupIdentifier(undefined);
    listContext.hoveredNodeMetaRef.current = undefined;
    listContext.destinationMetaRef.current = undefined;
  }, [listContext.onDragEnd, groupContext.identifier, props.identifier, props.index, isGroup]);

  const onHover = React.useCallback(
    (element: HTMLElement) => {
      // Initialize if the dragging item is this item or an ancestor group of this item.
      const draggingNodeMeta = listContext.draggingNodeMeta;
      const isNeededInitialization =
        draggingNodeMeta == undefined ||
        props.identifier === draggingNodeMeta.identifier ||
        checkIsAncestorItem(draggingNodeMeta.identifier, ancestorIdentifiers);
      if (isNeededInitialization) {
        listContext.setIsVisibleDropLineElement(false);
        listContext.hoveredNodeMetaRef.current = undefined;
        listContext.destinationMetaRef.current = undefined;

        return;
      }

      listContext.setIsVisibleDropLineElement(true);
      listContext.hoveredNodeMetaRef.current = getNodeMeta(
        element,
        props.identifier,
        groupContext.identifier,
        ancestorIdentifiers,
        props.index,
        isGroup,
      );
    },
    [listContext.draggingNodeMeta, groupContext.identifier, props.identifier, props.index, ancestorIdentifiers, isGroup],
  );
  const onMoveForStackableGroup = React.useCallback(
    <T extends ItemIdentifier>(hoveredNodeMeta: NodeMeta<T>) => {
      // Sets contexts to values.
      listContext.setIsVisibleDropLineElement(false);
      listContext.setStackedGroupIdentifier(props.identifier);
      listContext.destinationMetaRef.current = {
        groupIdentifier: props.identifier,
        index: undefined,
      };

      // Calls callbacks.
      listContext.onStackGroup?.({
        identifier: props.identifier,
        groupIdentifier: groupContext.identifier,
        index: props.index,
        isGroup,
        nextGroupIdentifier: hoveredNodeMeta.identifier,
      });
    },
    [listContext.stackableAreaThreshold, listContext.onStackGroup, groupContext.identifier, props.identifier, props.index],
  );
  const onMoveForItems = React.useCallback(
    (draggingNodeMeta: NodeMeta<T>, hoveredNodeMeta: NodeMeta<T>, absoluteXY: [number, number]) => {
      if (isLonley) {
        listContext.setIsVisibleDropLineElement(false);
        listContext.destinationMetaRef.current = undefined;

        return;
      }
      if (draggingNodeMeta.index !== hoveredNodeMeta.index) listContext.setIsVisibleDropLineElement(true);

      const dropLineElement = listContext.dropLineElementRef.current ?? undefined;
      setDropLineElementStyle(dropLineElement, absoluteXY, hoveredNodeMeta, listContext.direction);

      // Calculates the next index.
      const dropLineDirection = getDropLineDirectionFromXY(absoluteXY, hoveredNodeMeta, listContext.direction);
      const nextIndex = getDropLinePositionItemIndex(
        dropLineDirection,
        draggingNodeMeta.index,
        draggingNodeMeta.groupIdentifier,
        hoveredNodeMeta.index,
        hoveredNodeMeta.groupIdentifier,
      );

      // Calls callbacks if needed.
      const destinationMeta = listContext.destinationMetaRef.current;
      const isComeFromStackedGroup =
        destinationMeta != undefined && destinationMeta.groupIdentifier != undefined && destinationMeta.index == undefined;
      if (isComeFromStackedGroup) {
        listContext.onStackGroup?.({
          identifier: props.identifier,
          groupIdentifier: groupContext.identifier,
          index: props.index,
          isGroup,
          nextGroupIdentifier: undefined,
        });
      }

      // Sets contexts to values.
      listContext.setStackedGroupIdentifier(undefined);
      listContext.destinationMetaRef.current = { groupIdentifier: groupContext.identifier, index: nextIndex };
    },
    [
      listContext.direction,
      listContext.onStackGroup,
      groupContext.identifier,
      props.identifier,
      props.index,
      isGroup,
      isLonley,
    ],
  );
  const onMove = React.useCallback(
    (absoluteXY: [number, number]) => {
      const draggingNodeMeta = listContext.draggingNodeMeta;
      if (draggingNodeMeta == undefined) return;
      const hoveredNodeMeta = listContext.hoveredNodeMetaRef.current;
      if (hoveredNodeMeta == undefined) return;

      const hasNoItems = childIdentifiersRef.current.size === 0;
      if (
        isGroup &&
        hasNoItems &&
        checkIsInStackableArea(absoluteXY, hoveredNodeMeta, listContext.stackableAreaThreshold, listContext.direction)
      ) {
        onMoveForStackableGroup(hoveredNodeMeta);
      } else {
        onMoveForItems(draggingNodeMeta, hoveredNodeMeta, absoluteXY);
      }
    },
    [listContext.draggingNodeMeta, listContext.direction, onMoveForStackableGroup, onMoveForItems, isGroup],
  );
  const onLeave = React.useCallback(() => {
    if (listContext.draggingNodeMeta == undefined) return;

    // Clears a dragging node after 50ms in order to prevent setting and clearing at the same time.
    window.clearTimeout(clearingDraggingNodeTimeoutIdRef.current);
    clearingDraggingNodeTimeoutIdRef.current = window.setTimeout(() => {
      if (listContext.hoveredNodeMetaRef.current?.identifier !== props.identifier) return;

      listContext.setIsVisibleDropLineElement(false);
      listContext.setStackedGroupIdentifier(undefined);
      listContext.hoveredNodeMetaRef.current = undefined;
      listContext.destinationMetaRef.current = undefined;
    }, 50);
  }, [listContext.draggingNodeMeta, props.identifier]);

  const binder = useGesture({
    onHover: ({ event }) => {
      if (listContext.draggingNodeMeta == undefined) return;

      const element = event?.currentTarget;
      if (!(element instanceof HTMLElement)) return;

      event?.stopPropagation();
      onHover(element);
    },
    onMove: ({ xy }) => {
      if (listContext.draggingNodeMeta == undefined) return;

      // Skips if this item is an ancestor group of the dragging item.
      const hasItems = childIdentifiersRef.current.size > 0;
      const hoveredNodeAncestors = listContext.hoveredNodeMetaRef.current?.ancestorIdentifiers ?? [];
      if (hasItems && checkIsAncestorItem(props.identifier, hoveredNodeAncestors)) return;
      if (props.identifier === listContext.draggingNodeMeta.identifier) return;
      // Skips if the dragging item is an ancestor group of this item.
      if (checkIsAncestorItem(listContext.draggingNodeMeta.identifier, ancestorIdentifiers)) return;

      onMove(xy);
    },
    onPointerLeave: onLeave,
  });
  const dragHandlers: React.ContextType<typeof ItemContext>["dragHandlers"] = { onDragStart, onDrag, onDragEnd };
  const draggableBinder = useGesture({
    onDragStart: (state: any) => {
      if (isLocked) return;

      const event: React.SyntheticEvent = state.event;
      event.persist();
      event.stopPropagation();

      dragHandlers.onDragStart();
    },
    onDrag: ({ down, movement }) => {
      if (isLocked) return;

      dragHandlers.onDrag(down, movement);
    },
    onDragEnd: () => {
      if (isLocked) return;

      dragHandlers.onDragEnd();
    },
  });

  const contentElement = React.useMemo((): JSX.Element => {
    const draggingNodeMeta = listContext.draggingNodeMeta;
    const isDragging = draggingNodeMeta != undefined && props.identifier === draggingNodeMeta.identifier;
    const { renderPlaceholder, renderStackedGroup, itemSpacing, direction } = listContext;

    const rendererMeta: Omit<PlaceholderRendererMeta<any>, "isGroup"> | StackedGroupRendererMeta<any> = {
      identifier: props.identifier,
      groupIdentifier: groupContext.identifier,
      index: props.index,
    };

    let children = props.children;
    if (isDragging && renderPlaceholder != undefined) {
      const style = getPlaceholderElementStyle(draggingNodeMeta, itemSpacing, direction);
      children = renderPlaceholder({ style }, { ...rendererMeta, isGroup });
    }
    if (listContext.stackedGroupIdentifier === props.identifier && renderStackedGroup != undefined) {
      const style = getStackedGroupElementStyle(listContext.hoveredNodeMetaRef.current, itemSpacing, direction);
      children = renderStackedGroup({ style }, rendererMeta);
    }

    const padding: [string, string] = ["0", "0"];
    if (direction === "vertical") padding[0] = `${itemSpacing / 2}px`;
    if (direction === "horizontal") padding[1] = `${itemSpacing / 2}px`;

    return (
      <div
        ref={wrapperElementRef}
        style={{ boxSizing: "border-box", position: "static", padding: padding.join(" ") }}
        {...binder()}
        {...(isUsedCustomDragHandlers ? {} : draggableBinder())}
      >
        {children}
      </div>
    );
  }, [
    listContext.draggingNodeMeta,
    listContext.renderPlaceholder,
    listContext.renderStackedGroup,
    listContext.stackedGroupIdentifier,
    listContext.itemSpacing,
    listContext.direction,
    groupContext.identifier,
    props.identifier,
    props.children,
    props.index,
    isGroup,
    isUsedCustomDragHandlers,
    binder,
    draggableBinder,
  ]);
  if (!isGroup) return <ItemContext.Provider value={{ isLocked, dragHandlers }}>{contentElement}</ItemContext.Provider>;

  return (
    <GroupContext.Provider value={{ identifier: props.identifier, ancestorIdentifiers, childIdentifiersRef }}>
      <ItemContext.Provider value={{ isLocked, dragHandlers }}>{contentElement}</ItemContext.Provider>
    </GroupContext.Provider>
  );
}