Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]: watchFocus option #937

Merged
merged 1 commit into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions packages/embla-carousel-autoplay/src/components/Autoplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
22 changes: 19 additions & 3 deletions packages/embla-carousel-docs/src/content/pages/api/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Admonition>

---

### watchFocus

Type: <BrandPrimaryText>`boolean | (emblaApi: EmblaCarouselType, event: FocusEvent) => boolean | void`</BrandPrimaryText>
Default: <BrandSecondaryText>`true`</BrandSecondaryText>

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.

<Admonition type="note">
**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.
</Admonition>

---
Expand All @@ -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.
</Admonition>

---
Expand All @@ -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.
</Admonition>

---
2 changes: 1 addition & 1 deletion packages/embla-carousel/src/components/EmblaCarousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions packages/embla-carousel/src/components/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export function Engine(
containScroll,
watchResize,
watchSlides,
watchDrag
watchDrag,
watchFocus
} = options

// Measurements
Expand Down Expand Up @@ -263,7 +264,8 @@ export function Engine(
scrollTo,
scrollBody,
eventStore,
eventHandler
eventHandler,
watchFocus
)

// Engine
Expand Down
1 change: 1 addition & 0 deletions packages/embla-carousel/src/components/EventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface EmblaEventListType {
destroy: 'destroy'
reInit: 'reInit'
resize: 'resize'
slideFocusStart: 'slideFocusStart'
slideFocus: 'slideFocus'
}

Expand Down
5 changes: 4 additions & 1 deletion packages/embla-carousel/src/components/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,6 +37,7 @@ export type OptionsType = CreateOptionsType<{
watchDrag: DragHandlerOptionType
watchResize: ResizeHandlerOptionType
watchSlides: SlidesHandlerOptionType
watchFocus: FocusHandlerOptionType
}>

export const defaultOptions: OptionsType = {
Expand All @@ -57,7 +59,8 @@ export const defaultOptions: OptionsType = {
active: true,
watchDrag: true,
watchResize: true,
watchSlides: true
watchSlides: true,
watchFocus: true
}

export type EmblaOptionsType = Partial<OptionsType>
53 changes: 36 additions & 17 deletions packages/embla-carousel/src/components/SlideFocus.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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
}
Expand Down