@graphcommerce/framer-scroller 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/components/Scroller.tsx +1 -1
- package/components/ScrollerButton.tsx +1 -1
- package/components/ScrollerDots.tsx +1 -0
- package/components/ScrollerProvider.tsx +41 -35
- package/hooks/useScrollTo.ts +43 -31
- package/hooks/useScroller.ts +118 -38
- package/hooks/useVelocitySnapTo.ts +43 -27
- package/package.json +7 -6
- package/types.ts +7 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.1.0](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/framer-scroller@1.0.4...@graphcommerce/framer-scroller@1.1.0) (2021-12-03)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **framer-scroller:** do not scroll in the direction that is not being scrolled when dragging and thus delaying the reset to snap ([3198eed](https://github.com/ho-nl/m2-pwa/commit/3198eed7977039f712784ee6c17031d7efb20c25))
|
|
12
|
+
* **framer-scroller:** recursively find snapPoints for deeply nested children ([2203f00](https://github.com/ho-nl/m2-pwa/commit/2203f00a97bb4e4b381acad1d86b177105874d1f))
|
|
13
|
+
* **framer-scroller:** set the scrollTop when switching snap ([8bd35a2](https://github.com/ho-nl/m2-pwa/commit/8bd35a2ebf5d10b44d04048c6360c7ac770221cf))
|
|
14
|
+
* make sure the overlay becomes visible, even if the overlay is scrolled ([1738c98](https://github.com/ho-nl/m2-pwa/commit/1738c982ea84ec2b93daa824c4b8c86ab2a3f5ed))
|
|
15
|
+
* make sure the overlays are rendered correctly on mobile ([48f7050](https://github.com/ho-nl/m2-pwa/commit/48f705060e99b997f5b1db03ccc49f1051a1ed8f))
|
|
16
|
+
* make the headerHeight properly configurable ([c39c942](https://github.com/ho-nl/m2-pwa/commit/c39c942a62a9bb9687ea553be28e37fb49a6b065))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **framer-scroller-sheet:** created package replacing the framer-sheet package ([f9f2e91](https://github.com/ho-nl/m2-pwa/commit/f9f2e9101191f5cb5c4514ceb9534ddeb2476763))
|
|
22
|
+
* **framer-scroller:** find the direction of the scroller and set proper values ([631e24c](https://github.com/ho-nl/m2-pwa/commit/631e24c5c7ff67b49f83390789e8dadcb07eca04))
|
|
23
|
+
* **framer-scroller:** get the scrollSnapAlign from the element instead of the body ([ec8c24e](https://github.com/ho-nl/m2-pwa/commit/ec8c24e6d457a3ed1c055b27d7b94be5ed4b6f2c))
|
|
24
|
+
* **framer-scroller:** provide promise with scrollTo ([cbe59d9](https://github.com/ho-nl/m2-pwa/commit/cbe59d9e753cc2ab76b2de048139e0319f08d035))
|
|
25
|
+
* **framer-scroller:** split the grid functionality from the scroller ([81307ea](https://github.com/ho-nl/m2-pwa/commit/81307ea2652bf31a1f94e8db72af4ee161bdca2e))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
6
31
|
## [1.0.2](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/framer-scroller@1.0.1...@graphcommerce/framer-scroller@1.0.2) (2021-11-03)
|
|
7
32
|
|
|
8
33
|
|
package/components/Scroller.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { forwardRef } from 'react'
|
|
|
3
3
|
import { ScrollableProps, useScroller } from '../hooks/useScroller'
|
|
4
4
|
|
|
5
5
|
const Scroller = forwardRef<HTMLDivElement, ScrollableProps>((props, forwardedRef) => {
|
|
6
|
-
const scroller = useScroller<'div'>(props, forwardedRef)
|
|
6
|
+
const scroller = useScroller<'div'>({ grid: true, ...props }, forwardedRef)
|
|
7
7
|
return <m.div {...scroller} />
|
|
8
8
|
})
|
|
9
9
|
Scroller.displayName = 'Scroller'
|
|
@@ -2,9 +2,9 @@ import { UseStyles } from '@graphcommerce/next-ui'
|
|
|
2
2
|
import { Fab, FabProps, makeStyles, Theme } from '@material-ui/core'
|
|
3
3
|
import { m, useMotionValue, useSpring } from 'framer-motion'
|
|
4
4
|
import React from 'react'
|
|
5
|
-
import { useWatchItems } from '..'
|
|
6
5
|
import { useScrollTo } from '../hooks/useScrollTo'
|
|
7
6
|
import { useScrollerContext } from '../hooks/useScrollerContext'
|
|
7
|
+
import { useWatchItems } from '../hooks/useWatchItems'
|
|
8
8
|
import { SnapPositionDirection } from '../types'
|
|
9
9
|
|
|
10
10
|
const useStyles = makeStyles((theme: Theme) => ({
|
|
@@ -55,6 +55,7 @@ const ScrollerDots = m(
|
|
|
55
55
|
{...fabProps}
|
|
56
56
|
onClick={() => {
|
|
57
57
|
const positions = getScrollSnapPositions()
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
58
59
|
scrollTo({ x: positions.x[idx] ?? 0, y: positions.y[idx] ?? 0 })
|
|
59
60
|
}}
|
|
60
61
|
className={clsx(dot, props.className)}
|
|
@@ -16,26 +16,19 @@ import {
|
|
|
16
16
|
|
|
17
17
|
export type ScrollerProviderProps = {
|
|
18
18
|
children?: React.ReactNode | undefined
|
|
19
|
-
|
|
19
|
+
scrollSnapTypeSm?: ScrollSnapType
|
|
20
|
+
scrollSnapTypeMd?: ScrollSnapType
|
|
20
21
|
scrollSnapAlign?: ScrollSnapAlign
|
|
21
22
|
scrollSnapStop?: ScrollSnapStop
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
function useObserveItems(
|
|
25
|
-
scrollerRef: ReactHtmlRefObject,
|
|
26
|
-
items: MotionValue<ItemState[]>,
|
|
27
|
-
enableSnap: ScrollerContext['enableSnap'],
|
|
28
|
-
) {
|
|
25
|
+
function useObserveItems(scrollerRef: ReactHtmlRefObject, items: MotionValue<ItemState[]>) {
|
|
29
26
|
const observe = useCallback(
|
|
30
27
|
(itemsArr: ItemState[]) => {
|
|
31
28
|
if (!scrollerRef.current) return () => {}
|
|
32
29
|
|
|
33
30
|
const find = ({ target }: { target: Element }) => itemsArr.find((i) => i.el === target)
|
|
34
31
|
|
|
35
|
-
const resizeCallback = (entry: ResizeObserverEntry) => {
|
|
36
|
-
enableSnap()
|
|
37
|
-
find(entry)
|
|
38
|
-
}
|
|
39
32
|
const intersectionCallback = (entry: IntersectionObserverEntry) => {
|
|
40
33
|
const item = find(entry)
|
|
41
34
|
item?.visibility.set(entry.intersectionRatio)
|
|
@@ -44,7 +37,6 @@ function useObserveItems(
|
|
|
44
37
|
const scaled = (1 - 0.2) * entry.intersectionRatio + 0.2
|
|
45
38
|
item?.opacity.set(scaled)
|
|
46
39
|
}
|
|
47
|
-
const ro = new ResizeObserver((entries) => entries.forEach(resizeCallback))
|
|
48
40
|
const io = new IntersectionObserver((entries) => entries.forEach(intersectionCallback), {
|
|
49
41
|
root: scrollerRef.current,
|
|
50
42
|
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
|
|
@@ -55,18 +47,12 @@ function useObserveItems(
|
|
|
55
47
|
)
|
|
56
48
|
itemsArr.forEach((value, idx) => {
|
|
57
49
|
if (!value.el) value.el = htmlEls?.[idx]
|
|
58
|
-
if (value.el)
|
|
59
|
-
ro.observe(value.el)
|
|
60
|
-
io.observe(value.el)
|
|
61
|
-
}
|
|
50
|
+
if (value.el) io.observe(value.el)
|
|
62
51
|
})
|
|
63
52
|
|
|
64
|
-
return () =>
|
|
65
|
-
ro.disconnect()
|
|
66
|
-
io.disconnect()
|
|
67
|
-
}
|
|
53
|
+
return () => io.disconnect()
|
|
68
54
|
},
|
|
69
|
-
[
|
|
55
|
+
[scrollerRef],
|
|
70
56
|
)
|
|
71
57
|
|
|
72
58
|
useEffect(() => {
|
|
@@ -82,12 +68,14 @@ export default function ScrollerProvider(props: ScrollerProviderProps) {
|
|
|
82
68
|
const {
|
|
83
69
|
scrollSnapAlign = 'center center',
|
|
84
70
|
scrollSnapStop = 'normal',
|
|
85
|
-
|
|
71
|
+
scrollSnapTypeSm = 'inline mandatory',
|
|
72
|
+
scrollSnapTypeMd = 'inline mandatory',
|
|
86
73
|
...providerProps
|
|
87
74
|
} = props
|
|
75
|
+
|
|
88
76
|
const scrollSnap = useMemo(
|
|
89
|
-
() => ({
|
|
90
|
-
[scrollSnapAlign, scrollSnapStop,
|
|
77
|
+
() => ({ scrollSnapTypeMd, scrollSnapTypeSm, scrollSnapStop, scrollSnapAlign }),
|
|
78
|
+
[scrollSnapAlign, scrollSnapStop, scrollSnapTypeMd, scrollSnapTypeSm],
|
|
91
79
|
)
|
|
92
80
|
|
|
93
81
|
const snap = useMotionValue(true)
|
|
@@ -107,19 +95,25 @@ export default function ScrollerProvider(props: ScrollerProviderProps) {
|
|
|
107
95
|
}, [])
|
|
108
96
|
|
|
109
97
|
const disableSnap = useCallback(() => {
|
|
98
|
+
if (snap.get() === false) return
|
|
110
99
|
stop()
|
|
111
100
|
snap.set(false)
|
|
112
101
|
}, [snap, stop])
|
|
113
102
|
|
|
114
103
|
const enableSnap = useCallback(() => {
|
|
115
|
-
if (!scrollerRef.current) return
|
|
104
|
+
if (!scrollerRef.current || snap.get() === true) return
|
|
105
|
+
|
|
116
106
|
stop()
|
|
117
|
-
|
|
107
|
+
|
|
108
|
+
// We're setting the current scrollLeft to prevent resetting the scroll position on Safari
|
|
109
|
+
const l = scrollerRef.current.scrollLeft
|
|
110
|
+
const t = scrollerRef.current.scrollTop
|
|
118
111
|
snap.set(true)
|
|
119
|
-
scrollerRef.current.scrollLeft =
|
|
112
|
+
scrollerRef.current.scrollLeft = l
|
|
113
|
+
scrollerRef.current.scrollTop = t
|
|
120
114
|
}, [snap, stop])
|
|
121
115
|
|
|
122
|
-
useObserveItems(scrollerRef, items
|
|
116
|
+
useObserveItems(scrollerRef, items)
|
|
123
117
|
|
|
124
118
|
const registerChildren = useCallback(
|
|
125
119
|
(children: React.ReactNode) => {
|
|
@@ -152,6 +146,18 @@ export default function ScrollerProvider(props: ScrollerProviderProps) {
|
|
|
152
146
|
)
|
|
153
147
|
}
|
|
154
148
|
|
|
149
|
+
/** Finds all elements with scrollSnapAlign by using getComputedStyle */
|
|
150
|
+
function recursivelyFindElementsWithScrollSnapAlign(parent: HTMLElement) {
|
|
151
|
+
const elements: HTMLElement[] = []
|
|
152
|
+
;[...parent.children].forEach((child) => {
|
|
153
|
+
if (!(child instanceof HTMLElement)) return
|
|
154
|
+
|
|
155
|
+
if (getComputedStyle(child).scrollSnapAlign !== 'none') elements.push(child)
|
|
156
|
+
elements.push(...recursivelyFindElementsWithScrollSnapAlign(child))
|
|
157
|
+
})
|
|
158
|
+
return elements
|
|
159
|
+
}
|
|
160
|
+
|
|
155
161
|
function getSnapPositions(
|
|
156
162
|
parent: HTMLElement,
|
|
157
163
|
excludeOffAxis = true,
|
|
@@ -163,7 +169,7 @@ export default function ScrollerProvider(props: ScrollerProviderProps) {
|
|
|
163
169
|
y: { start: [], center: [], end: [] },
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
const descendants =
|
|
172
|
+
const descendants = recursivelyFindElementsWithScrollSnapAlign(parent)
|
|
167
173
|
|
|
168
174
|
for (const axis of ['x', 'y'] as Axis[]) {
|
|
169
175
|
const orthogonalAxis = axis === 'x' ? 'y' : 'x'
|
|
@@ -176,16 +182,16 @@ export default function ScrollerProvider(props: ScrollerProviderProps) {
|
|
|
176
182
|
|
|
177
183
|
// Skip child if it doesn't intersect the parent's opposite axis (it can never be in view)
|
|
178
184
|
if (excludeOffAxis && !domRectIntersects(parentRect, childRect, orthogonalAxis)) {
|
|
185
|
+
// eslint-disable-next-line no-continue
|
|
179
186
|
continue
|
|
180
187
|
}
|
|
181
188
|
|
|
182
|
-
|
|
183
|
-
let [childAlignY, childAlignX] =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
+
const align = getComputedStyle(child).scrollSnapAlign
|
|
190
|
+
let [childAlignY, childAlignX] = align.split(' ') as [
|
|
191
|
+
ScrollSnapAlignAxis,
|
|
192
|
+
ScrollSnapAlignAxis | undefined,
|
|
193
|
+
]
|
|
194
|
+
if (typeof childAlignX === 'undefined') childAlignX = childAlignY
|
|
189
195
|
|
|
190
196
|
const childAlign = axis === 'x' ? childAlignX : childAlignY
|
|
191
197
|
const childOffsetStart = childRect[axisStart] - parentRect[axisStart] + parent[axisScroll]
|
package/hooks/useScrollTo.ts
CHANGED
|
@@ -7,38 +7,50 @@ export function useScrollTo() {
|
|
|
7
7
|
const { scrollerRef, register, disableSnap, enableSnap } = useScrollerContext()
|
|
8
8
|
const scroll = useElementScroll(scrollerRef)
|
|
9
9
|
|
|
10
|
-
return (to: Point2D) => {
|
|
11
|
-
|
|
10
|
+
return async (to: Point2D) => {
|
|
11
|
+
const ref = scrollerRef.current
|
|
12
|
+
if (!ref) return
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const xDone = new Promise<void>((onComplete) => {
|
|
15
|
+
if (ref.scrollLeft !== to.x) {
|
|
16
|
+
disableSnap()
|
|
17
|
+
register(
|
|
18
|
+
animate({
|
|
19
|
+
from: ref.scrollLeft,
|
|
20
|
+
to: to.x,
|
|
21
|
+
velocity: scroll.x.getVelocity(),
|
|
22
|
+
onUpdate: (v) => {
|
|
23
|
+
ref.scrollLeft = v
|
|
24
|
+
},
|
|
25
|
+
onComplete,
|
|
26
|
+
onStop: onComplete,
|
|
27
|
+
bounce: 50,
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
} else onComplete()
|
|
31
|
+
})
|
|
15
32
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
onComplete: enableSnap,
|
|
40
|
-
bounce: 50,
|
|
41
|
-
}),
|
|
42
|
-
)
|
|
33
|
+
const yDone = new Promise<void>((onComplete) => {
|
|
34
|
+
if (ref.scrollTop !== to.y) {
|
|
35
|
+
disableSnap()
|
|
36
|
+
register(
|
|
37
|
+
animate({
|
|
38
|
+
from: ref.scrollTop,
|
|
39
|
+
to: to.y,
|
|
40
|
+
velocity: scroll.y.getVelocity(),
|
|
41
|
+
onUpdate: (v) => {
|
|
42
|
+
ref.scrollTop = v
|
|
43
|
+
},
|
|
44
|
+
onComplete,
|
|
45
|
+
onStop: onComplete,
|
|
46
|
+
bounce: 50,
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
} else onComplete()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await xDone
|
|
53
|
+
await yDone
|
|
54
|
+
enableSnap()
|
|
43
55
|
}
|
|
44
56
|
}
|
package/hooks/useScroller.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useConstant, useElementScroll, useMotionValueValue } from '@graphcommerce/framer-utils'
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { UseStyles, classesPicker } from '@graphcommerce/next-ui'
|
|
3
|
+
import { makeStyles, Theme } from '@material-ui/core'
|
|
4
4
|
import {
|
|
5
5
|
HTMLMotionProps,
|
|
6
6
|
motionValue,
|
|
@@ -11,52 +11,122 @@ import {
|
|
|
11
11
|
useTransform,
|
|
12
12
|
} from 'framer-motion'
|
|
13
13
|
import React, { ReactHTML, useState } from 'react'
|
|
14
|
-
import { ScrollSnapProps } from '../types'
|
|
14
|
+
import { ScrollSnapProps, ScrollSnapType } from '../types'
|
|
15
15
|
import { isHTMLMousePointerEvent } from '../utils/isHTMLMousePointerEvent'
|
|
16
16
|
import { useScrollerContext } from './useScrollerContext'
|
|
17
17
|
import { useVelocitySnapTo } from './useVelocitySnapTo'
|
|
18
18
|
|
|
19
19
|
const useStyles = makeStyles(
|
|
20
|
-
{
|
|
21
|
-
root:
|
|
20
|
+
(theme: Theme) => ({
|
|
21
|
+
root: {
|
|
22
|
+
'& *': {
|
|
23
|
+
userSelect: 'none',
|
|
24
|
+
userDrag: 'none',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
rootSmSnapDirNone: {
|
|
29
|
+
[theme.breakpoints.down('sm')]: {
|
|
30
|
+
overflow: 'hidden',
|
|
31
|
+
overscrollBehavior: 'auto',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
rootMdSnapDirNone: {
|
|
35
|
+
[theme.breakpoints.up('md')]: {
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
overscrollBehavior: 'auto',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
rootSmSnapDirBlock: {
|
|
41
|
+
[theme.breakpoints.down('sm')]: {
|
|
42
|
+
overflowY: 'auto',
|
|
43
|
+
overscrollBehaviorBlock: 'contain',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
rootMdSnapDirBlock: {
|
|
47
|
+
[theme.breakpoints.up('md')]: {
|
|
48
|
+
overflowY: 'auto',
|
|
49
|
+
overscrollBehaviorBlock: 'contain',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
rootSmSnapDirInline: {
|
|
53
|
+
[theme.breakpoints.down('sm')]: {
|
|
54
|
+
overflowX: 'auto',
|
|
55
|
+
overscrollBehaviorInline: 'contain',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
rootMdSnapDirInline: {
|
|
59
|
+
[theme.breakpoints.up('md')]: {
|
|
60
|
+
overflowX: 'auto',
|
|
61
|
+
overscrollBehaviorInline: 'contain',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
rootSmSnapDirBoth: {
|
|
65
|
+
[theme.breakpoints.down('sm')]: {
|
|
66
|
+
overflow: 'auto',
|
|
67
|
+
overscrollBehavior: 'contain',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
rootMdSnapDirBoth: {
|
|
71
|
+
[theme.breakpoints.up('md')]: {
|
|
72
|
+
overflow: 'auto',
|
|
73
|
+
overscrollBehavior: 'contain',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
rootSmGridDirBlock: ({ scrollSnapAlign, scrollSnapStop }) => ({
|
|
22
78
|
display: 'grid',
|
|
23
|
-
gridAutoFlow: '
|
|
79
|
+
gridAutoFlow: 'row',
|
|
24
80
|
gridAutoColumns: `40%`,
|
|
25
|
-
overflow: `auto`,
|
|
26
|
-
overscrollBehaviorInline: `contain`,
|
|
27
81
|
'& > *': {
|
|
28
82
|
scrollSnapAlign,
|
|
29
83
|
scrollSnapStop,
|
|
30
84
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
85
|
+
}),
|
|
86
|
+
rootSmGridDirInline: ({ scrollSnapAlign, scrollSnapStop }) => ({
|
|
87
|
+
display: 'grid',
|
|
88
|
+
gridAutoFlow: 'column',
|
|
89
|
+
gridAutoRows: `40%`,
|
|
90
|
+
'& > *': {
|
|
91
|
+
scrollSnapAlign,
|
|
92
|
+
scrollSnapStop,
|
|
34
93
|
},
|
|
35
94
|
}),
|
|
36
|
-
|
|
95
|
+
rootCanGrab: {
|
|
37
96
|
cursor: 'grab',
|
|
38
97
|
},
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
rootIsSnap: ({ scrollSnapTypeSm, scrollSnapTypeMd }: ScrollSnapProps) => ({
|
|
99
|
+
[theme.breakpoints.down('sm')]: {
|
|
100
|
+
scrollSnapType: scrollSnapTypeSm,
|
|
101
|
+
},
|
|
102
|
+
[theme.breakpoints.up('md')]: {
|
|
103
|
+
scrollSnapType: scrollSnapTypeMd,
|
|
104
|
+
},
|
|
41
105
|
}),
|
|
42
|
-
|
|
106
|
+
rootIsPanning: {
|
|
43
107
|
cursor: 'grabbing !important',
|
|
44
108
|
'& > *': {
|
|
45
109
|
pointerEvents: 'none',
|
|
46
110
|
},
|
|
47
111
|
},
|
|
48
|
-
|
|
112
|
+
rootHideScrollbar: {
|
|
49
113
|
scrollbarWidth: 'none',
|
|
50
114
|
'&::-webkit-scrollbar': {
|
|
51
115
|
display: 'none',
|
|
52
116
|
},
|
|
53
117
|
},
|
|
54
|
-
},
|
|
118
|
+
}),
|
|
55
119
|
{ name: 'Scroller' },
|
|
56
120
|
)
|
|
57
121
|
|
|
58
|
-
export type ScrollableProps<TagName extends keyof ReactHTML = 'div'> =
|
|
59
|
-
hideScrollbar?: boolean
|
|
122
|
+
export type ScrollableProps<TagName extends keyof ReactHTML = 'div'> = UseStyles<typeof useStyles> &
|
|
123
|
+
HTMLMotionProps<TagName> & { hideScrollbar?: boolean; grid?: boolean }
|
|
124
|
+
|
|
125
|
+
export function scrollSnapTypeDirection(scrollSnapType: ScrollSnapType) {
|
|
126
|
+
let smSnapDir = scrollSnapType.split(' ')[0]
|
|
127
|
+
smSnapDir = smSnapDir.replace('y', 'block')
|
|
128
|
+
smSnapDir = smSnapDir.replace('x', 'inline') as 'block' | 'inline' | 'both' | 'inline'
|
|
129
|
+
return smSnapDir
|
|
60
130
|
}
|
|
61
131
|
|
|
62
132
|
/** Make any HTML */
|
|
@@ -64,10 +134,11 @@ export function useScroller<TagName extends keyof ReactHTML = 'div'>(
|
|
|
64
134
|
props: ScrollableProps<TagName>,
|
|
65
135
|
forwardedRef: React.ForwardedRef<any>,
|
|
66
136
|
) {
|
|
137
|
+
const { hideScrollbar = false, children, grid = false, ...divProps } = props
|
|
138
|
+
|
|
67
139
|
const { scrollSnap, scrollerRef, enableSnap, disableSnap, snap, registerChildren } =
|
|
68
140
|
useScrollerContext()
|
|
69
141
|
|
|
70
|
-
const { hideScrollbar, children, ...divProps } = props
|
|
71
142
|
registerChildren(children)
|
|
72
143
|
|
|
73
144
|
const scroll = useElementScroll(scrollerRef)
|
|
@@ -77,28 +148,28 @@ export function useScroller<TagName extends keyof ReactHTML = 'div'>(
|
|
|
77
148
|
[scroll.xMax, scroll.yMax] as MotionValue<string | number>[],
|
|
78
149
|
([xMax, yMax]: number[]) => xMax || yMax,
|
|
79
150
|
),
|
|
80
|
-
(v) => v,
|
|
151
|
+
(v) => !!v,
|
|
81
152
|
)
|
|
82
153
|
|
|
83
154
|
const isSnap = useMotionValueValue(snap, (v) => v)
|
|
84
155
|
|
|
85
156
|
const classes = useStyles(scrollSnap)
|
|
86
157
|
|
|
87
|
-
const
|
|
158
|
+
const snapToVelocity = useVelocitySnapTo(scrollerRef)
|
|
159
|
+
|
|
88
160
|
const [isPanning, setPanning] = useState(false)
|
|
89
161
|
|
|
162
|
+
/** If the scroller doesn't have snap enabled and the user is not panning, enable snap */
|
|
90
163
|
useDomEvent(scrollerRef as React.RefObject<EventTarget>, 'wheel', (e) => {
|
|
91
164
|
/**
|
|
92
165
|
* Todo: this is actually incorrect because when enabling the snap points, the area jumps to the
|
|
93
166
|
* nearest point a snap.
|
|
94
167
|
*
|
|
95
|
-
* What we
|
|
96
|
-
*
|
|
97
|
-
*
|
|
168
|
+
* What we SHOULD do is wait for the scroll position to be set exactly on a snappoint and then
|
|
169
|
+
* enable it. However, to do that then we need to know the position of all elements at all time,
|
|
170
|
+
* we now are lazy :)
|
|
98
171
|
*/
|
|
99
|
-
if (!snap.get() && !isPanning && e instanceof WheelEvent)
|
|
100
|
-
enableSnap()
|
|
101
|
-
}
|
|
172
|
+
if (!snap.get() && !isPanning && e instanceof WheelEvent) enableSnap()
|
|
102
173
|
})
|
|
103
174
|
|
|
104
175
|
const scrollStart = useConstant(() => ({ x: motionValue(0), y: motionValue(0) }))
|
|
@@ -127,7 +198,8 @@ export function useScroller<TagName extends keyof ReactHTML = 'div'>(
|
|
|
127
198
|
if (!isHTMLMousePointerEvent(event)) return
|
|
128
199
|
|
|
129
200
|
setPanning(false)
|
|
130
|
-
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
202
|
+
snapToVelocity(info)
|
|
131
203
|
}
|
|
132
204
|
|
|
133
205
|
const ref: React.RefCallback<HTMLElement> = (el) => {
|
|
@@ -137,14 +209,22 @@ export function useScroller<TagName extends keyof ReactHTML = 'div'>(
|
|
|
137
209
|
else if (forwardedRef) forwardedRef.current = el
|
|
138
210
|
}
|
|
139
211
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
212
|
+
const smSnapDir = scrollSnapTypeDirection(scrollSnap.scrollSnapTypeSm)
|
|
213
|
+
const mdSnapDir = scrollSnapTypeDirection(scrollSnap.scrollSnapTypeMd)
|
|
214
|
+
|
|
215
|
+
const smGridDir = grid && smSnapDir
|
|
216
|
+
const mdGridDir = grid && mdSnapDir
|
|
217
|
+
|
|
218
|
+
const className = classesPicker(classes, {
|
|
219
|
+
smSnapDir,
|
|
220
|
+
smGridDir,
|
|
221
|
+
mdSnapDir,
|
|
222
|
+
mdGridDir,
|
|
223
|
+
isSnap,
|
|
224
|
+
isPanning,
|
|
225
|
+
hideScrollbar,
|
|
226
|
+
canGrab,
|
|
227
|
+
})('root', props.className)
|
|
148
228
|
|
|
149
|
-
return { ...divProps, ref, onPanStart, onPan, onPanEnd,
|
|
229
|
+
return { ...divProps, ref, onPanStart, onPan, onPanEnd, children, ...className }
|
|
150
230
|
}
|
|
@@ -17,7 +17,7 @@ const closest = (counts: number[], target: number) =>
|
|
|
17
17
|
export const useVelocitySnapTo = (
|
|
18
18
|
ref: React.RefObject<HTMLElement> | React.MutableRefObject<HTMLElement | undefined>,
|
|
19
19
|
) => {
|
|
20
|
-
const { disableSnap, register, getScrollSnapPositions
|
|
20
|
+
const { disableSnap, enableSnap, register, getScrollSnapPositions } = useScrollerContext()
|
|
21
21
|
|
|
22
22
|
const inertiaOptions: InertiaOptions = {
|
|
23
23
|
power: 1,
|
|
@@ -28,42 +28,58 @@ export const useVelocitySnapTo = (
|
|
|
28
28
|
// restSpeed: 1,
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const animatePan = (info: PanInfo
|
|
31
|
+
const animatePan = async (info: PanInfo) => {
|
|
32
32
|
const el = ref.current
|
|
33
33
|
if (!el) throw Error(`Can't find html element`)
|
|
34
34
|
|
|
35
35
|
const { scrollLeft, scrollTop } = el
|
|
36
|
-
disableSnap()
|
|
37
36
|
|
|
38
|
-
const
|
|
39
|
-
|
|
37
|
+
const xDone = new Promise<void>((onComplete) => {
|
|
38
|
+
const targetX = clamp(info, 'x') * -1 + scrollLeft
|
|
39
|
+
const closestX = closest(getScrollSnapPositions().x, targetX)
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
if (closestX !== scrollLeft) {
|
|
42
|
+
disableSnap()
|
|
43
|
+
register(
|
|
44
|
+
inertia({
|
|
45
|
+
velocity: info.velocity.x * -1,
|
|
46
|
+
max: typeof closestX !== 'undefined' ? closestX - scrollLeft : undefined,
|
|
47
|
+
min: typeof closestX !== 'undefined' ? closestX - scrollLeft : undefined,
|
|
48
|
+
...inertiaOptions,
|
|
49
|
+
onUpdate: (v: number) => (el.scrollLeft = Math.round(v + scrollLeft)),
|
|
50
|
+
onComplete,
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
} else {
|
|
54
|
+
onComplete()
|
|
55
|
+
}
|
|
50
56
|
})
|
|
51
|
-
register(cancelX)
|
|
52
57
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
const yDone = new Promise<void>((onComplete) => {
|
|
59
|
+
const targetY = clamp(info, 'y') * -1 + scrollTop
|
|
60
|
+
const closestY = closest(getScrollSnapPositions().y, targetY)
|
|
61
|
+
|
|
62
|
+
if (closestY !== scrollTop) {
|
|
63
|
+
disableSnap()
|
|
64
|
+
register(
|
|
65
|
+
inertia({
|
|
66
|
+
velocity: info.velocity.y * -1,
|
|
67
|
+
max: typeof closestY !== 'undefined' ? closestY - scrollTop : undefined,
|
|
68
|
+
min: typeof closestY !== 'undefined' ? closestY - scrollTop : undefined,
|
|
69
|
+
...inertiaOptions,
|
|
70
|
+
onUpdate: (v: number) => (el.scrollTop = Math.round(v + scrollTop)),
|
|
71
|
+
onComplete,
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
} else {
|
|
75
|
+
onComplete()
|
|
76
|
+
}
|
|
63
77
|
})
|
|
64
|
-
register(cancelY)
|
|
65
78
|
|
|
66
|
-
|
|
79
|
+
await xDone
|
|
80
|
+
await yDone
|
|
81
|
+
enableSnap()
|
|
67
82
|
}
|
|
83
|
+
|
|
68
84
|
return animatePan
|
|
69
85
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphcommerce/framer-scroller",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "tsc -W"
|
|
@@ -16,15 +16,16 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@graphcommerce/framer-utils": "^2.103.
|
|
20
|
-
"@graphcommerce/image": "^2.105.
|
|
19
|
+
"@graphcommerce/framer-utils": "^2.103.16",
|
|
20
|
+
"@graphcommerce/image": "^2.105.5",
|
|
21
|
+
"@graphcommerce/next-ui": "^3.20.1",
|
|
21
22
|
"@material-ui/core": "^4.12.3",
|
|
22
23
|
"popmotion": "9.3.6"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@graphcommerce/browserslist-config-pwa": "^3.0.2",
|
|
26
|
-
"@graphcommerce/eslint-config-pwa": "^3.1.
|
|
27
|
-
"@graphcommerce/prettier-config-pwa": "^3.0.
|
|
27
|
+
"@graphcommerce/eslint-config-pwa": "^3.1.6",
|
|
28
|
+
"@graphcommerce/prettier-config-pwa": "^3.0.4",
|
|
28
29
|
"@graphcommerce/typescript-config-pwa": "^3.1.1",
|
|
29
30
|
"@playwright/test": "^1.16.2"
|
|
30
31
|
},
|
|
@@ -35,5 +36,5 @@
|
|
|
35
36
|
"react-dom": "^17.0.2",
|
|
36
37
|
"type-fest": "^2.5.1"
|
|
37
38
|
},
|
|
38
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "b4bdc1cd365ebdd0bad8e1ed6afd374123bb2908"
|
|
39
40
|
}
|
package/types.ts
CHANGED
|
@@ -10,7 +10,8 @@ export type ItemState = {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export type ScrollSnapProps = {
|
|
13
|
-
|
|
13
|
+
scrollSnapTypeSm: ScrollSnapType
|
|
14
|
+
scrollSnapTypeMd: ScrollSnapType
|
|
14
15
|
scrollSnapAlign: ScrollSnapAlign
|
|
15
16
|
scrollSnapStop: ScrollSnapStop
|
|
16
17
|
}
|
|
@@ -43,14 +44,12 @@ export type ScrollerContext = {
|
|
|
43
44
|
registerChildren(children: React.ReactNode): void
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export type ScrollSnapTypeSingle = 'none' | 'block' | 'inline' | 'x' | 'y' | 'both'
|
|
48
|
+
|
|
46
49
|
export type ScrollSnapType =
|
|
47
|
-
|
|
|
48
|
-
| '
|
|
49
|
-
|
|
50
|
-
| 'x'
|
|
51
|
-
| 'y'
|
|
52
|
-
| 'both'
|
|
53
|
-
| `${'block' | 'inline' | 'x' | 'y' | 'both'} ${'mandatory' | 'proximity'}`
|
|
50
|
+
| ScrollSnapTypeSingle
|
|
51
|
+
| `${ScrollSnapTypeSingle} ${'mandatory' | 'proximity'}`
|
|
52
|
+
|
|
54
53
|
export type ScrollSnapAlignAxis = 'none' | 'center' | 'end' | 'start'
|
|
55
54
|
|
|
56
55
|
export type ScrollSnapAlign = ScrollSnapAlignAxis | `${ScrollSnapAlignAxis} ${ScrollSnapAlignAxis}`
|