Merge pull request #52 from vadimkorr/feature/#49-Update-focus-event-for-touch

feature/#49 Update focus handlers for touch events
This commit is contained in:
Vadim
2021-08-08 14:42:50 +03:00
committed by GitHub
15 changed files with 226 additions and 45 deletions

View File

@@ -43,7 +43,7 @@ Import component
| `autoplay` | `boolean` | `false` | Enables auto play 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` | Pauses autoplay on focus (for desktop - hover on the carousel to toggle the autoplay, for touchable devices - tap the carousel to toggle the autoplay) |
| `autoplayProgressVisible` | `boolean` | `false` | Show autoplay duration progress indicator | | `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

@@ -1,21 +1,15 @@
// focusin event // focusin event
export function addFocusinEventListener(source, cb) { export function addFocusinEventListener(source, cb) {
source.addEventListener('mouseenter', cb) source.addEventListener('mouseenter', cb)
source.addEventListener('touchstart', cb)
} }
export function removeFocusinEventListener(source, cb) { export function removeFocusinEventListener(source, cb) {
source.removeEventListener('mouseenter', cb) source.removeEventListener('mouseenter', cb)
source.removeEventListener('touchstart', cb)
} }
// focusout event // focusout event
export function addFocusoutEventListener(source, cb) { export function addFocusoutEventListener(source, cb) {
source.addEventListener('mouseleave', cb) source.addEventListener('mouseleave', cb)
source.addEventListener('touchend', cb)
source.addEventListener('touchcancel', cb)
} }
export function removeFocusoutEventListener(source, cb) { export function removeFocusoutEventListener(source, cb) {
source.removeEventListener('mouseleave', cb) source.removeEventListener('mouseleave', cb)
source.removeEventListener('touchend', cb)
source.removeEventListener('touchcancel', cb)
} }

View File

