Почему срабатывает pointerleave во время события pointermove на touch устройствах? Слайдер

Рейтинг: 0Ответов: 1Опубликовано: 08.04.2023

Пишу свой слайдер на JS. Появилась такая проблема - событие pointerleave на touch устройствах срабатывает во время перетаскивания элементов слайдера и останавливает перетаскивание. Не понимаю как с этим бороться, мне нужно отключить такое поведение

class Slider {

    #nodes = {
        sliderNode: null,
        sliderItemsNode: null,
        sliderItemNodes: [],
        sliderArrowLeftNode: null,
        sliderArrowRightNode: null,
        sliderWrapperNode: null
    };

    #cssSelectors = {
        items: '.slider__items',
        item: '.slider__item',
        wrapper: '.slider__wrapper',
        arrowLeft: '.slider__arrow_left',
        arrowRight: '.slider__arrow_right'
    };

    #centeringShiftX = 0;
    #changeSlideShiftX = 0;
    #moveSlideShiftX = 0;
    #shiftX = 0;
    #lastShifX = 0;

    #startMovePos = {
        x: 0,
        y: 0
    };

    #activeItemIndex = 0;

    #isDragging = false;
    #countDragging = 0;

    #teamsTitleNode = document.querySelector('.teams__title');


    constructor(sliderSelector) {
        this.#initNodes(sliderSelector);
        this.#initEventListeners();
        this.#calcAndSetShiftX();
    }

    #initNodes(sliderSelector) {
        this.#nodes.sliderNode = document.querySelector(sliderSelector);
        if (this.#nodes.sliderNode === null) {
            throw new Error(`Slider: по селектору ${sliderSelector} не найден элемент в DOM дереве`);
        }

        this.#nodes.sliderItemsNode = this.#nodes.sliderNode.querySelector(this.#cssSelectors.items);
        if (this.#nodes.sliderItemsNode === null) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.items} не найден элемент в DOM дереве`);
        }

        this.#nodes.sliderItemNodes = Array.from(this.#nodes.sliderNode.querySelectorAll(this.#cssSelectors.item));
        if (this.#nodes.sliderItemNodes.length === 0) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.item} не найдены элементы слайдера в DOM дереве`);
        }

        this.#nodes.sliderArrowLeftNode = this.#nodes.sliderNode.querySelector(this.#cssSelectors.arrowLeft);
        if (this.#nodes.sliderArrowLeftNode === null) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.arrowLeft} не найден элемент в DOM дереве`);
        }

        this.#nodes.sliderArrowRightNode = this.#nodes.sliderNode.querySelector(this.#cssSelectors.arrowRight);
        if (this.#nodes.sliderArrowLeftNode === null) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.arrowRight} не найден элемент в DOM дереве`);
        }

        this.#nodes.sliderWrapperNode = this.#nodes.sliderNode.querySelector(this.#cssSelectors.wrapper);
        if (this.#nodes.sliderWrapperNode === null) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.wrapper} не найден элемент в DOM дереве`);
        }

    }

    #initEventListeners() {
        window.addEventListener('resize', this.#debounce(this.#calcAndSetShiftX, 50));

        this.#nodes.sliderArrowLeftNode.addEventListener('click', () => {
            const nextIndex = this.#getNextIndex(-1);
            this.#changeSlide(nextIndex);
        });

        this.#nodes.sliderArrowRightNode.addEventListener('click', () => {
            const nextIndex = this.#getNextIndex(1);
            this.#changeSlide(nextIndex);
        });

        for (const sliderNode of this.#nodes.sliderItemNodes) {
            sliderNode.addEventListener('pointerdown', this.#dragStart);
        }

        document.addEventListener('pointermove', this.#dragging);
        // document.addEventListener('touchmove', this.#dragging);
        document.addEventListener('pointerup', this.#dragStop);
        document.addEventListener('pointerleave', (event) => {
            console.log('pointerleave');
            this.#dragStop();
        });
    }

    #dragStart = (e) => {
        if (e.button === 2 || e.button === 1) return false; // Если это правая или средняя кнопка мыши, это не тот клик
        this.#isDragging = true;
        this.#startMovePos = { x: e.clientX, y: e.clientY };
        this.#lastShifX = this.#shiftX;
        console.log('dragStart');
    }

    #dragStop = (e) => {
        this.#isDragging = false;
        if (this.#moveSlideShiftX === 0) return;
        const dir = this.#moveSlideShiftX < 0 ? 'left' : 'right';
        // console.log(this.#moveSlideShiftX, 'this.#moveSlideShiftX');
        // console.log(dir);
        const currentSliderNode = this.#nodes.sliderItemNodes[this.#activeItemIndex];
        // console.log(currentSliderNode);

        if (dir === 'left') {

        }

        this.#moveSlideShiftX = 0;
        this.#calcAndSetShiftX();
        console.log('dragStop');
    }

    #dragging = (e) => {
        if (this.#isDragging === false) return;

        const nextMovePos = { x: e.clientX, y: e.clientY };
        const diffMovePos = { x: nextMovePos.x - this.#startMovePos.x, y: nextMovePos.y - this.#startMovePos.y };

        this.#moveSlideShiftX = diffMovePos.x;
        this.#calcMoveSlideShiftX();
        this.#countDragging++;
        this.#teamsTitleNode.innerText = `${this.#countDragging} dragging`;

        console.log('dragging');
    }

    #getNextIndex(dir) {
        const quantityItems = this.#nodes.sliderItemNodes.length;
        return (this.#activeItemIndex + dir + quantityItems) % quantityItems;
    }

    #setCenteringShiftX = () => {
        const firstSliderItemNode = this.#nodes.sliderItemNodes[this.#activeItemIndex];
        const sliderItemsNode = this.#nodes.sliderItemsNode;

        const widthSlider = sliderItemsNode.offsetWidth;
        const widthFirstSliderItem = firstSliderItemNode.offsetWidth;

        this.#centeringShiftX = Math.max((widthSlider - widthFirstSliderItem) / 2, 0);
    }

    #setChangeSlideShiftX() {
        const firstRect = this.#nodes.sliderItemNodes[0].getBoundingClientRect();
        const nextRect = this.#nodes.sliderItemNodes[this.#activeItemIndex].getBoundingClientRect();
        const diffLeftNextSlide = nextRect.left - firstRect.left;

        this.#changeSlideShiftX = diffLeftNextSlide;
    }

    #setShiftX() {
        this.#shiftX = this.#centeringShiftX - this.#changeSlideShiftX;
        this.#nodes.sliderItemsNode.style.transform = `translateX(${this.#shiftX}px)`;
    }

    #calcAndSetShiftX = () => {
        this.#setCenteringShiftX();
        this.#setChangeSlideShiftX();
        this.#setShiftX();
    }

    #calcMoveSlideShiftX() {
        this.#shiftX = this.#lastShifX + this.#moveSlideShiftX;

        this.#nodes.sliderItemsNode.style.transform = `translateX(${this.#shiftX}px)`;
    }

    #changeSlide(nextIndex) {
        this.#activeItemIndex = nextIndex;
        this.#calcAndSetShiftX();
    }

    #debounce(func, delay) {
        let timeoutId;
        return function(...args) {
            if (timeoutId) {
                clearTimeout(timeoutId);
            }
            timeoutId = setTimeout(() => {
                func.apply(this, args);
            }, delay);
        };
    }

    #throttle(func, ms) {

        let isThrottled = false,
            savedArgs,
            savedThis;

        function wrapper() {

            if (isThrottled) { // (2)
                savedArgs = arguments;
                savedThis = this;
                return;
            }

            func.apply(this, arguments); // (1)

            isThrottled = true;

            setTimeout(function() {
                isThrottled = false; // (3)
                if (savedArgs) {
                    wrapper.apply(savedThis, savedArgs);
                    savedArgs = savedThis = null;
                }
            }, ms);
        }

        return wrapper;
    }

}

