@zeix/ui-element
Version:
UIElement - a HTML-first library for reactive Web Components
130 lines (121 loc) • 3.02 kB
text/typescript
import {
asInteger,
type Component,
component,
fromSelector,
on,
setProperty,
} from '../../..'
export type ModuleCarouselProps = {
readonly slides: HTMLElement[]
readonly index: number
}
const wrapAround = (index: number, total: number) => (index + total) % total
export default component(
'module-carousel',
{
slides: fromSelector('[role="tabpanel"]'),
index: asInteger((host: HTMLElement & { slides: HTMLElement[] }) =>
Math.max(
host.slides.findIndex(slide => slide.ariaCurrent === 'true'),
0,
),
),
},
(el, { all }) => {
const isCurrentDot = (target: HTMLElement) =>
target.dataset.index === String(el.index)
const scrollToCurrentSlide = () => {
el.slides[el.index].scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
}
return [
// Register IntersectionObserver to update index based on scroll position
() => {
const observer = new IntersectionObserver(
entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
el.index = el.slides.findIndex(
slide => slide === entry.target,
)
break
}
}
},
{
root: el.querySelector('.slides'),
rootMargin: '0px',
threshold: 0.99, // Ignore rounding errors
},
)
el.slides.forEach(slide => {
observer.observe(slide)
})
return () => {
observer.disconnect()
}
},
// Handle navigation button click and keyup events
all('nav button', [
on('click', ({ host, target }) => {
const total = host.slides.length
const nextIndex = target.classList.contains('prev')
? el.index - 1
: target.classList.contains('next')
? el.index + 1
: parseInt(target.dataset.index || '0')
el.index = Number.isInteger(nextIndex)
? wrapAround(nextIndex, total)
: 0
scrollToCurrentSlide()
}),
on('keyup', ({ event, host }) => {
const key = event.key
if (
['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(key)
) {
event.preventDefault()
event.stopPropagation()
const total = host.slides.length
const nextIndex =
key === 'Home'
? 0
: key === 'End'
? total - 1
: wrapAround(
el.index +
(key === 'ArrowLeft' ? -1 : 1),
total,
)
host.slides[nextIndex].focus()
el.index = nextIndex
scrollToCurrentSlide()
}
}),
]),
// Set the active slide in the navigation
all('[role="tab"]', [
setProperty('ariaSelected', target =>
String(isCurrentDot(target)),
),
setProperty('tabIndex', target =>
isCurrentDot(target) ? 0 : -1,
),
]),
// Set the active slide in the slides
all('[role="tabpanel"]', [
setProperty('ariaCurrent', target =>
String(target.id === el.slides[el.index].id),
),
]),
]
},
)
declare global {
interface HTMLElementTagNameMap {
'module-carousel': Component<ModuleCarouselProps>
}
}