@@ -1,25 +1,31 @@
import { createDispatcher } from '../../utils/event' import { createDispatcher } from '../../utils/event'
import { get } from '../../utils/object'
import { import {
addFocusinEventListener, addFocusinEventListener,
removeFocusinEventListener, removeFocusinEventListener,
addFocusoutEventListener, addFocusoutEventListener,
removeFocusoutEventListener, removeFocusoutEventListener
} from './event' } from './event'
export function focusable(node) { /**
const dispatch = createDispatcher(node) * focusable events are for mouse events only
*/
export function focusable(node, options) {
// pass custom dispatch fn in order to re-translate dispatched event
const dispatch = get(options, 'dispatch', createDispatcher(node))
function handleFocusin() { function handleFocusin() {
addFocusoutEventListener(node, handleFocusout)
dispatch('focused', { value: true }) dispatch('focused', { value: true })
} }
function handleFocusout() { function handleFocusout() {
dispatch('focused', { value: false }) dispatch('focused', { value: false })
removeFocusoutEventListener(node, handleFocusout)
} }
addFocusinEventListener(node, handleFocusin) addFocusinEventListener(node, handleFocusin)
addFocusoutEventListener(node, handleFocusout)
return { return {
destroy() { destroy() {
removeFocusinEventListener(node, handleFocusin) removeFocusinEventListener(node, handleFocusin)

View File

@@ -0,0 +1 @@
export * from './pausable'

View File

@@ -0,0 +1,30 @@
import {
createDispatcher,
getIsTouchable,
} from '../../utils/event'
import { focusable } from '../focusable'
import { tappable } from '../tappable'
export function pausable(node) {
const dispatch = createDispatcher(node)
if (getIsTouchable()) {
return tappable(node, {
dispatch: (_, payload) => {
dispatch('pausedToggle', {
isTouchable: true,
...payload
})
}
})
}
return focusable(node, {
dispatch: (_, payload) => {
dispatch('pausedToggle', {
isTouchable: false,
...payload
})
}
})
}

View File

@@ -8,6 +8,7 @@ import {
removeEndEventListener, removeEndEventListener,
} from './event' } from './event'
import { createDispatcher } from '../../utils/event' import { createDispatcher } from '../../utils/event'
import { SWIPE_MIN_DURATION_MS, SWIPE_MIN_DISTANCE_PX } from '../../units'
function getCoords(event) { function getCoords(event) {
if ('TouchEvent' in window && event instanceof TouchEvent) { if ('TouchEvent' in window && event instanceof TouchEvent) {
@@ -28,49 +29,64 @@ export function swipeable(node, { thresholdProvider }) {
let x let x
let y let y
let moved = 0 let moved = 0
let swipeStartedAt
let isTouching = false
function handleMousedown(event) { function isValidSwipe() {
const swipeDurationMs = Date.now() - swipeStartedAt
return swipeDurationMs >= SWIPE_MIN_DURATION_MS && Math.abs(moved) >= SWIPE_MIN_DISTANCE_PX
}
function handleDown(event) {
swipeStartedAt = Date.now()
moved = 0 moved = 0
isTouching = true
const coords = getCoords(event) const coords = getCoords(event)
x = coords.x x = coords.x
y = coords.y y = coords.y
dispatch('start', { x, y }) dispatch('swipeStart', { x, y })
addMoveEventListener(window, handleMousemove) addMoveEventListener(window, handleMove)
addEndEventListener(window, handleMouseup) addEndEventListener(window, handleUp)
} }
function handleMousemove(event) { function handleMove(event) {
if (!isTouching) return
const coords = getCoords(event) const coords = getCoords(event)
const dx = coords.x - x const dx = coords.x - x
const dy = coords.y - y const dy = coords.y - y
x = coords.x x = coords.x
y = coords.y y = coords.y
dispatch('move', { x, y, dx, dy }) dispatch('swipeMove', { x, y, dx, dy })
if (dx !== 0 && Math.sign(dx) !== Math.sign(moved)) { if (dx !== 0 && Math.sign(dx) !== Math.sign(moved)) {
moved = 0 moved = 0
} }
moved += dx moved += dx
if (Math.abs(moved) > thresholdProvider()) { if (Math.abs(moved) > thresholdProvider()) {
dispatch('threshold', { direction: moved > 0 ? PREV : NEXT }) dispatch('swipeThresholdReached', { direction: moved > 0 ? PREV : NEXT })
removeEndEventListener(window, handleMouseup) removeEndEventListener(window, handleUp)
removeMoveEventListener(window, handleMousemove) removeMoveEventListener(window, handleMove)
} }
} }
function handleMouseup(event) { function handleUp(event) {
removeEndEventListener(window, handleUp)
removeMoveEventListener(window, handleMove)
isTouching = false
if (!isValidSwipe()) {
dispatch('swipeFailed')
return
}
const coords = getCoords(event) const coords = getCoords(event)
x = coords.x dispatch('swipeEnd', { x: coords.x, y: coords.y })
y = coords.y
dispatch('end', { x, y })
removeEndEventListener(window, handleMouseup)
removeMoveEventListener(window, handleMousemove)
} }
addStartEventListener(node, handleMousedown) addStartEventListener(node, handleDown)
return { return {
destroy() { destroy() {
removeStartEventListener(node, handleMousedown) removeStartEventListener(node, handleDown)
}, },
} }
} }

View File

@@ -0,0 +1,15 @@
// tap start event
export function addFocusinEventListener(source, cb) {
source.addEventListener('touchstart', cb)
}
export function removeFocusinEventListener(source, cb) {
source.removeEventListener('touchstart', cb)
}
// tap end event
export function addFocusoutEventListener(source, cb) {
source.addEventListener('touchend', cb)
}
export function removeFocusoutEventListener(source, cb) {
source.removeEventListener('touchend', cb)
}

View File

@@ -0,0 +1 @@
export * from './tappable'

View File

@@ -0,0 +1,66 @@
import { createDispatcher } from '../../utils/event'
import { get } from '../../utils/object'
import { getDistance } from '../../utils/math'
import {
addFocusinEventListener,
removeFocusinEventListener,
addFocusoutEventListener,
removeFocusoutEventListener,
} from './event'
import {
TAP_DURATION_MS,
TAP_MOVEMENT_PX,
} from '../../units'
/**
* tappable events are for touchable devices only
*/
export function tappable(node, options) {
// pass custom dispatch fn in order to re-translate dispatched event
const dispatch = get(options, 'dispatch', createDispatcher(node))
let tapStartedAt = 0
let tapStartPos = { x: 0, y: 0 }
function getIsValidTap({
tapEndedAt,
tapEndedPos
}) {
const tapTime = tapEndedAt - tapStartedAt
const tapDist = getDistance(tapStartPos, tapEndedPos)
return (
tapTime <= TAP_DURATION_MS &&
tapDist <= TAP_MOVEMENT_PX
)
}
function handleTapstart(event) {
tapStartedAt = Date.now()
const touch = event.touches[0]
tapStartPos = { x: touch.clientX, y: touch.clientY }
addFocusoutEventListener(node, handleTapend)
}
function handleTapend(event) {
removeFocusoutEventListener(node, handleTapend)
const touch = event.changedTouches[0]
if (getIsValidTap({
tapEndedAt: Date.now(),
tapEndedPos: { x: touch.clientX, y: touch.clientY }
})) {
dispatch('tapped')
}
}
addFocusinEventListener(node, handleTapstart)
return {
destroy() {
removeFocusinEventListener(node, handleTapstart)
removeFocusoutEventListener(node, handleTapend)
},
}
}

View File

@@ -6,7 +6,7 @@
import Progress from '../Progress/Progress.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 { pausable } from '../../actions/pausable'
import { import {
addResizeEventListener, addResizeEventListener,
removeResizeEventListener removeResizeEventListener
@@ -168,7 +168,7 @@
children[pageIndex].style.maxWidth = `${pageWidth}px` children[pageIndex].style.maxWidth = `${pageWidth}px`
} }
offsetPage(false) offsetPage({ animated: false })
} }
function addClones() { function addClones() {
@@ -190,7 +190,9 @@
return return
} }
autoplay && await autoplayDirectionFnDescription[autoplayDirection]() if (autoplay) {
await autoplayDirectionFnDescription[autoplayDirection]()
}
} }
let cleanupFns = [] let cleanupFns = []
@@ -225,7 +227,8 @@
await showPage(pageIndex + Number(infinite)) await showPage(pageIndex + Number(infinite))
} }
function offsetPage(animated) { function offsetPage(options) {
const animated = get(options, 'animated', true)
return new Promise((resolve) => { return new Promise((resolve) => {
// _duration is an offset animation time // _duration is an offset animation time
_duration = animated ? duration : 0 _duration = animated ? duration : 0
@@ -254,11 +257,12 @@
// Disable page change while animation is in progress // Disable page change while animation is in progress
let disabled = false let disabled = false
async function changePage(updateStoreFn, options) { async function changePage(updateStoreFn, options) {
progressManager.reset()
if (disabled) return if (disabled) return
disabled = true disabled = true
updateStoreFn() updateStoreFn()
await offsetPage(get(options, 'animated', true)) await offsetPage({ animated: get(options, 'animated', true) })
disabled = false disabled = false
const jumped = await jumpIfNeeded() const jumped = await jumpIfNeeded()
@@ -289,7 +293,7 @@
if (!swiping) return if (!swiping) return
_duration = 0 _duration = 0
} }
async function handleThreshold(event) { async function handleSwipeThresholdReached(event) {
if (!swiping) return if (!swiping) return
await directionFnDescription[event.detail.direction]() await directionFnDescription[event.detail.direction]()
} }
@@ -301,7 +305,16 @@
if (!swiping) return if (!swiping) return
showPage(currentPageIndex) showPage(currentPageIndex)
} }
function handleFocused(event) { async function handleSwipeFailed() {
if (!swiping) return
await offsetPage({ animated: true })
}
function handlePausedToggle(event) {
if (event.detail.isTouchable) {
focused = !focused
return
}
focused = event.detail.value focused = event.detail.value
} }
</script> </script>
@@ -322,16 +335,18 @@
<div <div
class="sc-carousel__pages-window" class="sc-carousel__pages-window"
bind:this={pageWindowElement} bind:this={pageWindowElement}
use:focusable
on:focused={handleFocused} use:pausable
on:pausedToggle={handlePausedToggle}
> >
<div <div
class="sc-carousel__pages-container" class="sc-carousel__pages-container"
use:swipeable="{{ thresholdProvider: () => pageWidth/3 }}" use:swipeable="{{ thresholdProvider: () => pageWidth/3 }}"
on:start={handleSwipeStart} on:swipeStart={handleSwipeStart}
on:move={handleSwipeMove} on:swipeMove={handleSwipeMove}
on:end={handleSwipeEnd} on:swipeEnd={handleSwipeEnd}
on:threshold={handleThreshold} on:swipeFailed={handleSwipeFailed}
on:swipeThresholdReached={handleSwipeThresholdReached}
style=" style="
transform: translateX({offset}px); transform: translateX({offset}px);
transition-duration: {_duration}ms; transition-duration: {_duration}ms;

View File

@@ -269,7 +269,7 @@ Import component
| `autoplay` | `boolean` | `false` | Enables auto play 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` | Pauses autoplay on focus (for desktop - hover on the carousel to toggle the autoplay, for touchable devices - tap the carousel to toggle the autoplay) |
| `autoplayProgressVisible` | `boolean` | `false` | Show autoplay duration progress indicator | | `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 |

5
src/units.js Normal file
View File

@@ -0,0 +1,5 @@
export const TAP_DURATION_MS = 110
export const TAP_MOVEMENT_PX = 9 // max movement during the tap, keep it small
export const SWIPE_MIN_DURATION_MS = 111
export const SWIPE_MIN_DISTANCE_PX = 20

View File

@@ -7,12 +7,19 @@ export function removeResizeEventListener(cb) {
} }
export function createDispatcher(source) { export function createDispatcher(source) {
function dispatch(event, data) { return function (event, data) {
source.dispatchEvent( source.dispatchEvent(
new CustomEvent(event, { new CustomEvent(event, {
detail: data, detail: data,
}) })
) )
} }
return dispatch }
export function getIsTouchable() {
return (
('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0)
)
} }

6
src/utils/math.js Normal file
View File

@@ -0,0 +1,6 @@
export const getDistance = (p1, p2) => {
const xDist = p2.x - p1.x;
const yDist = p2.y - p1.y;
return Math.sqrt((xDist * xDist) + (yDist * yDist));
}

19
src/utils/math.test.js Normal file
View File

@@ -0,0 +1,19 @@
import {
getDistance,
} from './math.js'
describe('getDistance', () => {
it('returns correct distance between 2 points laying in one horizontal line', () => {
const p1 = { x: 0, y: 0 }
const p2 = { x: 5, y: 0 }
expect(getDistance(p1, p2)).toBe(5)
})
it('returns correct distance between 2 points', () => {
const p1 = { x: 1, y: 1 }
const p2 = { x: 5, y: 4 }
expect(getDistance(p1, p2)).toBe(5)
})
})