const slider = new Slider('.teams__slider');
*,
*::before,
*::after {
  box-sizing: inherit;
}

:root {
  --maxWidthContainer: 1094px;
}

html {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-size: 16px;
  font-family: 'Roboto', sans-serif;
}

img {
  display: block;
  max-width: 100%;
  max-height: 100%;
}

.container {
  max-width: var(--maxWidthContainer);
  display: block;
  margin: 0 auto;
  padding: 0 1em;
}

.slider {
  display: flex;
  flex-direction: column;
  gap: 32px;
  user-select: none;
}

.slider__arrows {
  display: flex;
  gap: 30px;
  margin: 0 auto;
  user-select: none;
}

.slider__items {
  display: flex;
  align-items: center;
  gap: 50px;
  user-select: none;
}

.slider__wrapper img {
  pointer-events: none;
}

.slider__item {
  cursor: grab;
}

.slider__arrow {
  cursor: pointer;
  padding: 20px;
  transition: background-color 0.2s ease;
}

.slider__arrow:hover {
  background-color: rgba(255, 255, 255, 0.1);
}

.teams {
  background-color: rgb(39, 55, 32);
  padding: 120px 0;
  overflow-x: hidden;
}

.teams__title {
  color: white;
  margin: 0;
  font-size: 4.5em;
  text-align: center;
}

