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:
@@ -43,7 +43,7 @@ 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`) |
|
||||
| `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 |
|
||||
| `dots` | `boolean` | `true` | Current page indicator dots |
|
||||
| `timingFunction` | `string` | `'ease-in-out'` | CSS animation timing function |
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
// focusin event
|
||||
export function addFocusinEventListener(source, cb) {
|
||||
source.addEventListener('mouseenter', cb)
|
||||
source.addEventListener('touchstart', cb)
|
||||
}
|
||||
export function removeFocusinEventListener(source, cb) {
|
||||
source.removeEventListener('mouseenter', cb)
|
||||
source.removeEventListener('touchstart', cb)
|
||||
}
|
||||
|
||||
// focusout event
|
||||
export function addFocusoutEventListener(source, cb) {
|
||||
source.addEventListener('mouseleave', cb)
|
||||
source.addEventListener('touchend', cb)
|
||||
source.addEventListener('touchcancel', cb)
|
||||
}
|
||||
export function removeFocusoutEventListener(source, cb) {
|
||||
source.removeEventListener('mouseleave', cb)
|
||||
source.removeEventListener('touchend', cb)
|
||||
source.removeEventListener('touchcancel', cb)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import { createDispatcher } from '../../utils/event'
|
||||
import { get } from '../../utils/object'
|
||||
import {
|
||||
addFocusinEventListener,
|
||||
removeFocusinEventListener,
|
||||
addFocusoutEventListener,
|
||||
removeFocusoutEventListener,
|
||||
removeFocusoutEventListener
|
||||
} 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() {
|
||||
addFocusoutEventListener(node, handleFocusout)
|
||||
dispatch('focused', { value: true })
|
||||
}
|
||||
|
||||
function handleFocusout() {
|
||||
dispatch('focused', { value: false })
|
||||
removeFocusoutEventListener(node, handleFocusout)
|
||||
}
|
||||
|
||||
addFocusinEventListener(node, handleFocusin)
|
||||
addFocusoutEventListener(node, handleFocusout)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
|
||||
1
src/actions/pausable/index.js
Normal file
1
src/actions/pausable/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './pausable'
|
||||
30
src/actions/pausable/pausable.js
Normal file
30
src/actions/pausable/pausable.js
Normal 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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
removeEndEventListener,
|
||||
} from './event'
|
||||
import { createDispatcher } from '../../utils/event'
|
||||
import { SWIPE_MIN_DURATION_MS, SWIPE_MIN_DISTANCE_PX } from '../../units'
|
||||
|
||||
function getCoords(event) {
|
||||
if ('TouchEvent' in window && event instanceof TouchEvent) {
|
||||
@@ -28,49 +29,64 @@ export function swipeable(node, { thresholdProvider }) {
|
||||
let x
|
||||
let y
|
||||
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
|
||||
isTouching = true
|
||||
const coords = getCoords(event)
|
||||
x = coords.x
|
||||
y = coords.y
|
||||
dispatch('start', { x, y })
|
||||
addMoveEventListener(window, handleMousemove)
|
||||
addEndEventListener(window, handleMouseup)
|
||||
dispatch('swipeStart', { x, y })
|
||||
addMoveEventListener(window, handleMove)
|
||||
addEndEventListener(window, handleUp)
|
||||
}
|
||||
|
||||
function handleMousemove(event) {
|
||||
function handleMove(event) {
|
||||
if (!isTouching) return
|
||||
const coords = getCoords(event)
|
||||
const dx = coords.x - x
|
||||
const dy = coords.y - y
|
||||
x = coords.x
|
||||
y = coords.y
|
||||
dispatch('move', { x, y, dx, dy })
|
||||
dispatch('swipeMove', { x, y, dx, dy })
|
||||
|
||||
if (dx !== 0 && Math.sign(dx) !== Math.sign(moved)) {
|
||||
moved = 0
|
||||
}
|
||||
moved += dx
|
||||
if (Math.abs(moved) > thresholdProvider()) {
|
||||
dispatch('threshold', { direction: moved > 0 ? PREV : NEXT })
|
||||
removeEndEventListener(window, handleMouseup)
|
||||
removeMoveEventListener(window, handleMousemove)
|
||||
dispatch('swipeThresholdReached', { direction: moved > 0 ? PREV : NEXT })
|
||||
removeEndEventListener(window, handleUp)
|
||||
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)
|
||||
x = coords.x
|
||||
y = coords.y
|
||||
dispatch('end', { x, y })
|
||||
removeEndEventListener(window, handleMouseup)
|
||||
removeMoveEventListener(window, handleMousemove)
|
||||
dispatch('swipeEnd', { x: coords.x, y: coords.y })
|
||||
}
|
||||
|
||||
addStartEventListener(node, handleMousedown)
|
||||
addStartEventListener(node, handleDown)
|
||||
return {
|
||||
destroy() {
|
||||
removeStartEventListener(node, handleMousedown)
|
||||
removeStartEventListener(node, handleDown)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
15
src/actions/tappable/event.js
Normal file
15
src/actions/tappable/event.js
Normal 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)
|
||||
}
|
||||
1
src/actions/tappable/index.js
Normal file
1
src/actions/tappable/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './tappable'
|
||||
66
src/actions/tappable/tappable.js
Normal file
66
src/actions/tappable/tappable.js
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
import Progress from '../Progress/Progress.svelte'
|
||||
import { NEXT, PREV } from '../../direction'
|
||||
import { swipeable } from '../../actions/swipeable'
|
||||
import { focusable } from '../../actions/focusable'
|
||||
import { pausable } from '../../actions/pausable'
|
||||
import {
|
||||
addResizeEventListener,
|
||||
removeResizeEventListener
|
||||
@@ -168,7 +168,7 @@
|
||||
children[pageIndex].style.maxWidth = `${pageWidth}px`
|
||||
}
|
||||
|
||||
offsetPage(false)
|
||||
offsetPage({ animated: false })
|
||||
}
|
||||
|
||||
function addClones() {
|
||||
@@ -190,7 +190,9 @@
|
||||
return
|
||||
}
|
||||
|
||||
autoplay && await autoplayDirectionFnDescription[autoplayDirection]()
|
||||
if (autoplay) {
|
||||
await autoplayDirectionFnDescription[autoplayDirection]()
|
||||
}
|
||||
}
|
||||
|
||||
let cleanupFns = []
|
||||
@@ -225,7 +227,8 @@
|
||||
await showPage(pageIndex + Number(infinite))
|
||||
}
|
||||
|
||||
function offsetPage(animated) {
|
||||
function offsetPage(options) {
|
||||
const animated = get(options, 'animated', true)
|
||||
return new Promise((resolve) => {
|
||||
// _duration is an offset animation time
|
||||
_duration = animated ? duration : 0
|
||||
@@ -254,11 +257,12 @@
|
||||
// Disable page change while animation is in progress
|
||||
let disabled = false
|
||||
async function changePage(updateStoreFn, options) {
|
||||
progressManager.reset()
|
||||
if (disabled) return
|
||||
disabled = true
|
||||
|
||||
updateStoreFn()
|
||||
await offsetPage(get(options, 'animated', true))
|
||||
await offsetPage({ animated: get(options, 'animated', true) })
|
||||
disabled = false
|
||||
|
||||
const jumped = await jumpIfNeeded()
|
||||
@@ -289,7 +293,7 @@
|
||||
if (!swiping) return
|
||||
_duration = 0
|
||||
}
|
||||
async function handleThreshold(event) {
|
||||
async function handleSwipeThresholdReached(event) {
|
||||
if (!swiping) return
|
||||
await directionFnDescription[event.detail.direction]()
|
||||
}
|
||||
@@ -301,7 +305,16 @@
|
||||
if (!swiping) return
|
||||
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
|
||||
}
|
||||
</script>
|
||||
@@ -322,16 +335,18 @@
|
||||
<div
|
||||
class="sc-carousel__pages-window"
|
||||
bind:this={pageWindowElement}
|
||||
use:focusable
|
||||
on:focused={handleFocused}
|
||||
|
||||
use:pausable
|
||||
on:pausedToggle={handlePausedToggle}
|
||||
>
|
||||
<div
|
||||
class="sc-carousel__pages-container"
|
||||
use:swipeable="{{ thresholdProvider: () => pageWidth/3 }}"
|
||||
on:start={handleSwipeStart}
|
||||
on:move={handleSwipeMove}
|
||||
on:end={handleSwipeEnd}
|
||||
on:threshold={handleThreshold}
|
||||
on:swipeStart={handleSwipeStart}
|
||||
on:swipeMove={handleSwipeMove}
|
||||
on:swipeEnd={handleSwipeEnd}
|
||||
on:swipeFailed={handleSwipeFailed}
|
||||
on:swipeThresholdReached={handleSwipeThresholdReached}
|
||||
style="
|
||||
transform: translateX({offset}px);
|
||||
transition-duration: {_duration}ms;
|
||||
|
||||
@@ -269,7 +269,7 @@ 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`) |
|
||||
| `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 |
|
||||
| `dots` | `boolean` | `true` | Current page indicator dots |
|
||||
| `timingFunction` | `string` | `'ease-in-out'` | CSS animation timing function |
|
||||
|
||||
5
src/units.js
Normal file
5
src/units.js
Normal 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
|
||||
@@ -7,12 +7,19 @@ export function removeResizeEventListener(cb) {
|
||||
}
|
||||
|
||||
export function createDispatcher(source) {
|
||||
function dispatch(event, data) {
|
||||
return function (event, data) {
|
||||
source.dispatchEvent(
|
||||
new CustomEvent(event, {
|
||||
detail: data,
|
||||
})
|
||||
)
|
||||
}
|
||||
return dispatch
|
||||
}
|
||||
|
||||
export function getIsTouchable() {
|
||||
return (
|
||||
('ontouchstart' in window) ||
|
||||
(navigator.maxTouchPoints > 0) ||
|
||||
(navigator.msMaxTouchPoints > 0)
|
||||
)
|
||||
}
|
||||
|
||||
6
src/utils/math.js
Normal file
6
src/utils/math.js
Normal 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
19
src/utils/math.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user