From 87e1027479eb66d6d4abff68184872e3d5fa9e8c Mon Sep 17 00:00:00 2001 From: Samuel Shingler Date: Thu, 11 Jul 2024 10:34:22 +1200 Subject: [PATCH] Implement #936. --- .../src/components/AutoScroll.ts | 6 +-- .../src/components/Autoplay.ts | 7 ++- .../src/content/pages/api/options.mdx | 22 ++++++-- .../src/components/EmblaCarousel.ts | 2 +- .../embla-carousel/src/components/Engine.ts | 6 ++- .../src/components/EventHandler.ts | 1 + .../embla-carousel/src/components/Options.ts | 5 +- .../src/components/SlideFocus.ts | 53 +++++++++++++------ 8 files changed, 72 insertions(+), 30 deletions(-) diff --git a/packages/embla-carousel-auto-scroll/src/components/AutoScroll.ts b/packages/embla-carousel-auto-scroll/src/components/AutoScroll.ts index 4e49d81a2..485aef165 100644 --- a/packages/embla-carousel-auto-scroll/src/components/AutoScroll.ts +++ b/packages/embla-carousel-auto-scroll/src/components/AutoScroll.ts @@ -83,10 +83,7 @@ function AutoScroll(userOptions: AutoScrollOptionsType = {}): AutoScrollType { } if (options.stopOnFocusIn) { - eventStore.add(container, 'focusin', () => { - stopScroll() - emblaApi.scrollTo(emblaApi.selectedScrollSnap(), true) - }) + emblaApi.on('slideFocusStart', stopScroll) if (!options.stopOnInteraction) { eventStore.add(container, 'focusout', startScroll) @@ -100,6 +97,7 @@ function AutoScroll(userOptions: AutoScrollOptionsType = {}): AutoScrollType { emblaApi .off('pointerDown', stopScroll) .off('pointerUp', startScrollOnSettle) + .off('slideFocusStart', stopScroll) .off('settle', onSettle) stopScroll() destroyed = true diff --git a/packages/embla-carousel-autoplay/src/components/Autoplay.ts b/packages/embla-carousel-autoplay/src/components/Autoplay.ts index 1baa6a887..86e9fff17 100644 --- a/packages/embla-carousel-autoplay/src/components/Autoplay.ts +++ b/packages/embla-carousel-autoplay/src/components/Autoplay.ts @@ -79,7 +79,7 @@ function Autoplay(userOptions: AutoplayOptionsType = {}): AutoplayType { } if (options.stopOnFocusIn) { - eventStore.add(container, 'focusin', stopTimer) + emblaApi.on('slideFocusStart', stopTimer) if (!options.stopOnInteraction) { eventStore.add(container, 'focusout', startTimer) @@ -92,7 +92,10 @@ function Autoplay(userOptions: AutoplayOptionsType = {}): AutoplayType { } function destroy(): void { - emblaApi.off('pointerDown', stopTimer).off('pointerUp', startTimer) + emblaApi + .off('pointerDown', stopTimer) + .off('pointerUp', startTimer) + .off('slideFocusStart', stopTimer) stopTimer() destroyed = true playing = false diff --git a/packages/embla-carousel-docs/src/content/pages/api/options.mdx b/packages/embla-carousel-docs/src/content/pages/api/options.mdx index fd0e3ffd9..c6c627c45 100644 --- a/packages/embla-carousel-docs/src/content/pages/api/options.mdx +++ b/packages/embla-carousel-docs/src/content/pages/api/options.mdx @@ -604,7 +604,23 @@ Enables for scrolling the carousel with mouse and touch interactions. Set this t **Note:** When passing a custom callback it will run **before** the default Embla drag behaviour. Return `true` in your callback if you want Embla to run its default drag behaviour after your callback, or return `false` if you want - to disable it. + to skip it. + + +--- + +### watchFocus + +Type: `boolean | (emblaApi: EmblaCarouselType, event: FocusEvent) => boolean | void` +Default: `true` + +Embla automatically watches the [slides](api/options/#slides) for focus events. The default callback fires the [slideFocus](/api/events/#slidefocus/) event and [scrolls](/api/methods/#scrollto/) to the focused element. Set this to `false` to disable this behaviour or pass a custom callback to add your own focus logic. + + + **Note:** When passing a custom callback it will run **before** the default + Embla focus behaviour. Return `true` in your callback if you want Embla to run + its default focus behaviour after your callback, or return `false` if you want + to skip it. --- @@ -620,7 +636,7 @@ Embla automatically watches the [container](/api/methods/#containernode/) and [s **Note:** When passing a custom callback it will run **before** the default Embla resize behaviour. Return `true` in your callback if you want Embla to run its default resize behaviour after your callback, or return `false` if you - want to disable it. + want to skip it. --- @@ -636,7 +652,7 @@ Embla automatically watches the [container](/api/methods/#containernode/) for ** **Note:** When passing a custom callback it will run **before** the default Embla mutation behaviour. Return `true` in your callback if you want Embla to run its default mutation behaviour after your callback, or return `false` if - you want to disable it. + you want to skip it. --- diff --git a/packages/embla-carousel/src/components/EmblaCarousel.ts b/packages/embla-carousel/src/components/EmblaCarousel.ts index 9b8148737..b620cb814 100644 --- a/packages/embla-carousel/src/components/EmblaCarousel.ts +++ b/packages/embla-carousel/src/components/EmblaCarousel.ts @@ -112,7 +112,7 @@ function EmblaCarousel( engine.translate.to(engine.location.get()) engine.animation.init() engine.slidesInView.init() - engine.slideFocus.init() + engine.slideFocus.init(self) engine.eventHandler.init(self) engine.resizeHandler.init(self) engine.slidesHandler.init(self) diff --git a/packages/embla-carousel/src/components/Engine.ts b/packages/embla-carousel/src/components/Engine.ts index f2a7d3626..c00e2d53f 100644 --- a/packages/embla-carousel/src/components/Engine.ts +++ b/packages/embla-carousel/src/components/Engine.ts @@ -99,7 +99,8 @@ export function Engine( containScroll, watchResize, watchSlides, - watchDrag + watchDrag, + watchFocus } = options // Measurements @@ -263,7 +264,8 @@ export function Engine( scrollTo, scrollBody, eventStore, - eventHandler + eventHandler, + watchFocus ) // Engine diff --git a/packages/embla-carousel/src/components/EventHandler.ts b/packages/embla-carousel/src/components/EventHandler.ts index aa911d748..cc2c1e942 100644 --- a/packages/embla-carousel/src/components/EventHandler.ts +++ b/packages/embla-carousel/src/components/EventHandler.ts @@ -17,6 +17,7 @@ export interface EmblaEventListType { destroy: 'destroy' reInit: 'reInit' resize: 'resize' + slideFocusStart: 'slideFocusStart' slideFocus: 'slideFocus' } diff --git a/packages/embla-carousel/src/components/Options.ts b/packages/embla-carousel/src/components/Options.ts index cfee3164e..b42583f69 100644 --- a/packages/embla-carousel/src/components/Options.ts +++ b/packages/embla-carousel/src/components/Options.ts @@ -6,6 +6,7 @@ import { DragHandlerOptionType } from './DragHandler' import { ResizeHandlerOptionType } from './ResizeHandler' import { SlidesHandlerOptionType } from './SlidesHandler' import { SlidesInViewOptionsType } from './SlidesInView' +import { FocusHandlerOptionType } from './SlideFocus' export type LooseOptionsType = { [key: string]: unknown @@ -36,6 +37,7 @@ export type OptionsType = CreateOptionsType<{ watchDrag: DragHandlerOptionType watchResize: ResizeHandlerOptionType watchSlides: SlidesHandlerOptionType + watchFocus: FocusHandlerOptionType }> export const defaultOptions: OptionsType = { @@ -57,7 +59,8 @@ export const defaultOptions: OptionsType = { active: true, watchDrag: true, watchResize: true, - watchSlides: true + watchSlides: true, + watchFocus: true } export type EmblaOptionsType = Partial diff --git a/packages/embla-carousel/src/components/SlideFocus.ts b/packages/embla-carousel/src/components/SlideFocus.ts index a101e6413..521b97a3c 100644 --- a/packages/embla-carousel/src/components/SlideFocus.ts +++ b/packages/embla-carousel/src/components/SlideFocus.ts @@ -1,12 +1,20 @@ +import { EmblaCarouselType } from './EmblaCarousel' import { EventHandlerType } from './EventHandler' import { EventStoreType } from './EventStore' import { ScrollBodyType } from './ScrollBody' import { ScrollToType } from './ScrollTo' import { SlideRegistryType } from './SlideRegistry' -import { isNumber } from './utils' +import { isBoolean, isNumber } from './utils' + +type FocusHandlerCallbackType = ( + emblaApi: EmblaCarouselType, + evt: FocusEvent +) => boolean | void + +export type FocusHandlerOptionType = boolean | FocusHandlerCallbackType export type SlideFocusType = { - init: () => void + init: (emblaApi: EmblaCarouselType) => void } export function SlideFocus( @@ -16,43 +24,54 @@ export function SlideFocus( scrollTo: ScrollToType, scrollBody: ScrollBodyType, eventStore: EventStoreType, - eventHandler: EventHandlerType + eventHandler: EventHandlerType, + watchFocus: FocusHandlerOptionType ): SlideFocusType { + const focusListenerOptions = { passive: true, capture: true } let lastTabPressTime = 0 - function init(): void { - eventStore.add(document, 'keydown', registerTabPress, false) - slides.forEach(addSlideFocusEvent) - } - - function registerTabPress(event: KeyboardEvent): void { - if (event.code === 'Tab') lastTabPressTime = new Date().getTime() - } + function init(emblaApi: EmblaCarouselType): void { + if (!watchFocus) return - function addSlideFocusEvent(slide: HTMLElement): void { - const focus = (): void => { + function defaultCallback(index: number): void { const nowTime = new Date().getTime() const diffTime = nowTime - lastTabPressTime if (diffTime > 10) return + eventHandler.emit('slideFocusStart') root.scrollLeft = 0 - const index = slides.indexOf(slide) + const group = slideRegistry.findIndex((group) => group.includes(index)) if (!isNumber(group)) return scrollBody.useDuration(0) scrollTo.index(group, 0) + eventHandler.emit('slideFocus') } - eventStore.add(slide, 'focus', focus, { - passive: true, - capture: true + eventStore.add(document, 'keydown', registerTabPress, false) + + slides.forEach((slide, slideIndex) => { + eventStore.add( + slide, + 'focus', + (evt: FocusEvent) => { + if (isBoolean(watchFocus) || watchFocus(emblaApi, evt)) { + defaultCallback(slideIndex) + } + }, + focusListenerOptions + ) }) } + function registerTabPress(event: KeyboardEvent): void { + if (event.code === 'Tab') lastTabPressTime = new Date().getTime() + } + const self: SlideFocusType = { init }