.teams__wrapper {
  display: flex;
  flex-direction: column;
  gap: 57px;
}

.teams__slider-item {
  padding: 44px 82px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 30px;
  border-radius: 30px;
  --width: max(75%, 500px);
  min-width: var(--width);
  max-width: var(--width);
}

.teams__slider-item:nth-child(2n + 1) {
  /* нечётные */
  background-color: white;
}

.teams__slider-item:nth-child(2n) {
  /* чётные */
  background-color: #D2EBD5;
}

.teams__slider-item-img {
  --width: 128px;
  min-width: var(--width);
  max-width: var(--width);
}

.teams__slider-text {
  margin: 0;
  font-size: 2em;
}

.teams__slider-review {
  display: flex;
  align-items: center;
  gap: 24px;
}

.teams__slider-review-img {
  --width: 64px;
  min-width: var(--width);
  max-width: var(--width);
}

.teams__slider-review-text {
  font-size: 1.125em;
}

.as-console {
  display: none;
  visibility: hidden;
  opacity: 0;
  position: fixed;
  z-index: -1;
}
<section class="teams">
  <div class="container">
    <div class="teams__wrapper">
      <h2 class="teams__title">Trusted by top teams</h2>
      <div class="teams__slider slider">
        <div class="slider__wrapper">
          <div class="slider__items teams__slider-items">

            <div class="slider__item teams__slider-item">

              <div class="teams__slider-item-img-wrapper">
                <img src="https://picsum.photos/100" alt="" class="teams__slider-item-img">
              </div>
              <p class="teams__slider-text">
                “HyperComply has enabled us to complete questionnaires quickly and accurately, and has had a hugely positive effect on speeding up our sales cycle”
              </p>
              <div class="teams__slider-review">
                <div class="teams__slider-review-img-wrapper">
                  <img src="https://picsum.photos/50" alt="" class="teams__slider-review-img">
                </div>
                <div class="teams__slider-review-text">
                  Desiree R, Sr. Director of Information Security
                </div>
              </div>

            </div>
            <!-- .slider__item.teams__slider-item -->

            <div class="slider__item teams__slider-item">

              <div class="teams__slider-item-img-wrapper">
                <img src="https://picsum.photos/100" alt="" class="teams__slider-item-img">
              </div>
              <p class="teams__slider-text">
                “With HyperComply, we can now complete questionnaires with ease and accuracy, resulting in a faster sales cycle. This innovative tool has truly transformed the way we do business”
              </p>
              <div class="teams__slider-review">
                <div class="teams__slider-review-img-wrapper">
                  <img src="https://picsum.photos/50" alt="" class="teams__slider-review-img">
                </div>
                <div class="teams__slider-review-text">
                  Desiree R, Sr. Director of Information Security
                </div>

              </div>

            </div>
            <!-- .slider__item.teams__slider-item -->

            <div class="slider__item teams__slider-item">

              <div class="teams__slider-item-img-wrapper">
                <img src="https://picsum.photos/100" alt="" class="teams__slider-item-img">
              </div>
              <p class="teams__slider-text">
                “Thanks to HyperComply, we are able to breeze through questionnaires and speed up our sales cycle. This game-changing technology has made a significant impact on our business operations.”
              </p>
              <div class="teams__slider-review">
                <div class="teams__slider-review-img-wrapper">
                  <img src="https://picsum.photos/50" alt="" class="teams__slider-review-img">
                </div>
                <div class="teams__slider-review-text">
                  Desiree R, Sr. Director of Information Security
                </div>

              </div>

            </div>

          </div>

        </div>

        <div class="slider__arrows">
          <div class="slider__arrow slider__arrow_left">
            <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
              xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="560 120 583 1000" id="svg3001" version="1.1" inkscape:version="0.48.3.1 r9886" width="20px" height="auto" sodipodi:docname="angle_left_font_awesome.svg">
  <metadata id="metadata3011">
    <rdf:RDF>
      <cc:Work rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs id="defs3009"/>
  <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="640" inkscape:window-height="480" id="namedview3007" showgrid="false" inkscape:zoom="0.13169643" inkscape:cx="896" inkscape:cy="896" inkscape:window-x="0" inkscape:window-y="25" inkscape:window-maximized="0" inkscape:current-layer="svg3001"/>
  <g transform="matrix(1,0,0,-1,516.33898,1194.3051)" id="g3003">
    <path fill="white" d="m 627,992 q 0,-13 -10,-23 L 224,576 617,183 q 10,-10 10,-23 0,-13 -10,-23 L 567,87 Q 557,77 544,77 531,77 521,87 L 55,553 q -10,10 -10,23 0,13 10,23 l 466,466 q 10,10 23,10 13,0 23,-10 l 50,-50 q 10,-10 10,-23 z" id="path3005" inkscape:connector-curvature="0"/>
  </g>
