Skip to content

Commit

Permalink
Carousels: Optimise updating button state (#12923)
Browse files Browse the repository at this point in the history
* Toggle button state when card half in or out of view

* Throttle scroll events with requestAnimationFrame

* Throttle scroll events with timeout as no animation

* Add comments to event throttling functions
  • Loading branch information
jamesmockett authored Nov 28, 2024
1 parent e35a3c6 commit 3fd1c24
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 19 deletions.
32 changes: 25 additions & 7 deletions dotcom-rendering/src/components/ScrollableCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,9 @@ export const ScrollableCarousel = ({
* Updates state of navigation buttons based on carousel's scroll position.
*
* This function checks the current scroll position of the carousel and sets
* the styles of the previous and next buttons accordingly. The previous
* button is disabled if the carousel is at the start, and the next button
* is disabled if the carousel is at the end.
* the styles of the previous and next buttons accordingly. The button state
* is toggled when the midpoint of the first or last card has been scrolled
* in or out of view.
*/
const updateButtonVisibilityOnScroll = () => {
const carouselElement = carouselRef.current;
Expand All @@ -299,9 +299,27 @@ export const ScrollableCarousel = ({
const scrollLeft = carouselElement.scrollLeft;
const maxScrollLeft =
carouselElement.scrollWidth - carouselElement.clientWidth;
const cardWidth = carouselElement.querySelector('li')?.offsetWidth ?? 0;

setPreviousButtonEnabled(scrollLeft > 0);
setNextButtonEnabled(scrollLeft < maxScrollLeft);
setPreviousButtonEnabled(scrollLeft > cardWidth / 2);
setNextButtonEnabled(scrollLeft < maxScrollLeft - cardWidth / 2);
};

/**
* Throttle scroll events to optimise performance. As we're only using this
* to toggle button state as the carousel is scrolled we don't need to
* handle every event. This function ensures the callback is only called
* once every 200ms, no matter how many scroll events are fired.
*/
const throttleEvent = (callback: () => void) => {
let isThrottled: boolean = false;
return function () {
if (!isThrottled) {
callback();
isThrottled = true;
setTimeout(() => (isThrottled = false), 200);
}
};
};

useEffect(() => {
Expand All @@ -310,13 +328,13 @@ export const ScrollableCarousel = ({

carouselElement.addEventListener(
'scroll',
updateButtonVisibilityOnScroll,
throttleEvent(updateButtonVisibilityOnScroll),
);

return () => {
carouselElement.removeEventListener(
'scroll',
updateButtonVisibilityOnScroll,
throttleEvent(updateButtonVisibilityOnScroll),
);
};
}, []);
Expand Down
34 changes: 22 additions & 12 deletions dotcom-rendering/src/components/SlideshowCarousel.importable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,26 +124,36 @@ export const SlideshowCarousel = ({
* Updates state of navigation buttons based on carousel's scroll position.
*
* This function checks the current scroll position of the carousel and sets
* the styles of the previous and next buttons accordingly. The previous
* button is disabled if the carousel is at the start, and the next button
* is disabled if the carousel is at the end.
* the styles of the previous and next buttons accordingly. The button state
* is toggled when the midpoint of the first or last card has been scrolled
* in or out of view.
*/
const updatePaginationStateOnScroll = () => {
const carouselElement = carouselRef.current;
if (!carouselElement) return;

const scrollLeft = carouselElement.scrollLeft;

const maxScrollLeft =
carouselElement.scrollWidth - carouselElement.clientWidth;

setPreviousButtonEnabled(scrollLeft > 0);
setNextButtonEnabled(scrollLeft < maxScrollLeft);

const cardWidth = carouselElement.querySelector('li')?.offsetWidth ?? 0;
const page = Math.round(scrollLeft / cardWidth);

setCurrentPage(page);
setPreviousButtonEnabled(scrollLeft > cardWidth / 2);
setNextButtonEnabled(scrollLeft < maxScrollLeft - cardWidth / 2);
setCurrentPage(Math.round(scrollLeft / cardWidth));
};

/**
* Throttle scroll events to optimise performance. As the scroll events are
* used to trigger the pagination dot animation we're using
* `requestAnimationFrame` rather than `setTimeout` to ensure this animates
* smoothly in sync with the carousel being scrolled.
*/
const throttleEvent = (callback: () => void) => {
let requestId: number;
return function () {
cancelAnimationFrame(requestId);
requestId = requestAnimationFrame(callback);
};
};

useEffect(() => {
Expand All @@ -152,13 +162,13 @@ export const SlideshowCarousel = ({

carouselElement.addEventListener(
'scroll',
updatePaginationStateOnScroll,
throttleEvent(updatePaginationStateOnScroll),
);

return () => {
carouselElement.removeEventListener(
'scroll',
updatePaginationStateOnScroll,
throttleEvent(updatePaginationStateOnScroll),
);
};
}, []);
Expand Down

0 comments on commit 3fd1c24

Please sign in to comment.