@graphcommerce/framer-scroller 1.0.4 → 1.1.0

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 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
 
@@ -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
- scrollSnapType?: ScrollSnapType
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
- [enableSnap, scrollerRef],
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
- scrollSnapType = 'inline mandatory',
71
+ scrollSnapTypeSm = 'inline mandatory',
72
+ scrollSnapTypeMd = 'inline mandatory',
86
73
  ...providerProps
87
74
  } = props
75
+
88
76
  const scrollSnap = useMemo(
89
- () => ({ scrollSnapType, scrollSnapStop, scrollSnapAlign }),
90
- [scrollSnapAlign, scrollSnapStop, scrollSnapType],
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
- const p = scrollerRef.current.scrollLeft
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 = p
112
+ scrollerRef.current.scrollLeft = l
113
+ scrollerRef.current.scrollTop = t
120
114
  }, [snap, stop])
121
115
 
122
- useObserveItems(scrollerRef, items, enableSnap)
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 = [...parent.children]
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
- // eslint-disable-next-line prefer-const
183
- let [childAlignY, childAlignX] = scrollSnap.scrollSnapAlign.split(
184
- ' ',
185
- ) as ScrollSnapAlignAxis[]
186
- if (typeof childAlignX === 'undefined') {
187
- childAlignX = childAlignY
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]
@@ -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
- if (!scrollerRef.current) return
10
+ return async (to: Point2D) => {
11
+ const ref = scrollerRef.current
12
+ if (!ref) return
12
13
 
13
- // const notCompletelyVisible = items.filter((item) => item.visibility.get() < 0.9)
14
- // todo get the target element and move one right, keep the current velocity
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
- disableSnap()
17
- register(
18
- animate({
19
- from: scrollerRef.current.scrollLeft,
20
- to: to.x,
21
- velocity: scroll.x.getVelocity(),
22
- onUpdate: (v) => {
23
- if (!scrollerRef.current) return
24
- scrollerRef.current.scrollLeft = v
25
- },
26
- onComplete: enableSnap,
27
- bounce: 50,
28
- }),
29
- )
30
- register(
31
- animate({
32
- from: scrollerRef.current.scrollTop,
33
- to: to.y,
34
- velocity: scroll.y.getVelocity(),
35
- onUpdate: (v) => {
36
- if (!scrollerRef.current) return
37
- scrollerRef.current.scrollTop = v
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
  }
@@ -1,6 +1,6 @@
1
1
  import { useConstant, useElementScroll, useMotionValueValue } from '@graphcommerce/framer-utils'
2
- import { makeStyles } from '@material-ui/core'
3
- import clsx from 'clsx'
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: ({ scrollSnapAlign, scrollSnapStop }: ScrollSnapProps) => ({
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: 'column',
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
- userSelect: 'none',
33
- userDrag: 'none',
85
+ }),
86
+ rootSmGridDirInline: ({ scrollSnapAlign, scrollSnapStop }) => ({
87
+ display: 'grid',
88
+ gridAutoFlow: 'column',
89
+ gridAutoRows: `40%`,
90
+ '& > *': {
91
+ scrollSnapAlign,
92
+ scrollSnapStop,
34
93
  },
35
94
  }),
36
- canGrab: {
95
+ rootCanGrab: {
37
96
  cursor: 'grab',
38
97
  },
39
- snap: ({ scrollSnapType }: ScrollSnapProps) => ({
40
- scrollSnapType,
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
- panning: {
106
+ rootIsPanning: {
43
107
  cursor: 'grabbing !important',
44
108
  '& > *': {
45
109
  pointerEvents: 'none',
46
110
  },
47
111
  },
48
- hideScrollbar: {
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'> = HTMLMotionProps<TagName> & {
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 animatePan = useVelocitySnapTo(scrollerRef)
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 *should* do is wait for the scroll position to be set exactly like a snappoint and
96
- * then enable it. However, to lazy to do that then we need to know the position of all elements
97
- * at all time, we now are lazy :)
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
- animatePan(info, enableSnap)
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 className = clsx(
141
- classes.root,
142
- isSnap && classes.snap,
143
- isPanning && classes.panning,
144
- hideScrollbar && classes.hideScrollbar,
145
- canGrab && classes.canGrab,
146
- props.className,
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, className, children }
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, scrollSnap } = useScrollerContext()
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, onComplete?: () => void) => {
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 targetX = clamp(info, 'x') * -1 + scrollLeft
39
- const closestX = closest(getScrollSnapPositions().x, targetX)
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
- const cancelX = inertia({
42
- velocity: info.velocity.x * -1,
43
- max: typeof closestX !== 'undefined' ? closestX - scrollLeft : undefined,
44
- min: typeof closestX !== 'undefined' ? closestX - scrollLeft : undefined,
45
- ...inertiaOptions,
46
- onUpdate: (v: number) => {
47
- el.scrollLeft = Math.round(v + scrollLeft)
48
- },
49
- onComplete,
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 targetY = clamp(info, 'y') * -1 + scrollTop
54
- const closestY = closest(getScrollSnapPositions().y, targetY)
55
- const cancelY = inertia({
56
- velocity: info.velocity.y * -1,
57
- max: typeof closestY !== 'undefined' ? closestY - scrollTop : undefined,
58
- min: typeof closestY !== 'undefined' ? closestY - scrollTop : undefined,
59
- ...inertiaOptions,
60
- onUpdate: (v: number) => {
61
- el.scrollTop = v + scrollTop
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
- return () => {}
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.0.4",
3
+ "version": "1.1.0",
4
4
  "sideEffects": false,
5
5
  "scripts": {
6
6
  "dev": "tsc -W"
@@ -16,14 +16,15 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@graphcommerce/framer-utils": "^2.103.15",
20
- "@graphcommerce/image": "^2.105.4",
19
+ "@graphcommerce/framer-utils": "^2.103.16",
20
+ "@graphcommerce/image": "^2.105.5",
21
+ "@graphcommerce/next-ui": "^3.20.0",
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.5",
27
+ "@graphcommerce/eslint-config-pwa": "^3.1.6",
27
28
  "@graphcommerce/prettier-config-pwa": "^3.0.4",
28
29
  "@graphcommerce/typescript-config-pwa": "^3.1.1",
29
30
  "@playwright/test": "^1.16.2"
@@ -35,5 +36,5 @@
35
36
  "react-dom": "^17.0.2",
36
37
  "type-fest": "^2.5.1"
37
38
  },
38
- "gitHead": "6a39908a131938d9c3365cc937b92c1f1f8b33c6"
39
+ "gitHead": "8f156415c7f5a963e363f0d6d18fe5d6bbd5dba2"
39
40
  }
package/types.ts CHANGED
@@ -10,7 +10,8 @@ export type ItemState = {
10
10
  }
11
11
 
12
12
  export type ScrollSnapProps = {
13
- scrollSnapType: ScrollSnapType
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
- | 'none'
48
- | 'block'
49
- | 'inline'
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}`