</svg>

          </div>
          <div class="slider__arrow slider__arrow_right">
            <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
              xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="560 120 583 1000" id="svg3001" version="1.1" inkscape:version="0.48.3.1 r9886" width="20px" height="auto" style="transform: scaleX(-1)" sodipodi:docname="angle_left_font_awesome.svg">
  <metadata id="metadata3011">
    <rdf:RDF>
      <cc:Work rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs id="defs3009"/>
  <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="640" inkscape:window-height="480" id="namedview3007" showgrid="false" inkscape:zoom="0.13169643" inkscape:cx="896" inkscape:cy="896" inkscape:window-x="0" inkscape:window-y="25" inkscape:window-maximized="0" inkscape:current-layer="svg3001"/>
  <g transform="matrix(1,0,0,-1,516.33898,1194.3051)" id="g3003">
    <path fill="white" d="m 627,992 q 0,-13 -10,-23 L 224,576 617,183 q 10,-10 10,-23 0,-13 -10,-23 L 567,87 Q 557,77 544,77 531,77 521,87 L 55,553 q -10,10 -10,23 0,13 10,23 l 466,466 q 10,10 23,10 13,0 23,-10 l 50,-50 q 10,-10 10,-23 z" id="path3005" inkscape:connector-curvature="0"/>
  </g>
</svg>

          </div>
        </div>
      </div>
    </div>
