Skip to content

Commit

Permalink
Carousel: Ensure autoplay is reset on interactions and correctly stop…
Browse files Browse the repository at this point in the history
…s on slider focus/mouseover (#32897)
  • Loading branch information
Mitch-At-Work authored Sep 24, 2024
1 parent 871477e commit 6c31158
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Add resetAutoplay feature to reset index change timer",
"packageName": "@fluentui/react-carousel-preview",
"email": "mifraser@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export type CarouselContextValue = {
selectPageByIndex: (event: React_2.MouseEvent<HTMLButtonElement | HTMLAnchorElement>, value: number, jump?: boolean) => void;
subscribeForValues: (listener: (data: CarouselUpdateData) => void) => () => void;
enableAutoplay: (autoplay: boolean) => void;
resetAutoplay: () => void;
containerRef?: React_2.RefObject<HTMLDivElement>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,17 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref<HTMLDi
} = props;

const { dir } = useFluent();
const { activeIndex, carouselApi, containerRef, subscribeForValues, enableAutoplay } = useEmblaCarousel({
align,
direction: dir,
loop: circular,
slidesToScroll: groupSize,
defaultActiveIndex: props.defaultActiveIndex,
activeIndex: props.activeIndex,
watchDrag: draggable,
containScroll: whitespace ? false : 'keepSnaps',
});
const { activeIndex, carouselApi, containerRef, subscribeForValues, enableAutoplay, resetAutoplay } =
useEmblaCarousel({
align,
direction: dir,
loop: circular,
slidesToScroll: groupSize,
defaultActiveIndex: props.defaultActiveIndex,
activeIndex: props.activeIndex,
watchDrag: draggable,
containScroll: whitespace ? false : 'keepSnaps',
});

const selectPageByElement: CarouselContextValue['selectPageByElement'] = useEventCallback((event, element, jump) => {
const foundIndex = carouselApi.scrollToElement(element, jump);
Expand Down Expand Up @@ -82,5 +83,6 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref<HTMLDi
selectPageByIndex,
subscribeForValues,
enableAutoplay,
resetAutoplay,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function useCarouselContextValues_unstable(state: CarouselState): Carouse
selectPageByIndex,
subscribeForValues,
enableAutoplay,
resetAutoplay,
circular,
containerRef,
} = state;
Expand All @@ -23,6 +24,7 @@ export function useCarouselContextValues_unstable(state: CarouselState): Carouse
selectPageByIndex,
subscribeForValues,
enableAutoplay,
resetAutoplay,
circular,
containerRef,
}),
Expand All @@ -33,6 +35,7 @@ export function useCarouselContextValues_unstable(state: CarouselState): Carouse
selectPageByIndex,
subscribeForValues,
enableAutoplay,
resetAutoplay,
circular,
containerRef,
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const useCarouselButton_unstable = (
const containerRef = useCarouselContext(ctx => ctx.containerRef);
const selectPageByDirection = useCarouselContext(ctx => ctx.selectPageByDirection);
const subscribeForValues = useCarouselContext(ctx => ctx.subscribeForValues);
const resetAutoplay = useCarouselContext(ctx => ctx.resetAutoplay);

const isTrailing = useCarouselContext(ctx => {
if (circular) {
Expand Down Expand Up @@ -77,6 +78,8 @@ export const useCarouselButton_unstable = (
}
});
}

resetAutoplay();
};

useIsomorphicLayoutEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const carouselContextDefaultValue: CarouselContextValue = {
enableAutoplay: () => {
/** noop */
},
resetAutoplay: () => {
/** noop */
},
circular: false,
containerRef: undefined,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type CarouselContextValue = {
) => void;
subscribeForValues: (listener: (data: CarouselUpdateData) => void) => () => void;
enableAutoplay: (autoplay: boolean) => void;
resetAutoplay: () => void;
containerRef?: React.RefObject<HTMLDivElement>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ export const useCarouselNavButton_unstable = (
const selectPageByIndex = useCarouselContext(ctx => ctx.selectPageByIndex);
const selected = useCarouselContext(ctx => ctx.activeIndex === index);
const subscribeForValues = useCarouselContext(ctx => ctx.subscribeForValues);
const resetAutoplay = useCarouselContext(ctx => ctx.resetAutoplay);

const handleClick: ARIAButtonSlotProps['onClick'] = useEventCallback(event => {
onClick?.(event);

if (!event.defaultPrevented && isHTMLElement(event.target)) {
selectPageByIndex(event, index);
}

// Ensure any autoplay timers are extended/reset
resetAutoplay();
});

const defaultTabProps = useTabsterAttributes({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { carouselSliderClassNames } from './CarouselSlider/useCarouselSliderStyl
import { CarouselUpdateData, CarouselVisibilityEventDetail } from '../Carousel';
import Autoplay from 'embla-carousel-autoplay';

const sliderClassname = `.${carouselSliderClassNames.root}`;

const DEFAULT_EMBLA_OPTIONS: EmblaOptionsType = {
containScroll: 'trimSnaps',
inViewThreshold: 0.99,
watchDrag: false,
skipSnaps: true,

container: `.${carouselSliderClassNames.root}`,
container: sliderClassname,
slides: `.${carouselCardClassNames.root}`,
};

Expand Down Expand Up @@ -56,18 +58,27 @@ export function useEmblaCarousel(
});

const emblaApi = React.useRef<EmblaCarouselType | null>(null);

const autoplayRef = React.useRef<boolean>(false);
/* Our autoplay button, which is required by standards for autoplay to be enabled, will handle controlled state */
const enableAutoplay = React.useCallback((autoplay: boolean) => {
autoplayRef.current = autoplay;
if (autoplay) {
emblaApi.current?.plugins().autoplay.play();
} else {
emblaApi.current?.plugins().autoplay.stop();
}

const resetAutoplay = React.useCallback(() => {
emblaApi.current?.plugins().autoplay.reset();
}, []);

/* Our autoplay button, which is required by standards for autoplay to be enabled, will handle controlled state */
const enableAutoplay = React.useCallback(
(autoplay: boolean) => {
autoplayRef.current = autoplay;
if (autoplay) {
emblaApi.current?.plugins().autoplay.play();
// Reset after play to ensure timing and any focus/mouse pause state is reset.
resetAutoplay();
} else {
emblaApi.current?.plugins().autoplay.stop();
}
},
[resetAutoplay],
);

// Listeners contains callbacks for UI elements that may require state update based on embla changes
const listeners = React.useRef(new Set<(data: CarouselUpdateData) => void>());
const subscribeForValues = React.useCallback((listener: (data: CarouselUpdateData) => void) => {
Expand Down Expand Up @@ -144,6 +155,9 @@ export function useEmblaCarousel(
stopOnInteraction: !autoplayRef.current,
stopOnMouseEnter: true,
stopOnFocusIn: true,
rootNode: (emblaRoot: HTMLElement) => {
return emblaRoot.querySelector(sliderClassname) ?? emblaRoot;
},
}),
],
);
Expand Down Expand Up @@ -207,6 +221,9 @@ export function useEmblaCarousel(
stopOnInteraction: !autoplayRef.current,
stopOnMouseEnter: true,
stopOnFocusIn: true,
rootNode: (emblaRoot: HTMLElement) => {
return emblaRoot.querySelector(sliderClassname) ?? emblaRoot;
},
}),
],
);
Expand All @@ -218,5 +235,6 @@ export function useEmblaCarousel(
containerRef,
subscribeForValues,
enableAutoplay,
resetAutoplay,
};
}

0 comments on commit 6c31158

Please sign in to comment.