reactjsreact-colorembla-carousel

Draggable Container (color picker) in Embla Carousel not working as intended


Apologies if a similar question has been asked before.

My issue is that when using Embla Carousel, and implementing a color picker (on it's own or wrapped in a div) , dragging the color picker around doesn't work as expected as it drags the carousel as well.

I have implemented a simple code sandbox for your reference.

Minimal reproduction

In my own code, I have tried wrapping the <ChromePicker /> in a div (applied padding on the div). and implementing onPointerDown, onPointerMove and onPointerUp to stopPropagation and preventDefault.

So this works only on the div, where if i try to drag inside the div (on the padded area), it doesn't drag the carousel. But dragging the <ChromePicker /> around still makes the carousel move.

Appreciate the guide @Phil, apologies for not including the code. Below is from the sandbox

index.tsx - Default Embla Carousel with a slight change to pass in <ChromePicker />

import React, { useState } from 'react'
import ReactDOM from 'react-dom/client'
import EmblaCarousel from './EmblaCarousel'
import { EmblaOptionsType } from 'embla-carousel'
import Header from './Header'
import Footer from './Footer'
import '../css/base.css'
import '../css/sandbox.css'
import '../css/embla.css'
import { ChromePicker } from 'react-color'

const OPTIONS: EmblaOptionsType = {}

const App: React.FC = () => {
  const [hex, setHex] = useState<string>('#000000')
  const SLIDES = [
    // Added ChromePicker here
    <ChromePicker color={hex} onChange={(color) => setHex(color.hex)} />,
    2,
    3,
    4,
    5
  ]

  return (
    <>
      <Header />
      <EmblaCarousel slides={SLIDES} options={OPTIONS} />
      <Footer />
    </>
  )
}

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

EmblaCarousel.tsx - Default Embla Carousel with slight change to .map function to show <ChromePicker/ >

import React from 'react'
import { EmblaOptionsType } from 'embla-carousel'
import useEmblaCarousel from 'embla-carousel-react'
import AutoHeight from 'embla-carousel-auto-height'
import {
  NextButton,
  PrevButton,
  usePrevNextButtons
} from './EmblaCarouselArrowButtons'
import { DotButton, useDotButton } from './EmblaCarouselDotButton'

type PropType = {
  // Changed this to any[] for now to allow passing in ChromePicker
  slides: any[]
  options?: EmblaOptionsType
}

const EmblaCarousel: React.FC<PropType> = (props) => {
  const { slides, options } = props
  const [emblaRef, emblaApi] = useEmblaCarousel(options, [AutoHeight()])

  const { selectedIndex, scrollSnaps, onDotButtonClick } =
    useDotButton(emblaApi)

  const {
    prevBtnDisabled,
    nextBtnDisabled,
    onPrevButtonClick,
    onNextButtonClick
  } = usePrevNextButtons(emblaApi)

  return (
    <div className="embla">
      <div className="embla__viewport" ref={emblaRef}>
        <div className="embla__container">
          {slides.map((slide, index) => (
            <div className="embla__slide" key={index}>
              {/* Edited to show slide instead of generating index as per default of Embla */}
              <div className="embla__slide__number">{slide}</div>
            </div>
          ))}
        </div>
      </div>

      <div className="embla__controls">
        <div className="embla__buttons">
          <PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
          <NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
        </div>

        <div className="embla__dots">
          {scrollSnaps.map((_, index) => (
            <DotButton
              key={index}
              onClick={() => onDotButtonClick(index)}
              className={'embla__dot'.concat(
                index === selectedIndex ? ' embla__dot--selected' : ''
              )}
            />
          ))}
        </div>
      </div>
    </div>
  )
}

export default EmblaCarousel

Edited Thank you @Kostas Minaidis for the solution!

Solution added watchDrag in OPTIONS - index.tsx

import React, { useState } from 'react'
import ReactDOM from 'react-dom/client'
import EmblaCarousel from './EmblaCarousel'
import { EmblaOptionsType } from 'embla-carousel'
import Header from './Header'
import Footer from './Footer'
import '../css/base.css'
import '../css/sandbox.css'
import '../css/embla.css'
import { ChromePicker } from 'react-color'

const App: React.FC = () => {
  const [hex, setHex] = useState<string>('#000000')
  const SLIDES = [
    // Added ChromePicker here
    <ChromePicker color={hex} onChange={(color) => setHex(color.hex)} />,
    2,
    3,
    4,
    5
  ]

  // Solution from @Kostas Minaidis works perfectly!
  const OPTIONS: EmblaOptionsType = {
    watchDrag: (_, event) => {
      const target = event.target as HTMLElement
      if (
        target.classList.contains('chrome-picker') ||
        target.closest('.chrome-picker')
      ) {
        return false
      }
      return true
    }
  }

  return (
    <>
      <Header />
      <EmblaCarousel slides={SLIDES} options={OPTIONS} />
      <Footer />
    </>
  )
}

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)


Solution

  • You can utilize the following configuration options, and specifically the watchdrag option as provided by EmblaCarousel and enable dragging when the dragged target is the container div (return true) or disable dragging otherwise (anywhere inside the color picker).

    const OPTIONS: EmblaOptionsType = {
      watchDrag: (_, event) => {
        if (event.target.classList.contains('embla__slide__number')) {
          return true; // <= Will enabled dragging
        }
        return false; // <= Will disable dragging
      }
    }