</section>

Ответы

▲ 0Принят

Нужно было прокручиваему элементу, .slider__items написать touch-action: pan-y, чтобы указать браузеру, что этот элемент может браузером прокручиваться только по оси y. Но не по оси x, ось x будет занята для кручения слайдера

class Slider {

    #nodes = {
        sliderNode: null,
        sliderItemsNode: null,
        sliderItemNodes: [],
        sliderArrowLeftNode: null,
        sliderArrowRightNode: null,
        sliderWrapperNode: null
    };

    #cssSelectors = {
        items: '.slider__items',
        item: '.slider__item',
        wrapper: '.slider__wrapper',
        arrowLeft: '.slider__arrow_left',
        arrowRight: '.slider__arrow_right'
    };

    #centeringShiftX = 0;
    #changeSlideShiftX = 0;
    #moveSlideShiftX = 0;
    #shiftX = 0;
    #lastShifX = 0;

    #startMovePos = {
        x: 0,
        y: 0
    };

    #activeItemIndex = 0;

    #isDragging = false;
    #countDragging = 0;

    #teamsTitleNode = document.querySelector('.teams__title');


    constructor(sliderSelector) {
        this.#initNodes(sliderSelector);
        this.#initEventListeners();
        this.#calcAndSetShiftX();
    }

    #initNodes(sliderSelector) {
        this.#nodes.sliderNode = document.querySelector(sliderSelector);
        if (this.#nodes.sliderNode === null) {
            throw new Error(`Slider: по селектору ${sliderSelector} не найден элемент в DOM дереве`);
        }

        this.#nodes.sliderItemsNode = this.#nodes.sliderNode.querySelector(this.#cssSelectors.items);
        if (this.#nodes.sliderItemsNode === null) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.items} не найден элемент в DOM дереве`);
        }

        this.#nodes.sliderItemNodes = Array.from(this.#nodes.sliderNode.querySelectorAll(this.#cssSelectors.item));
        if (this.#nodes.sliderItemNodes.length === 0) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.item} не найдены элементы слайдера в DOM дереве`);
        }

        this.#nodes.sliderArrowLeftNode = this.#nodes.sliderNode.querySelector(this.#cssSelectors.arrowLeft);
        if (this.#nodes.sliderArrowLeftNode === null) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.arrowLeft} не найден элемент в DOM дереве`);
        }

        this.#nodes.sliderArrowRightNode = this.#nodes.sliderNode.querySelector(this.#cssSelectors.arrowRight);
        if (this.#nodes.sliderArrowLeftNode === null) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.arrowRight} не найден элемент в DOM дереве`);
        }

        this.#nodes.sliderWrapperNode = this.#nodes.sliderNode.querySelector(this.#cssSelectors.wrapper);
        if (this.#nodes.sliderWrapperNode === null) {
            throw new Error(`Slider: по селектору ${this.#cssSelectors.wrapper} не найден элемент в DOM дереве`);
        }

    }

    #initEventListeners() {
        window.addEventListener('resize', this.#debounce(this.#calcAndSetShiftX, 50));

        this.#nodes.sliderArrowLeftNode.addEventListener('click', () => {
            const nextIndex = this.#getNextIndex(-1);
            this.#changeSlide(nextIndex);
        });

        this.#nodes.sliderArrowRightNode.addEventListener('click', () => {
            const nextIndex = this.#getNextIndex(1);
            this.#changeSlide(nextIndex);
        });

        for (const sliderNode of this.#nodes.sliderItemNodes) {
            sliderNode.addEventListener('pointerdown', this.#dragStart);
        }

        document.addEventListener('pointermove', this.#dragging);
        // document.addEventListener('touchmove', this.#dragging);
        document.addEventListener('pointerup', this.#dragStop);
        document.addEventListener('pointerleave', (event) => {
            console.log('pointerleave');
            this.#dragStop();
        });
    }

    #dragStart = (e) => {
        if (e.button === 2 || e.button === 1) return false; // Если это правая или средняя кнопка мыши, это не тот клик
        this.#isDragging = true;
        this.#startMovePos = { x: e.clientX, y: e.clientY };
        this.#lastShifX = this.#shiftX;
        console.log('dragStart');
    }

    #dragStop = (e) => {
        this.#isDragging = false;
        if (this.#moveSlideShiftX === 0) return;
        const dir = this.#moveSlideShiftX < 0 ? 'left' : 'right';
        // console.log(this.#moveSlideShiftX, 'this.#moveSlideShiftX');
        // console.log(dir);
        const currentSliderNode = this.#nodes.sliderItemNodes[this.#activeItemIndex];
        // console.log(currentSliderNode);

        if (dir === 'left') {

        }

        this.#moveSlideShiftX = 0;
        this.#calcAndSetShiftX();
        console.log('dragStop');
    }

    #dragging = (e) => {
        if (this.#isDragging === false) return;

        const nextMovePos = { x: e.clientX, y: e.clientY };
        const diffMovePos = { x: nextMovePos.x - this.#startMovePos.x, y: nextMovePos.y - this.#startMovePos.y };

        this.#moveSlideShiftX = diffMovePos.x;
        this.#calcMoveSlideShiftX();
        this.#countDragging++;
        this.#teamsTitleNode.innerText = `${this.#countDragging} dragging`;

        console.log('dragging');
    }

    #getNextIndex(dir) {
        const quantityItems = this.#nodes.sliderItemNodes.length;
        return (this.#activeItemIndex + dir + quantityItems) % quantityItems;
    }

    #setCenteringShiftX = () => {
        const firstSliderItemNode = this.#nodes.sliderItemNodes[this.#activeItemIndex];
        const sliderItemsNode = this.#nodes.sliderItemsNode;

        const widthSlider = sliderItemsNode.offsetWidth;
        const widthFirstSliderItem = firstSliderItemNode.offsetWidth;

        this.#centeringShiftX = Math.max((widthSlider - widthFirstSliderItem) / 2, 0);
    }

    #setChangeSlideShiftX() {
        const firstRect = this.#nodes.sliderItemNodes[0].getBoundingClientRect();
        const nextRect = this.#nodes.sliderItemNodes[this.#activeItemIndex].getBoundingClientRect();
        const diffLeftNextSlide = nextRect.left - firstRect.left;

        this.#changeSlideShiftX = diffLeftNextSlide;
    }

    #setShiftX() {
        this.#shiftX = this.#centeringShiftX - this.#changeSlideShiftX;
        this.#nodes.sliderItemsNode.style.transform = `translateX(${this.#shiftX}px)`;
    }

    #calcAndSetShiftX = () => {
        this.#setCenteringShiftX();
        this.#setChangeSlideShiftX();
        this.#setShiftX();
    }

    #calcMoveSlideShiftX() {
        this.#shiftX = this.#lastShifX + this.#moveSlideShiftX;

        this.#nodes.sliderItemsNode.style.transform = `translateX(${this.#shiftX}px)`;
    }

    #changeSlide(nextIndex) {
        this.#activeItemIndex = nextIndex;
        this.#calcAndSetShiftX();
    }

    #debounce(func, delay) {
        let timeoutId;
        return function(...args) {
            if (timeoutId) {
                clearTimeout(timeoutId);
            }
            timeoutId = setTimeout(() => {
                func.apply(this, args);
            }, delay);
        };
    }

    #throttle(func, ms) {

        let isThrottled = false,
            savedArgs,
            savedThis;

        function wrapper() {

            if (isThrottled) { // (2)
                savedArgs = arguments;
                savedThis = this;
                return;
            }

            func.apply(this, arguments); // (1)

            isThrottled = true;

            setTimeout(function() {
                isThrottled = false; // (3)
                if (savedArgs) {
                    wrapper.apply(savedThis, savedArgs);
                    savedArgs = savedThis = null;
                }
            }, ms);
        }

        return wrapper;
    }

}

