-
Notifications
You must be signed in to change notification settings - Fork 296
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
efcd4bf
commit 7407d27
Showing
7 changed files
with
454 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Carousel ||10 | ||
|
||
-> go to Overview |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Carousel >> Overview ||10 | ||
|
||
A carousel web component. The component allows users to navigate through a series of images using either UI controls or keyboard navigation. | ||
|
||
```js script | ||
import { html } from '@mdjs/mdjs-preview'; | ||
import '@lion/ui/define/lion-carousel.js'; | ||
``` | ||
|
||
```js preview-story | ||
export const main = () => | ||
html` | ||
<lion-carousel> | ||
<img slot="slide" src="https://picsum.photos/800/400?random=1" alt="random image for demo" /> | ||
<img slot="slide" src="https://picsum.photos/800/400?random=2" alt="random image for demo" /> | ||
<img slot="slide" src="https://picsum.photos/800/400?random=3" alt="random image for demo" /> | ||
<img slot="slide" src="https://picsum.photos/800/400?random=4" alt="random image for demo" /> | ||
<!-- Insert more elements as needed --> | ||
</lion-carousel> | ||
`; | ||
``` | ||
|
||
## Features | ||
|
||
- Flexible Content and Styling: LionCarousel accepts diverse content through `slot=slide` and offers styling options. | ||
- Autoplay and Manual Navigation: It supports both automatic cycling of slides for showcases and manual navigation for user control. | ||
- Accessibility-Focused: Built with accessibility in mind, featuring keyboard navigation, ARIA attributes, and motion preferences. | ||
|
||
## Installation | ||
|
||
```bash | ||
npm i --save @lion/ui | ||
``` | ||
|
||
```js | ||
import { LionCarousel } from '@lion/ui/carousel.js'; | ||
// or | ||
import '@lion/ui/define/lion-carousel.js'; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# Carousel >> Use Cases ||20 | ||
|
||
```js script | ||
import { html } from '@mdjs/mdjs-preview'; | ||
import '@lion/ui/define/lion-carousel.js'; | ||
``` | ||
|
||
## Pre-select the first active slide | ||
|
||
You can preselect the active slide using the `current` attribute. | ||
|
||
```html preview-story | ||
<lion-carousel current="2"> | ||
<div slot="slide">slide 1</div> | ||
<div slot="slide">slide 2</div> | ||
<div slot="slide">slide 3</div> | ||
<div slot="slide">Any HTML content</div> | ||
<p slot="slide">More content here</p> | ||
</lion-carousel> | ||
``` | ||
|
||
## Autoplay Carousel | ||
|
||
You can define an autoplay carousel using the `slideshow` atribute and set the delay duration using the `duration` attribute, also adds the "play" and "stop" buttons for users to control it. | ||
|
||
```html preview-story | ||
<lion-carousel slideshow duration="4000"> | ||
<div slot="slide">slide 1</div> | ||
<div slot="slide">slide 2</div> | ||
<div slot="slide">slide 3</div> | ||
<div slot="slide">Any HTML content</div> | ||
<p slot="slide">More content here</p> | ||
</lion-carousel> | ||
``` | ||
|
||
## Carousel with Pagination | ||
|
||
You can compose the carousel component to work with LionPagination component | ||
|
||
```html preview-story | ||
<lion-carousel pagination> | ||
<div slot="slide">slide 1</div> | ||
<div slot="slide">slide 2</div> | ||
<div slot="slide">slide 3</div> | ||
<div slot="slide">Any HTML content</div> | ||
<p slot="slide">More content here</p> | ||
</lion-carousel> | ||
``` | ||
|
||
## Carousel with all options | ||
|
||
```html preview-story | ||
<lion-carousel pagination slideshow> | ||
<img slot="slide" src="https://picsum.photos/800/400?random=1" alt="random image for demo" /> | ||
<img slot="slide" src="https://picsum.photos/800/400?random=2" alt="random image for demo" /> | ||
<img slot="slide" src="https://picsum.photos/800/400?random=3" alt="random image for demo" /> | ||
<img slot="slide" src="https://picsum.photos/800/400?random=4" alt="random image for demo" /> | ||
</lion-carousel> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; | ||
import { LitElement, html, css } from 'lit'; | ||
import { LionPagination } from '@lion/ui/pagination.js'; | ||
|
||
export class LionCarousel extends ScopedElementsMixin(LitElement) { | ||
static get scopedElements() { | ||
return { | ||
'lion-pagination': LionPagination, | ||
}; | ||
} | ||
|
||
static get styles() { | ||
return [ | ||
css` | ||
:host { | ||
display: block; | ||
} | ||
::slotted([slot='slide']) { | ||
display: none; | ||
} | ||
::slotted([slot='slide'].active) { | ||
display: block; | ||
} | ||
:host [hidden] { | ||
clip: rect(0 0 0 0); | ||
clip-path: inset(50%); | ||
height: 1px; | ||
overflow: hidden; | ||
position: absolute; | ||
white-space: nowrap; | ||
width: 1px; | ||
} | ||
`, | ||
]; | ||
} | ||
|
||
static get properties() { | ||
return { | ||
currentIndex: { type: Number, attribute: 'current', reflect: true }, | ||
pagination: { type: Boolean, attribute: 'pagination', reflect: true }, | ||
slideshow: { type: Boolean, reflect: true }, | ||
duration: { type: Number, reflect: true }, | ||
}; | ||
} | ||
|
||
render() { | ||
return html` | ||
<div @keydown="${this._handleKeyDown}"> | ||
<slot name="slide"></slot> | ||
</div> | ||
<button class="prev" @click="${this.prevSlide}" aria-label="previous">◀</button> | ||
<button class="next" @click="${this.nextSlide}" aria-label="next">▶</button> | ||
${this._paginationTemplate} ${this._slideshowControlsTemplate} | ||
<div aria-live="polite" hidden> | ||
Viewing slide ${this.currentIndex} of ${this.slides.length} | ||
</div> | ||
`; | ||
} | ||
|
||
constructor() { | ||
super(); | ||
this.currentIndex = 1; | ||
this.pagination = false; | ||
this.slideshow = false; | ||
this.slideshowAnimating = false; | ||
/** | ||
* @type {number | undefined} | ||
*/ | ||
this.duration = undefined; | ||
} | ||
|
||
firstUpdated() { | ||
this._updateActiveSlide(); | ||
if (this.slideshow) this._startSlideShow(); | ||
} | ||
|
||
connectedCallback() { | ||
super.connectedCallback(); | ||
this.slides.forEach((slide, index) => { | ||
// eslint-disable-next-line no-param-reassign | ||
slide.ariaLabel = `${index + 1}/${this.slides.length}`; | ||
}); | ||
} | ||
|
||
/** | ||
* @protected | ||
*/ | ||
get _paginationTemplate() { | ||
return this.pagination | ||
? html` | ||
<lion-pagination | ||
count="${this._getSlidesCount()}" | ||
current="${this.currentIndex}" | ||
@current-changed=${this._handlePaginationChange} | ||
></lion-pagination> | ||
` | ||
: ''; | ||
} | ||
|
||
/** | ||
* @protected | ||
*/ | ||
get _slideshowControlsTemplate() { | ||
return this.slideshow | ||
? html` | ||
<button @click="${this._startSlideShow}" aria-label="Start slide show">▶</button> | ||
<button @click="${this._stopSlideShow}" aria-label="Stop slide show">◼</button> | ||
` | ||
: ''; | ||
} | ||
|
||
/** | ||
* @param {import('lit').PropertyValues } changedProperties | ||
*/ | ||
updated(changedProperties) { | ||
super.updated(changedProperties); | ||
if (changedProperties.has('currentIndex')) { | ||
this._updateActiveSlide(); | ||
} | ||
} | ||
|
||
/** | ||
* Animates slide transition. | ||
* This method can be overridden by subclasses to implement custom animation logic. | ||
* @param {Number} _oldIndex index of slide transitioning from. | ||
* @param {Number} _newIndex index of slide transitioning to. | ||
* @param {'next'|'prev'} _direction The direction of the transition ('next' or 'prev'). | ||
*/ | ||
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars | ||
async _slideAnimation(_oldIndex, _newIndex, _direction) { | ||
// Default implementation does nothing. | ||
} | ||
|
||
async nextSlide() { | ||
const oldIndex = this.currentIndex; | ||
const newIndex = (this.currentIndex % this.slides.length) + 1; | ||
await this._slideAnimation(oldIndex, newIndex, 'next'); | ||
this.currentIndex = newIndex; | ||
} | ||
|
||
async prevSlide() { | ||
const oldIndex = this.currentIndex; | ||
const newIndex = ((this.currentIndex - 2 + this.slides.length) % this.slides.length) + 1; | ||
await this._slideAnimation(oldIndex, newIndex, 'prev'); | ||
this.currentIndex = newIndex; | ||
} | ||
|
||
/** | ||
* | ||
* @param {{ key: string; }} e | ||
*/ | ||
_handleKeyDown(e) { | ||
if (e.key === 'ArrowRight') { | ||
this.nextSlide(); | ||
} else if (e.key === 'ArrowLeft') { | ||
this.prevSlide(); | ||
} | ||
} | ||
|
||
/** | ||
* | ||
* @param {{ target: { current: any; }; }} e | ||
*/ | ||
async _handlePaginationChange(e) { | ||
const newIndex = e.target.current; | ||
if (newIndex === this.currentIndex) return; | ||
const direction = newIndex > this.currentIndex ? 'next' : 'prev'; | ||
await this._slideAnimation(this.currentIndex, newIndex, direction); | ||
this.currentIndex = newIndex; | ||
} | ||
|
||
get slides() { | ||
return /** @type {HTMLElement[]} */ (Array.from(this.children)).filter( | ||
child => child.slot === 'slide', | ||
); | ||
} | ||
|
||
/** | ||
* @protected | ||
*/ | ||
_updateActiveSlide() { | ||
const { slides } = this; | ||
slides.forEach((slide, index) => { | ||
const isActive = index + 1 === this.currentIndex; | ||
if (isActive) { | ||
slide.classList.add('active'); | ||
slide.removeAttribute('tabindex'); | ||
} else { | ||
slide.classList.remove('active'); | ||
// eslint-disable-next-line no-param-reassign | ||
slide.tabIndex = -1; | ||
} | ||
slide.setAttribute('aria-hidden', `${!isActive}`); | ||
if (isActive && !this.slideshowAnimating) { | ||
// eslint-disable-next-line no-param-reassign | ||
slide.tabIndex = 0; | ||
slide.focus(); | ||
} | ||
}); | ||
} | ||
|
||
_getSlidesCount() { | ||
return this.slides.length; | ||
} | ||
|
||
_updateAriaLiveSettingForAutoplay() { | ||
const liveRegion = this.shadowRoot?.querySelector('.aria-live'); | ||
if (this.slideshowAnimating) { | ||
liveRegion?.setAttribute('aria-live', 'off'); | ||
} else { | ||
liveRegion?.setAttribute('aria-live', 'polite'); | ||
} | ||
} | ||
|
||
/** | ||
* @protected | ||
*/ | ||
_startSlideShow() { | ||
if (this.slideshowAnimating) return; | ||
this.slideshowAnimating = true; | ||
const duration = this.duration || 5000; | ||
this.slideShowTimer = setInterval(() => { | ||
this.nextSlide(); | ||
}, duration); | ||
} | ||
|
||
/** | ||
* @protected | ||
*/ | ||
_stopSlideShow() { | ||
if (!this.slideshowAnimating) return; | ||
this.slideshowAnimating = false; | ||
clearInterval(this.slideShowTimer); | ||
} | ||
|
||
disconnectedCallback() { | ||
super.disconnectedCallback(); | ||
clearInterval(this.slideShowTimer); | ||
} | ||
} |
Oops, something went wrong.