Merge pull request #37 from vadimkorr/feature/#31_Show-pause-indicator
Feature/#31 Show pause indicator
This commit is contained in:
3
.babelrc
3
.babelrc
@@ -7,6 +7,9 @@
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [["@babel/preset-env"]]
|
||||
|
||||
@@ -35,7 +35,7 @@ Import component
|
||||
|
||||
## Props
|
||||
| Prop | Type | Default | Description |
|
||||
|----------------------|------------|-----------------|-----------------------------------------------|
|
||||
|---------------------------|------------|-----------------|-----------------------------------------------|
|
||||
| `arrows` | `boolean` | `true` | Enable Next/Prev arrows |
|
||||
| `infinite` | `boolean` | `true` | Infinite looping |
|
||||
| `initialPageIndex` | `number` | `0` | Page to start on |
|
||||
@@ -44,6 +44,7 @@ Import component
|
||||
| `autoplayDuration` | `number` | `3000` | Autoplay change interval (ms) |
|
||||
| `autoplayDirection` | `string` | `'next'` | Autoplay change direction (`next` or `prev`) |
|
||||
| `pauseOnFocus` | `boolean` | `false` | Pause autoplay on focus |
|
||||
| `autoplayProgressVisible` | `boolean` | `false` | Show autoplay duration progress indicator |
|
||||
| `dots` | `boolean` | `true` | Current page indicator dots |
|
||||
| `timingFunction` | `string` | `'ease-in-out'` | CSS animation timing function |
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { createStore } from '../../store'
|
||||
import Dots from '../Dots/Dots.svelte'
|
||||
import Arrow from '../Arrow/Arrow.svelte'
|
||||
import Progress from '../Progress/Progress.svelte'
|
||||
import { NEXT, PREV } from '../../direction'
|
||||
import { swipeable } from '../../actions/swipeable'
|
||||
import { focusable } from '../../actions/focusable'
|
||||
@@ -12,12 +13,30 @@
|
||||
} from '../../utils/event'
|
||||
import { getAdjacentIndexes } from '../../utils/page'
|
||||
import { get } from '../../utils/object'
|
||||
import { ProgressManager } from '../../utils/ProgressManager.js'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const autoplayDirectionFnDescription = {
|
||||
[NEXT]: () => {
|
||||
progressManager.start(() => {
|
||||
showNextPage()
|
||||
})
|
||||
},
|
||||
[PREV]: () => {
|
||||
progressManager.start(() => {
|
||||
showPrevPage()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const directionFnDescription = {
|
||||
[NEXT]: showNextPage,
|
||||
[PREV]: showPrevPage
|
||||
[NEXT]: () => {
|
||||
showNextPage()
|
||||
},
|
||||
[PREV]: () => {
|
||||
showPrevPage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +86,11 @@
|
||||
*/
|
||||
export let pauseOnFocus = false
|
||||
|
||||
/**
|
||||
* Show autoplay duration progress indicator
|
||||
*/
|
||||
export let autoplayProgressVisible = false
|
||||
|
||||
/**
|
||||
* Current page indicator dots
|
||||
*/
|
||||
@@ -103,13 +127,20 @@
|
||||
let pagesElement
|
||||
let focused = false
|
||||
|
||||
let autoplayInterval = null
|
||||
let progressValue
|
||||
const progressManager = new ProgressManager({
|
||||
autoplayDuration,
|
||||
onProgressValueChange: (value) => {
|
||||
progressValue = 1 - value
|
||||
}
|
||||
})
|
||||
|
||||
$: {
|
||||
if (pauseOnFocus) {
|
||||
if (focused) {
|
||||
clearAutoplay()
|
||||
progressManager.pause()
|
||||
} else {
|
||||
applyAutoplay()
|
||||
progressManager.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,19 +162,6 @@
|
||||
offsetPage(false)
|
||||
}
|
||||
|
||||
function applyAutoplay() {
|
||||
if (autoplay && !autoplayInterval) {
|
||||
autoplayInterval = setInterval(() => {
|
||||
directionFnDescription[autoplayDirection]()
|
||||
}, autoplayDuration)
|
||||
}
|
||||
}
|
||||
|
||||
function clearAutoplay() {
|
||||
clearInterval(autoplayInterval)
|
||||
autoplayInterval = null
|
||||
}
|
||||
|
||||
function addClones() {
|
||||
const first = pagesElement.children[0]
|
||||
const last = pagesElement.children[pagesElement.children.length - 1]
|
||||
@@ -151,13 +169,38 @@
|
||||
pagesElement.append(first.cloneNode(true))
|
||||
}
|
||||
|
||||
function applyAutoplayIfNeeded(options) {
|
||||
// prevent progress change if not infinite for first and last page
|
||||
if (
|
||||
!infinite && (
|
||||
(autoplayDirection === NEXT && currentPageIndex === pagesCount - 1) ||
|
||||
(autoplayDirection === PREV && currentPageIndex === 0)
|
||||
)
|
||||
) {
|
||||
progressManager.reset()
|
||||
return
|
||||
}
|
||||
if (autoplay) {
|
||||
const delayMs = get(options, 'delayMs', 0)
|
||||
if (delayMs) {
|
||||
setTimeout(() => {
|
||||
autoplayDirectionFnDescription[autoplayDirection]()
|
||||
}, delayMs)
|
||||
} else {
|
||||
autoplayDirectionFnDescription[autoplayDirection]()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cleanupFns = []
|
||||
|
||||
onMount(() => {
|
||||
(async () => {
|
||||
await tick()
|
||||
cleanupFns.push(store.subscribe(value => {
|
||||
currentPageIndex = value.currentPageIndex
|
||||
}))
|
||||
cleanupFns.push(() => progressManager.reset())
|
||||
if (pagesElement && pageWindowElement) {
|
||||
// load first and last child to clone them
|
||||
loaded = [0, pagesElement.children.length - 1]
|
||||
@@ -167,13 +210,14 @@
|
||||
store.init(initialPageIndex + Number(infinite))
|
||||
applyPageSizes()
|
||||
}
|
||||
applyAutoplay()
|
||||
|
||||
applyAutoplayIfNeeded()
|
||||
|
||||
addResizeEventListener(applyPageSizes)
|
||||
})()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
clearAutoplay()
|
||||
removeResizeEventListener(applyPageSizes)
|
||||
cleanupFns.filter(fn => fn && typeof fn === 'function').forEach(fn => fn())
|
||||
})
|
||||
@@ -218,14 +262,14 @@
|
||||
|
||||
function showPage(pageIndex, options) {
|
||||
const animated = get(options, 'animated', true)
|
||||
const offsetDelayMs = get(options, 'offsetDelayMs', true)
|
||||
const offsetDelayMs = get(options, 'offsetDelayMs', 0)
|
||||
safeChangePage(() => {
|
||||
store.moveToPage({ pageIndex, pagesCount })
|
||||
// delayed page transition, used for infinite autoplay to jump to real page
|
||||
setTimeout(() => {
|
||||
offsetPage(animated)
|
||||
const jumped = jumpIfNeeded()
|
||||
!jumped && applyAutoplay()
|
||||
!jumped && applyAutoplayIfNeeded({ delayMs: _duration }) // while offset animation is in progress (delayMs = _duration ms) wait for it
|
||||
}, offsetDelayMs)
|
||||
}, { animated })
|
||||
}
|
||||
@@ -235,7 +279,7 @@
|
||||
store.prev({ infinite, pagesCount })
|
||||
offsetPage(animated)
|
||||
const jumped = jumpIfNeeded()
|
||||
!jumped && applyAutoplay()
|
||||
!jumped && applyAutoplayIfNeeded({ delayMs: _duration })
|
||||
}, { animated })
|
||||
}
|
||||
function showNextPage(options) {
|
||||
@@ -244,7 +288,7 @@
|
||||
store.next({ infinite, pagesCount })
|
||||
offsetPage(animated)
|
||||
const jumped = jumpIfNeeded()
|
||||
!jumped && applyAutoplay()
|
||||
!jumped && applyAutoplayIfNeeded({ delayMs: _duration })
|
||||
}, { animated })
|
||||
}
|
||||
|
||||
@@ -301,6 +345,11 @@
|
||||
>
|
||||
<slot {loaded}></slot>
|
||||
</div>
|
||||
{#if autoplayProgressVisible}
|
||||
<div class="sc-carousel-progress__container">
|
||||
<Progress value={progressValue} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if arrows}
|
||||
<slot name="next" {showNextPage}>
|
||||
@@ -353,6 +402,7 @@
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.sc-carousel__pages-container {
|
||||
width: 100%;
|
||||
@@ -366,4 +416,11 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.sc-carousel-progress__container {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
background-color: var(--sc-color-rgb-light-50p);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
/**
|
||||
* CSS animation timing function
|
||||
*/
|
||||
export let timingFunction = "ease-in-out";
|
||||
export let timingFunction = 'ease-in-out';
|
||||
|
||||
/**
|
||||
* Enable Next/Previos arrows
|
||||
@@ -46,6 +46,11 @@
|
||||
*/
|
||||
export let pauseOnFocus = false
|
||||
|
||||
/**
|
||||
* Show autoplay duration progress indicator
|
||||
*/
|
||||
export let autoplayProgressVisible = false
|
||||
|
||||
/**
|
||||
* Current page indicator dots
|
||||
*/
|
||||
@@ -82,6 +87,7 @@
|
||||
{autoplayDuration}
|
||||
{autoplayDirection}
|
||||
{pauseOnFocus}
|
||||
{autoplayProgressVisible}
|
||||
{dots}
|
||||
on:pageChange={
|
||||
event => console.log(`Current page index: ${event.detail}`)
|
||||
@@ -107,6 +113,7 @@
|
||||
{autoplayDuration}
|
||||
{autoplayDirection}
|
||||
{pauseOnFocus}
|
||||
{autoplayProgressVisible}
|
||||
{dots}
|
||||
>
|
||||
{#each colors2 as { color, text } (color)}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/**
|
||||
* CSS animation timing function
|
||||
*/
|
||||
export let timingFunction = "ease-in-out";
|
||||
export let timingFunction = 'ease-in-out';
|
||||
|
||||
/**
|
||||
* Enable Next/Previos arrows
|
||||
@@ -46,6 +46,11 @@
|
||||
*/
|
||||
export let pauseOnFocus = false
|
||||
|
||||
/**
|
||||
* Show autoplay duration progress indicator
|
||||
*/
|
||||
export let autoplayProgressVisible = false
|
||||
|
||||
/**
|
||||
* Current page indicator dots
|
||||
*/
|
||||
@@ -76,6 +81,7 @@
|
||||
{autoplayDuration}
|
||||
{autoplayDirection}
|
||||
{pauseOnFocus}
|
||||
{autoplayProgressVisible}
|
||||
{dots}
|
||||
let:showPrevPage
|
||||
let:showNextPage
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/**
|
||||
* CSS animation timing function
|
||||
*/
|
||||
export let timingFunction = "ease-in-out";
|
||||
export let timingFunction = 'ease-in-out';
|
||||
|
||||
/**
|
||||
* Enable Next/Previos arrows
|
||||
@@ -46,13 +46,18 @@
|
||||
*/
|
||||
export let pauseOnFocus = false
|
||||
|
||||
/**
|
||||
* Show autoplay duration progress indicator
|
||||
*/
|
||||
export let autoplayProgressVisible = false
|
||||
|
||||
/**
|
||||
* Current page indicator dots
|
||||
*/
|
||||
export let dots = true
|
||||
|
||||
function onPageChange(event, showPage) {
|
||||
showPage(event.target.value)
|
||||
showPage(Number(event.target.value))
|
||||
}
|
||||
|
||||
const colors = [
|
||||
@@ -80,6 +85,7 @@
|
||||
{autoplayDuration}
|
||||
{autoplayDirection}
|
||||
{pauseOnFocus}
|
||||
{autoplayProgressVisible}
|
||||
{dots}
|
||||
let:currentPageIndex
|
||||
let:pagesCount
|
||||
|
||||
27
src/components/Progress/Progress.svelte
Normal file
27
src/components/Progress/Progress.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import { tweened } from 'svelte/motion';
|
||||
import { cubicInOut } from 'svelte/easing';
|
||||
|
||||
const MAX_PERCENT = 100;
|
||||
|
||||
/**
|
||||
* Progress value, [0, 1]
|
||||
*/
|
||||
export let value = 0
|
||||
|
||||
$: width = Math.min(Math.max(value * MAX_PERCENT, 0), MAX_PERCENT)
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="sc-carousel-progress__indicator"
|
||||
style="
|
||||
width: {width}%;
|
||||
"
|
||||
></div>
|
||||
|
||||
<style>
|
||||
.sc-carousel-progress__indicator {
|
||||
height: 100%;
|
||||
background-color: var(--sc-color-hex-dark-50p);
|
||||
}
|
||||
</style>
|
||||
13
src/components/Progress/stories/Progress.stories.js
Normal file
13
src/components/Progress/stories/Progress.stories.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import ProgressView from './ProgressView.svelte';
|
||||
|
||||
export default {
|
||||
title: 'Default Components/Progress',
|
||||
component: ProgressView
|
||||
};
|
||||
|
||||
const Template = ({ ...args }) => ({
|
||||
Component: ProgressView,
|
||||
props: args
|
||||
});
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
23
src/components/Progress/stories/ProgressView.svelte
Normal file
23
src/components/Progress/stories/ProgressView.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import Progress from '../Progress.svelte'
|
||||
|
||||
/**
|
||||
* Progress value
|
||||
*/
|
||||
export let value = 0
|
||||
</script>
|
||||
|
||||
<div class="progress-container">
|
||||
<Progress
|
||||
{value}
|
||||
>
|
||||
</Progress>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
background-color: rgba(93, 93, 93, 0.5);
|
||||
}
|
||||
</style>
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
## Autoplay
|
||||
<Carousel
|
||||
autoplay={true}
|
||||
autoplay
|
||||
autoplayDuration={2000}
|
||||
>
|
||||
{#each colors as { color, text } (color)}
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
```jsx
|
||||
<Carousel
|
||||
autoplay={true}
|
||||
autoplay
|
||||
autoplayDuration={2000}
|
||||
>
|
||||
{#each colors as { color, text } (color)}
|
||||
@@ -76,6 +76,58 @@
|
||||
|
||||
<Divider />
|
||||
|
||||
## Autoplay with duration progress
|
||||
<Carousel
|
||||
autoplay
|
||||
autoplayDuration={5000}
|
||||
autoplayProgressVisible
|
||||
>
|
||||
{#each colors as { color, text } (color)}
|
||||
<Color {color} {text} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
|
||||
```jsx
|
||||
<Carousel
|
||||
autoplay
|
||||
autoplayDuration={5000}
|
||||
autoplayProgressVisible
|
||||
>
|
||||
{#each colors as { color, text } (color)}
|
||||
<Color {color} {text} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
```
|
||||
|
||||
<Divider />
|
||||
|
||||
## Autoplay with pause on focus
|
||||
<Carousel
|
||||
autoplay
|
||||
autoplayDuration={5000}
|
||||
autoplayProgressVisible
|
||||
pauseOnFocus
|
||||
>
|
||||
{#each colors as { color, text } (color)}
|
||||
<Color {color} {text} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
|
||||
```jsx
|
||||
<Carousel
|
||||
autoplay
|
||||
autoplayDuration={5000}
|
||||
autoplayProgressVisible
|
||||
pauseOnFocus
|
||||
>
|
||||
{#each colors as { color, text } (color)}
|
||||
<Color {color} {text} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
```
|
||||
|
||||
<Divider />
|
||||
|
||||
## Lazy loading of images
|
||||
<Carousel
|
||||
let:loaded
|
||||
@@ -209,7 +261,7 @@ Import component
|
||||
# Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|----------------------|------------|-----------------|-----------------------------------------------|
|
||||
|---------------------------|------------|-----------------|-----------------------------------------------|
|
||||
| `arrows` | `boolean` | `true` | Enable Next/Prev arrows |
|
||||
| `infinite` | `boolean` | `true` | Infinite looping |
|
||||
| `initialPageIndex` | `number` | `0` | Page to start on |
|
||||
@@ -217,8 +269,9 @@ Import component
|
||||
| `autoplay` | `boolean` | `false` | Enables auto play of pages |
|
||||
| `autoplayDuration` | `number` | `3000` | Autoplay change interval (ms) |
|
||||
| `autoplayDirection` | `string` | `'next'` | Autoplay change direction (`next` or `prev`) |
|
||||
| `dots` | `boolean` | `true` | Current page indicator dots |
|
||||
| `pauseOnFocus` | `boolean` | `false` | Pause autoplay on focus |
|
||||
| `autoplayProgressVisible` | `boolean` | `false` | Show autoplay duration progress indicator |
|
||||
| `dots` | `boolean` | `true` | Current page indicator dots |
|
||||
| `timingFunction` | `string` | `'ease-in-out'` | CSS animation timing function |
|
||||
|
||||
<Divider />
|
||||
|
||||
52
src/utils/ProgressManager.js
Normal file
52
src/utils/ProgressManager.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { setIntervalImmediate } from './interval'
|
||||
|
||||
const STEP_MS = 35
|
||||
export class ProgressManager {
|
||||
#autoplayDuration
|
||||
#onProgressValueChange
|
||||
|
||||
#interval
|
||||
#paused = false
|
||||
|
||||
constructor({
|
||||
autoplayDuration,
|
||||
onProgressValueChange,
|
||||
}) {
|
||||
this.#autoplayDuration = autoplayDuration
|
||||
this.#onProgressValueChange = onProgressValueChange
|
||||
}
|
||||
|
||||
start(onFinish) {
|
||||
this.reset()
|
||||
|
||||
const stepMs = Math.min(STEP_MS, this.#autoplayDuration)
|
||||
let progress = -stepMs
|
||||
|
||||
this.#interval = setIntervalImmediate(() => {
|
||||
if (this.#paused) {
|
||||
return
|
||||
}
|
||||
progress += stepMs
|
||||
|
||||
const value = progress / this.#autoplayDuration
|
||||
this.#onProgressValueChange(value)
|
||||
|
||||
if (value > 1) {
|
||||
this.reset()
|
||||
onFinish()
|
||||
}
|
||||
}, stepMs)
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.#paused = true
|
||||
}
|
||||
|
||||
resume() {
|
||||
this.#paused = false
|
||||
}
|
||||
|
||||
reset() {
|
||||
clearInterval(this.#interval)
|
||||
}
|
||||
}
|
||||
4
src/utils/interval.js
Normal file
4
src/utils/interval.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const setIntervalImmediate = (fn, ms) => {
|
||||
fn();
|
||||
return setInterval(fn, ms);
|
||||
}
|
||||
30
src/utils/interval.test.js
Normal file
30
src/utils/interval.test.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
setIntervalImmediate,
|
||||
} from './interval.js'
|
||||
|
||||
describe('setIntervalImmediate', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
})
|
||||
|
||||
it('runs callback immediately and them each n ms', () => {
|
||||
let interval
|
||||
const durationMs = 1000
|
||||
|
||||
const callNumbersToStopTimer = 3
|
||||
let calledTimes = 0
|
||||
const callback = () => {
|
||||
calledTimes++
|
||||
if (calledTimes === callNumbersToStopTimer) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}
|
||||
|
||||
interval = setIntervalImmediate(callback, durationMs)
|
||||
jest.runAllTimers()
|
||||
|
||||
expect(calledTimes).toBe(callNumbersToStopTimer)
|
||||
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), durationMs)
|
||||
expect(clearInterval).toHaveBeenCalledWith(interval)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user