const slider = new Slider('.teams__slider');
*,
*::before,
*::after {
  box-sizing: inherit;
}

:root {
  --maxWidthContainer: 1094px;
}

html {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-size: 16px;
  font-family: 'Roboto', sans-serif;
}

img {
  display: block;
  max-width: 100%;
  max-height: 100%;
}

.container {
  max-width: var(--maxWidthContainer);
  display: block;
  margin: 0 auto;
  padding: 0 1em;
}

.slider {
  display: flex;
  flex-direction: column;
  gap: 32px;
  user-select: none;
}

.slider__arrows {
  display: flex;
  gap: 30px;
  margin: 0 auto;
  user-select: none;
}

.slider__items {
  display: flex;
  align-items: center;
  gap: 50px;
  user-select: none;
  touch-action: pan-y;
}

.slider__wrapper img {
  pointer-events: none;
}

.slider__item {
  cursor: grab;
}

.slider__arrow {
  cursor: pointer;
  padding: 20px;
  transition: background-color 0.2s ease;
}

.slider__arrow:hover {
  background-color: rgba(255, 255, 255, 0.1);
}

.teams {
  background-color: rgb(39, 55, 32);
  padding: 120px 0;
  overflow-x: hidden;
}

.teams__title {
  color: white;
  margin: 0;
  font-size: 4.5em;
  text-align: center;
}

