Skip to content

Commit

Permalink
add webui-carousel
Browse files Browse the repository at this point in the history
  • Loading branch information
basher committed Apr 5, 2024
1 parent 03e2322 commit b75b456
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 41 deletions.
76 changes: 37 additions & 39 deletions ui/src/javascript/web-components/webui-carousel.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
type SliderConfig = {
type CarouselConfig = {
showSlideCount: boolean;
showSlideCountPips: boolean;
showPrevNextButtons: boolean;
};

class Slider {
private slider: Element;
export default class WebUICarousel extends HTMLElement {
private carousel: HTMLUListElement | null;
private slides: NodeListOf<HTMLElement>;
private visibleSlideClass: string;
private currentSlideClass: string;
private currentPipClass: string;

private config: SliderConfig = {
private config: CarouselConfig = {
showSlideCount: false,
showSlideCountPips: false,
showPrevNextButtons: false,
};

constructor(slider: Element) {
this.slider = slider;
this.slides = this.slider.querySelectorAll('.slider__slide');
constructor() {
super();

this.carousel = this.querySelector('.carousel');
this.slides = this.querySelectorAll('.carousel__slide');
this.visibleSlideClass = 'is-visible';
this.currentSlideClass = 'is-current';
this.currentPipClass = 'is-current';

if (!this.carousel || this.slides.length === 0) return;

this.initConfiguration();
this.init();
}

public static start(): void {
const sliders = document.querySelectorAll('[data-module="slider"]');

sliders.forEach((slider) => {
const instance = new Slider(slider);
return instance;
});
}

private initConfiguration(): void {
// HTML 'data-' attribute flags.
const flags = {
Expand All @@ -55,7 +50,7 @@ class Slider {
}

private init(): void {
this.setAccessibility();
this.setupA11y();
this.setVisibleSlide();

// Show slide counter (text).
Expand All @@ -76,11 +71,11 @@ class Slider {
// Manage :FOCUS event on slides.
this.handleFocus();

// Manage keyboard (ARROW keys) events on slider.
// Manage keyboard (ARROW keys) events on carousel.
this.handleKeyboard();
}

private setAccessibility(): void {
private setupA11y(): void {
// Add slide counter labels for screen readers.
this.slides.forEach((slide: HTMLElement, i: number) => {
slide.setAttribute(
Expand All @@ -92,7 +87,7 @@ class Slider {

private setVisibleSlide(): void {
const observerSettings = {
root: this.slider,
root: this.carousel,
// Fire callback when when observed item is 100% in view.
threshold: [1.0],
};
Expand Down Expand Up @@ -132,20 +127,20 @@ class Slider {

private showSlideCount(): void {
const counter = document.createElement('p');
counter.classList.add('slider__counter');
counter.classList.add('carousel__counter');
counter.setAttribute('data-counter', '');
counter.innerHTML = `slide 1 of ${this.slides.length}`;
this.slider.after(counter);
this.carousel?.after(counter);
}

private showSlideCountPips(): void {
const counterPips = document.createElement('p');
counterPips.classList.add('slider__counter--pips');
counterPips.classList.add('carousel__counter--pips');
counterPips.setAttribute('data-counter-pips', '');

this.slides.forEach((_slide, i) => {
counterPips.innerHTML += `
<span class="slider__counter__pip" data-pip>
<span class="carousel__counter__pip" data-pip>
${i + 1}
</span>
`;
Expand All @@ -154,18 +149,18 @@ class Slider {
const firstPip = counterPips.querySelector('[data-pip]');
firstPip && firstPip.classList.add(this.currentPipClass);

this.slider.after(counterPips);
this.carousel?.after(counterPips);
}

private showPrevNextButtons(): void {
// Prevent keyboard :FOCUS on slider when displaying PREV/NEXT buttons.
this.slider.setAttribute('tabIndex', '-1');
// Prevent keyboard :FOCUS on carousel when displaying PREV/NEXT buttons.
this.carousel?.setAttribute('tabIndex', '-1');

const buttonGroup = document.createElement('div');
buttonGroup.classList.add('slider__controls', 'button-group');
buttonGroup.classList.add('carousel__controls', 'button-group');
buttonGroup.setAttribute('role', 'region');
buttonGroup.setAttribute('aria-label', 'slider controls');
this.slider.before(buttonGroup);
buttonGroup.setAttribute('aria-label', 'carousel controls');
this.carousel?.before(buttonGroup);

buttonGroup.innerHTML = `
<button class="button button--text" data-button="prev">
Expand Down Expand Up @@ -251,12 +246,12 @@ class Slider {

private setCurrentSlideCounter(i: number): void {
const counter =
this.slider.parentElement?.querySelector('[data-counter]');
this.carousel?.parentElement?.querySelector('[data-counter]');
if (counter) {
counter.innerHTML = `slide ${i + 1} of ${this.slides.length}`;
}

const counterPips = this.slider.parentElement?.querySelector(
const counterPips = this.carousel?.parentElement?.querySelector(
'[data-counter-pips]',
);
if (counterPips) {
Expand All @@ -269,14 +264,15 @@ class Slider {
}

private scrollToSlide(slide: HTMLElement, i: number): void {
const scrollWidth = this.carousel?.scrollWidth || 0;
const slidePosition = Math.floor(
this.slider.scrollWidth * (i / this.slides.length),
scrollWidth * (i / this.slides.length),
);
if (
!slide.classList.contains(this.visibleSlideClass) ||
slide.classList.contains(this.currentSlideClass)
) {
this.slider.scrollTo({
this.carousel?.scrollTo({
left: slidePosition,
behavior: 'smooth',
});
Expand All @@ -295,9 +291,7 @@ class Slider {
}

private handleKeyboard(): void {
this.slider.addEventListener('keydown', (e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.carousel?.addEventListener('keydown', (e) => {
switch (e.code) {
case 'ArrowRight':
e.preventDefault();
Expand All @@ -314,7 +308,11 @@ class Slider {
}

private getBoolAttribute(name: string): boolean {
return this.slider.getAttribute(name) === 'true';
return this.getAttribute(name) === 'true';
}

// Handle constructor() event listeners.
handleEvent(e: MouseEvent) {
//
}
}
export default Slider;
4 changes: 2 additions & 2 deletions ui/src/stylesheets/web-components/_webui-carousel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ webui-carousel {
box-shadow: inset 0 0 0 $border-width-l $color-brand; // For demo purposes.
}

&__controls {
.carousel__controls {
margin-block-end: $gutter-m;
}

&__counter {
.carousel__counter {
&--pips {
display: flex;
flex-wrap: wrap;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const style = `
<style>
.carousel > * {
background: hsl(51, 100%, 45%, 15%);
border: 1px dashed hsl(51, 100%, 45%);
padding: 5rem;
}
</style>
`;

export const WebUICarouselHtml = (args) => `
${style}
<webui-carousel
${args.showSlideCount === true ? 'data-slide-count="true"' : ''}
${args.showSlideCountPips === true ? 'data-slide-count-pips="true"' : ''}
${args.showPrevNextButtons === true ? 'data-prev-next-buttons="true"' : ''}
>
<section class="carousel-wrapper" aria-label="[meaningful label for carousel]"
>
<ul
class="carousel ${args.makeFullwidth === true ? 'carousel--fullwidth' : ''}"
tabindex="0"
>
<li class="carousel__slide">
Slide 1<br>More content<br>Slides have equal height
<br><br>
<a
href="#"
class="button button--text button--primary"
>
Button
</a>
</li>
<li class="carousel__slide">
Slide 2
<br><br>
<a
href="#"
class="button button--text button--primary"
>
Button
</a>
</li>
<li class="carousel__slide">
Slide 3
<br><br>
<a
href="#"
class="button button--text button--primary"
>
Button
</a>
</li>
<li class="carousel__slide">
Slide 4
<br><br>
<a
href="#"
class="button button--text button--primary"
>
Button
</a>
</li>
<li class="carousel__slide">
Slide 5
<br><br>
<a
href="#"
class="button button--text button--primary"
>
Button
</a>
</li>
<li class="carousel__slide">
Slide 6
<br><br>
<a
href="#"
class="button button--text button--primary"
>
Button
</a>
</li>
</ul>
<p class="carousel-instructions">Scroll or use your arrow keys for more</p>
</section>
</webui-carousel>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Meta, Canvas, Controls } from '@storybook/blocks';
import * as WebUICarousel from './WebUICarousel.stories';

<Meta of={WebUICarousel} />

# `<webui-carousel>`
- This is an enhancement of the [default CSS carousel](/story/components-carousel--carousel) - showing a slide count, PREV and NEXT buttons, etc.
- Native scrolling with `LEFT|RIGHT` arrow keys is overridden, allowing JavaScript to correctly update the slide count when using arrow keys.
- Each slide has an `aria-label`, indicating the current slide number and the total number of slides.
- When PREV and NEXT buttons are shown, keyboard `focus` is modified such that **only visible slides are focusable**.

## Required and optional HTML or `data-` attributes

Attribute | Behaviour
--- | ---
`data-slide-count="true"` | Optional. Displays slide count & current slide.
`data-slide-count-pips="true"` | Optional. Displays slide count & current slide as pips instead of text.
`data-prev-next-buttons="true"` | Optional. Displays PREV and NEXT slide buttons.

## TODO
- Update counter with touch/swipe events?

<Canvas of={WebUICarousel.WebUICarousel} />
<Controls of={WebUICarousel.WebUICarousel} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { WebUICarouselHtml } from './WebUICarousel';

export default {
title: 'Web Components Or Custom Elements/<webui-carousel>',
parameters: {
status: {
type: 'stable',
},
},
argTypes: {
makeFullwidth: { control: 'boolean' },
showSlideCount: { control: 'boolean' },
showSlideCountPips: { control: 'boolean' },
showPrevNextButtons: { control: 'boolean' },
},
};

export const WebUICarousel = {
render: (args) => WebUICarouselHtml(args),
};
WebUICarousel.storyName = '<webui-carousel>';

0 comments on commit b75b456

Please sign in to comment.