Merge pull request #37 from vadimkorr/feature/#31_Show-pause-indicator

Feature/#31 Show pause indicator
This commit is contained in:
Vadim
2021-07-17 18:33:52 +03:00
committed by GitHub
13 changed files with 338 additions and 56 deletions

View File

@@ -7,6 +7,9 @@
} }
}] }]
], ],
"plugins": [
"@babel/plugin-proposal-class-properties"
],
"env": { "env": {
"test": { "test": {
"presets": [["@babel/preset-env"]] "presets": [["@babel/preset-env"]]

View File

@@ -35,15 +35,16 @@ Import component
## Props ## Props
| Prop | Type | Default | Description | | Prop | Type | Default | Description |
|----------------------|------------|-----------------|-----------------------------------------------| |---------------------------|------------|-----------------|-----------------------------------------------|
| `arrows` | `boolean` | `true` | Enable Next/Prev arrows | | `arrows` | `boolean` | `true` | Enable Next/Prev arrows |
| `infinite` | `boolean` | `true` | Infinite looping | | `infinite` | `boolean` | `true` | Infinite looping |
| `initialPageIndex` | `number` | `0` | Page to start on | | `initialPageIndex` | `number` | `0` | Page to start on |
| `duration` | `number` | `500` | Transition duration (ms) | | `duration` | `number` | `500` | Transition duration (ms) |
| `autoplay` | `boolean` | `false` | Enables autoplay of pages | | `autoplay` | `boolean` | `false` | Enables auto play of pages |
| `autoplayDuration` | `number` | `3000` | Autoplay change interval (ms) | | `autoplayDuration` | `number` | `3000` | Autoplay change interval (ms) |
| `autoplayDirection` | `string` | `'next'` | Autoplay change direction (`next` or `prev`) | | `autoplayDirection` | `string` | `'next'` | Autoplay change direction (`next` or `prev`) |
| `pauseOnFocus` | `boolean` | `false` | Pause autoplay on focus | | `pauseOnFocus` | `boolean` | `false` | Pause autoplay on focus |
| `autoplayProgressVisible` | `boolean` | `false` | Show autoplay duration progress indicator |
| `dots` | `boolean` | `true` | Current page indicator dots | | `dots` | `boolean` | `true` | Current page indicator dots |
| `timingFunction` | `string` | `'ease-in-out'` | CSS animation timing function | | `timingFunction` | `string` | `'ease-in-out'` | CSS animation timing function |

View File

@@ -3,6 +3,7 @@
import { createStore } from '../../store' import { createStore } from '../../store'
import Dots from '../Dots/Dots.svelte' import Dots from '../Dots/Dots.svelte'
import Arrow from '../Arrow/Arrow.svelte' import Arrow from '../Arrow/Arrow.svelte'
import Progress from '../Progress/Progress.svelte'
import { NEXT, PREV } from '../../direction' import { NEXT, PREV } from '../../direction'
import { swipeable } from '../../actions/swipeable' import { swipeable } from '../../actions/swipeable'
import { focusable } from '../../actions/focusable' import { focusable } from '../../actions/focusable'
@@ -12,12 +13,30 @@
} from '../../utils/event' } from '../../utils/event'
import { getAdjacentIndexes } from '../../utils/page' import { getAdjacentIndexes } from '../../utils/page'
import { get } from '../../utils/object' import { get } from '../../utils/object'
import { ProgressManager } from '../../utils/ProgressManager.js'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const autoplayDirectionFnDescription = {
[NEXT]: () => {
progressManager.start(() => {
showNextPage()
})
},
[PREV]: () => {
progressManager.start(() => {
showPrevPage()
})
}
}
const directionFnDescription = { const directionFnDescription = {
[NEXT]: showNextPage, [NEXT]: () => {
[PREV]: showPrevPage showNextPage()
},
[PREV]: () => {
showPrevPage()
}
} }
/** /**
@@ -67,6 +86,11 @@
*/ */
export let pauseOnFocus = false export let pauseOnFocus = false
/**
* Show autoplay duration progress indicator
*/
export let autoplayProgressVisible = false
/** /**
* Current page indicator dots * Current page indicator dots
*/ */
@@ -103,13 +127,20 @@
let pagesElement let pagesElement
let focused = false let focused = false
let autoplayInterval = null let progressValue
const progressManager = new ProgressManager({
autoplayDuration,
onProgressValueChange: (value) => {
progressValue = 1 - value
}
})
$: { $: {
if (pauseOnFocus) { if (pauseOnFocus) {
if (focused) { if (focused) {
clearAutoplay() progressManager.pause()
} else { } else {
applyAutoplay() progressManager.resume()
} }
} }
} }
@@ -131,19 +162,6 @@
offsetPage(false) offsetPage(false)
} }
function applyAutoplay() {
if (autoplay && !autoplayInterval) {
autoplayInterval = setInterval(() => {
directionFnDescription[autoplayDirection]()
}, autoplayDuration)
}
}
function clearAutoplay() {
clearInterval(autoplayInterval)
autoplayInterval = null
}
function addClones() { function addClones() {
const first = pagesElement.children[0] const first = pagesElement.children[0]
const last = pagesElement.children[pagesElement.children.length - 1] const last = pagesElement.children[pagesElement.children.length - 1]
@@ -151,13 +169,38 @@
pagesElement.append(first.cloneNode(true)) 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 = [] let cleanupFns = []
onMount(() => { onMount(() => {
(async () => { (async () => {
await tick() await tick()
cleanupFns.push(store.subscribe(value => { cleanupFns.push(store.subscribe(value => {
currentPageIndex = value.currentPageIndex currentPageIndex = value.currentPageIndex
})) }))
cleanupFns.push(() => progressManager.reset())
if (pagesElement && pageWindowElement) { if (pagesElement && pageWindowElement) {
// load first and last child to clone them // load first and last child to clone them
loaded = [0, pagesElement.children.length - 1] loaded = [0, pagesElement.children.length - 1]
@@ -167,13 +210,14 @@
store.init(initialPageIndex + Number(infinite)) store.init(initialPageIndex + Number(infinite))
applyPageSizes() applyPageSizes()
} }
applyAutoplay()
applyAutoplayIfNeeded()
addResizeEventListener(applyPageSizes) addResizeEventListener(applyPageSizes)
})() })()
}) })
onDestroy(() => { onDestroy(() => {
clearAutoplay()
removeResizeEventListener(applyPageSizes) removeResizeEventListener(applyPageSizes)
cleanupFns.filter(fn => fn && typeof fn === 'function').forEach(fn => fn()) cleanupFns.filter(fn => fn && typeof fn === 'function').forEach(fn => fn())
}) })
@@ -218,14 +262,14 @@
function showPage(pageIndex, options) { function showPage(pageIndex, options) {
const animated = get(options, 'animated', true) const animated = get(options, 'animated', true)
const offsetDelayMs = get(options, 'offsetDelayMs', true) const offsetDelayMs = get(options, 'offsetDelayMs', 0)
safeChangePage(() => { safeChangePage(() => {
store.moveToPage({ pageIndex, pagesCount }) store.moveToPage({ pageIndex, pagesCount })
// delayed page transition, used for infinite autoplay to jump to real page // delayed page transition, used for infinite autoplay to jump to real page
setTimeout(() => { setTimeout(() => {
offsetPage(animated) offsetPage(animated)
const jumped = jumpIfNeeded() const jumped = jumpIfNeeded()
!jumped && applyAutoplay() !jumped && applyAutoplayIfNeeded({ delayMs: _duration }) // while offset animation is in progress (delayMs = _duration ms) wait for it
}, offsetDelayMs) }, offsetDelayMs)
}, { animated }) }, { animated })
} }
@@ -235,7 +279,7 @@
store.prev({ infinite, pagesCount }) store.prev({ infinite, pagesCount })
offsetPage(animated) offsetPage(animated)
const jumped = jumpIfNeeded() const jumped = jumpIfNeeded()
!jumped && applyAutoplay() !jumped && applyAutoplayIfNeeded({ delayMs: _duration })
}, { animated }) }, { animated })
} }
function showNextPage(options) { function showNextPage(options) {
@@ -244,7 +288,7 @@
store.next({ infinite, pagesCount }) store.next({ infinite, pagesCount })
offsetPage(animated) offsetPage(animated)
const jumped = jumpIfNeeded() const jumped = jumpIfNeeded()
!jumped && applyAutoplay() !jumped && applyAutoplayIfNeeded({ delayMs: _duration })
}, { animated }) }, { animated })
} }
@@ -301,6 +345,11 @@
> >
<slot {loaded}></slot> <slot {loaded}></slot>
</div> </div>
{#if autoplayProgressVisible}
<div class="sc-carousel-progress__container">
<Progress value={progressValue} />
</div>
{/if}
</div> </div>
{#if arrows} {#if arrows}
<slot name="next" {showNextPage}> <slot name="next" {showNextPage}>
@@ -353,6 +402,7 @@
display: flex; display: flex;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
position: relative;
} }
.sc-carousel__pages-container { .sc-carousel__pages-container {
width: 100%; width: 100%;
@@ -366,4 +416,11 @@
align-items: center; align-items: center;
justify-content: 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> </style>

View File

@@ -4,7 +4,7 @@
/** /**
* CSS animation timing function * CSS animation timing function
*/ */
export let timingFunction = "ease-in-out"; export let timingFunction = 'ease-in-out';
/** /**
* Enable Next/Previos arrows * Enable Next/Previos arrows
@@ -46,6 +46,11 @@
*/ */
export let pauseOnFocus = false export let pauseOnFocus = false
/**
* Show autoplay duration progress indicator
*/
export let autoplayProgressVisible = false
/** /**
* Current page indicator dots * Current page indicator dots
*/ */
@@ -82,6 +87,7 @@
{autoplayDuration} {autoplayDuration}
{autoplayDirection} {autoplayDirection}
{pauseOnFocus} {pauseOnFocus}
{autoplayProgressVisible}
{dots} {dots}
on:pageChange={ on:pageChange={
event => console.log(`Current page index: ${event.detail}`) event => console.log(`Current page index: ${event.detail}`)
@@ -107,6 +113,7 @@
{autoplayDuration} {autoplayDuration}
{autoplayDirection} {autoplayDirection}
{pauseOnFocus} {pauseOnFocus}
{autoplayProgressVisible}
{dots} {dots}
> >
{#each colors2 as { color, text } (color)} {#each colors2 as { color, text } (color)}

View File

@@ -4,7 +4,7 @@
/** /**
* CSS animation timing function * CSS animation timing function
*/ */
export let timingFunction = "ease-in-out"; export let timingFunction = 'ease-in-out';
/** /**
* Enable Next/Previos arrows * Enable Next/Previos arrows
@@ -46,6 +46,11 @@
*/ */
export let pauseOnFocus = false export let pauseOnFocus = false
/**
* Show autoplay duration progress indicator
*/
export let autoplayProgressVisible = false
/** /**
* Current page indicator dots * Current page indicator dots
*/ */
@@ -76,6 +81,7 @@
{autoplayDuration} {autoplayDuration}
{autoplayDirection} {autoplayDirection}
{pauseOnFocus} {pauseOnFocus}
{autoplayProgressVisible}
{dots} {dots}
let:showPrevPage let:showPrevPage
let:showNextPage let:showNextPage

View File

@@ -4,7 +4,7 @@
/** /**
* CSS animation timing function * CSS animation timing function
*/ */
export let timingFunction = "ease-in-out"; export let timingFunction = 'ease-in-out';
/** /**
* Enable Next/Previos arrows * Enable Next/Previos arrows
@@ -46,13 +46,18 @@
*/ */
export let pauseOnFocus = false export let pauseOnFocus = false
/**
* Show autoplay duration progress indicator
*/
export let autoplayProgressVisible = false
/** /**
* Current page indicator dots * Current page indicator dots
*/ */
export let dots = true export let dots = true
function onPageChange(event, showPage) { function onPageChange(event, showPage) {
showPage(event.target.value) showPage(Number(event.target.value))
} }
const colors = [ const colors = [
@@ -80,6 +85,7 @@
{autoplayDuration} {autoplayDuration}
{autoplayDirection} {autoplayDirection}
{pauseOnFocus} {pauseOnFocus}
{autoplayProgressVisible}
{dots} {dots}
let:currentPageIndex let:currentPageIndex
let:pagesCount let:pagesCount

View 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>

View 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({});

View 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>

View File

@@ -55,7 +55,7 @@
## Autoplay ## Autoplay
<Carousel <Carousel
autoplay={true} autoplay
autoplayDuration={2000} autoplayDuration={2000}
> >
{#each colors as { color, text } (color)} {#each colors as { color, text } (color)}
@@ -65,7 +65,7 @@
```jsx ```jsx
<Carousel <Carousel
autoplay={true} autoplay
autoplayDuration={2000} autoplayDuration={2000}
> >
{#each colors as { color, text } (color)} {#each colors as { color, text } (color)}
@@ -76,6 +76,58 @@
<Divider /> <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 ## Lazy loading of images
<Carousel <Carousel
let:loaded let:loaded
@@ -209,16 +261,17 @@ Import component
# Props # Props
| Prop | Type | Default | Description | | Prop | Type | Default | Description |
|----------------------|------------|-----------------|-----------------------------------------------| |---------------------------|------------|-----------------|-----------------------------------------------|
| `arrows` | `boolean` | `true` | Enable Next/Prev arrows | | `arrows` | `boolean` | `true` | Enable Next/Prev arrows |
| `infinite` | `boolean` | `true` | Infinite looping | | `infinite` | `boolean` | `true` | Infinite looping |
| `initialPageIndex` | `number` | `0` | Page to start on | | `initialPageIndex` | `number` | `0` | Page to start on |
| `duration` | `number` | `500` | Transition duration (ms) | | `duration` | `number` | `500` | Transition duration (ms) |
| `autoplay` | `boolean` | `false` | Enables autoplay of pages | | `autoplay` | `boolean` | `false` | Enables auto play of pages |
| `autoplayDuration` | `number` | `3000` | Autoplay change interval (ms) | | `autoplayDuration` | `number` | `3000` | Autoplay change interval (ms) |
| `autoplayDirection` | `string` | `'next'` | Autoplay change direction (`next` or `prev`) | | `autoplayDirection` | `string` | `'next'` | Autoplay change direction (`next` or `prev`) |
| `dots` | `boolean` | `true` | Current page indicator dots |
| `pauseOnFocus` | `boolean` | `false` | Pause autoplay on focus | | `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 | | `timingFunction` | `string` | `'ease-in-out'` | CSS animation timing function |
<Divider /> <Divider />

View 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
View File

@@ -0,0 +1,4 @@
export const setIntervalImmediate = (fn, ms) => {
fn();
return setInterval(fn, ms);
}

View 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)
})
})