.teams__wrapper {
  display: flex;
  flex-direction: column;
  gap: 57px;
}

.teams__slider-item {
  padding: 44px 82px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 30px;
  border-radius: 30px;
  --width: max(75%, 500px);
  min-width: var(--width);
  max-width: var(--width);
}

.teams__slider-item:nth-child(2n + 1) {
  /* нечётные */
  background-color: white;
}

.teams__slider-item:nth-child(2n) {
  /* чётные */
  background-color: #D2EBD5;
}

.teams__slider-item-img {
  --width: 128px;
  min-width: var(--width);
  max-width: var(--width);
}

.teams__slider-text {
  margin: 0;
  font-size: 2em;
}

.teams__slider-review {
  display: flex;
  align-items: center;
  gap: 24px;
}

.teams__slider-review-img {
  --width: 64px;
  min-width: var(--width);
  max-width: var(--width);
}

.teams__slider-review-text {
  font-size: 1.125em;
}

.as-console {
  display: none;
  visibility: hidden;
  opacity: 0;
  position: fixed;
  z-index: -1;
}
<section class="teams">
  <div class="container">
    <div class="teams__wrapper">
      <h2 class="teams__title">Trusted by top teams</h2>
      <div class="teams__slider slider">
        <div class="slider__wrapper">
          <div class="slider__items teams__slider-items">

            <div class="slider__item teams__slider-item">

              <div class="teams__slider-item-img-wrapper">
                <img src="https://picsum.photos/100" alt="" class="teams__slider-item-img">
              </div>
              <p class="teams__slider-text">
                “HyperComply has enabled us to complete questionnaires quickly and accurately, and has had a hugely positive effect on speeding up our sales cycle”
              </p>
              <div class="teams__slider-review">
                <div class="teams__slider-review-img-wrapper">
                  <img src="https://picsum.photos/50" alt="" class="teams__slider-review-img">
                </div>
                <div class="teams__slider-review-text">
                  Desiree R, Sr. Director of Information Security
                </div>
              </div>

            </div>
            <!-- .slider__item.teams__slider-item -->

            <div class="slider__item teams__slider-item">

              <div class="teams__slider-item-img-wrapper">
                <img src="https://picsum.photos/100" alt="" class="teams__slider-item-img">
              </div>
              <p class="teams__slider-text">
                “With HyperComply, we can now complete questionnaires with ease and accuracy, resulting in a faster sales cycle. This innovative tool has truly transformed the way we do business”
              </p>
              <div class="teams__slider-review">
                <div class="teams__slider-review-img-wrapper">
                  <img src="https://picsum.photos/50" alt="" class="teams__slider-review-img">
                </div>
                <div class="teams__slider-review-text">
                  Desiree R, Sr. Director of Information Security
                </div>

              </div>

            </div>
            <!-- .slider__item.teams__slider-item -->

            <div class="slider__item teams__slider-item">

              <div class="teams__slider-item-img-wrapper">
                <img src="https://picsum.photos/100" alt="" class="teams__slider-item-img">
              </div>
              <p class="teams__slider-text">
                “Thanks to HyperComply, we are able to breeze through questionnaires and speed up our sales cycle. This game-changing technology has made a significant impact on our business operations.”
              </p>
              <div class="teams__slider-review">
                <div class="teams__slider-review-img-wrapper">
                  <img src="https://picsum.photos/50" alt="" class="teams__slider-review-img">
                </div>
                <div class="teams__slider-review-text">
                  Desiree R, Sr. Director of Information Security
                </div>

              </div>

            </div>

          </div>

        </div>

        <div class="slider__arrows">
          <div class="slider__arrow slider__arrow_left">
            <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
              xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="560 120 583 1000" id="svg3001" version="1.1" inkscape:version="0.48.3.1 r9886" width="20px" height="auto" sodipodi:docname="angle_left_font_awesome.svg">
  <metadata id="metadata3011">
    <rdf:RDF>
      <cc:Work rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs id="defs3009"/>
  <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="640" inkscape:window-height="480" id="namedview3007" showgrid="false" inkscape:zoom="0.13169643" inkscape:cx="896" inkscape:cy="896" inkscape:window-x="0" inkscape:window-y="25" inkscape:window-maximized="0" inkscape:current-layer="svg3001"/>
  <g transform="matrix(1,0,0,-1,516.33898,1194.3051)" id="g3003">
    <path fill="white" d="m 627,992 q 0,-13 -10,-23 L 224,576 617,183 q 10,-10 10,-23 0,-13 -10,-23 L 567,87 Q 557,77 544,77 531,77 521,87 L 55,553 q -10,10 -10,23 0,13 10,23 l 466,466 q 10,10 23,10 13,0 23,-10 l 50,-50 q 10,-10 10,-23 z" id="path3005" inkscape:connector-curvature="0"/>
  </g>
</svg>

          </div>
          <div class="slider__arrow slider__arrow_right">
            <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
              xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="560 120 583 1000" id="svg3001" version="1.1" inkscape:version="0.48.3.1 r9886" width="20px" height="auto" style="transform: scaleX(-1)" sodipodi:docname="angle_left_font_awesome.svg">
  <metadata id="metadata3011">
    <rdf:RDF>
      <cc:Work rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs id="defs3009"/>
  <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="640" inkscape:window-height="480" id="namedview3007" showgrid="false" inkscape:zoom="0.13169643" inkscape:cx="896" inkscape:cy="896" inkscape:window-x="0" inkscape:window-y="25" inkscape:window-maximized="0" inkscape:current-layer="svg3001"/>
  <g transform="matrix(1,0,0,-1,516.33898,1194.3051)" id="g3003">
    <path fill="white" d="m 627,992 q 0,-13 -10,-23 L 224,576 617,183 q 10,-10 10,-23 0,-13 -10,-23 L 567,87 Q 557,77 544,77 531,77 521,87 L 55,553 q -10,10 -10,23 0,13 10,23 l 466,466 q 10,10 23,10 13,0 23,-10 l 50,-50 q 10,-10 10,-23 z" id="path3005" inkscape:connector-curvature="0"/>
  </g>
</svg>

          </div>
        </div>
      </div>
    </div>
</section>