@graphcommerce/framer-scroller 1.0.3 → 1.1.2

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,42 @@
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.2](https://github.com/ho-nl/m2-pwa/compare/@graphcommerce/framer-scroller@1.1.1...@graphcommerce/framer-scroller@1.1.2) (2021-12-03)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * height of scroller isn't correct ([8d847ad](https://github.com/ho-nl/m2-pwa/commit/8d847ad210cfe00bf88386912d97233402e16b81))
12
+
13
+
14
+
15
+
16
+
17
+ # [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)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **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))
23
+ * **framer-scroller:** recursively find snapPoints for deeply nested children ([2203f00](https://github.com/ho-nl/m2-pwa/commit/2203f00a97bb4e4b381acad1d86b177105874d1f))
24
+ * **framer-scroller:** set the scrollTop when switching snap ([8bd35a2](https://github.com/ho-nl/m2-pwa/commit/8bd35a2ebf5d10b44d04048c6360c7ac770221cf))
25
+ * make sure the overlay becomes visible, even if the overlay is scrolled ([1738c98](https://github.com/ho-nl/m2-pwa/commit/1738c982ea84ec2b93daa824c4b8c86ab2a3f5ed))
26
+ * make sure the overlays are rendered correctly on mobile ([48f7050](https://github.com/ho-nl/m2-pwa/commit/48f705060e99b997f5b1db03ccc49f1051a1ed8f))
27
+ * make the headerHeight properly configurable ([c39c942](https://github.com/ho-nl/m2-pwa/commit/c39c942a62a9bb9687ea553be28e37fb49a6b065))
28
+
29
+
30
+ ### Features
31
+
32
+ * **framer-scroller-sheet:** created package replacing the framer-sheet package ([f9f2e91](https://github.com/ho-nl/m2-pwa/commit/f9f2e9101191f5cb5c4514ceb9534ddeb2476763))
33
+ * **framer-scroller:** find the direction of the scroller and set proper values ([631e24c](https://github.com/ho-nl/m2-pwa/commit/631e24c5c7ff67b49f83390789e8dadcb07eca04))
34
+ * **framer-scroller:** get the scrollSnapAlign from the element instead of the body ([ec8c24e](https://github.com/ho-nl/m2-pwa/commit/ec8c24e6d457a3ed1c055b27d7b94be5ed4b6f2c))
35
+ * **framer-scroller:** provide promise with scrollTo ([cbe59d9](https://github.com/ho-nl/m2-pwa/commit/cbe59d9e753cc2ab76b2de048139e0319f08d035))
36
+ * **framer-scroller:** split the grid functionality from the scroller ([81307ea](https://github.com/ho-nl/m2-pwa/commit/81307ea2652bf31a1f94e8db72af4ee161bdca2e))
37
+
38
+
39
+
40
+
41
+
6
42
  ## [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
43
 
8
44
 
@@ -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,123 @@ 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
+ gridTemplateRows: 'auto',
91
+ '& > *': {
92
+ scrollSnapAlign,
93
+ scrollSnapStop,
34
94
  },
35
95
  }),
36
- canGrab: {
96
+ rootCanGrab: {
37
97
  cursor: 'grab',
38
98
  },
39
- snap: ({ scrollSnapType }: ScrollSnapProps) => ({
40
- scrollSnapType,
99
+ rootIsSnap: ({ scrollSnapTypeSm, scrollSnapTypeMd }: ScrollSnapProps) => ({
100
+ [theme.breakpoints.down('sm')]: {
101
+ scrollSnapType: scrollSnapTypeSm,
102
+ },
103
+ [theme.breakpoints.up('md')]: {
104
+ scrollSnapType: scrollSnapTypeMd,
105
+ },
41
106
  }),
42
- panning: {
107
+ rootIsPanning: {
43
108
  cursor: 'grabbing !important',
44
109
  '& > *': {
45
110
  pointerEvents: 'none',
46
111
  },
47
112
  },
48
- hideScrollbar: {
113
+ rootHideScrollbar: {
49
114
  scrollbarWidth: 'none',
50
115
  '&::-webkit-scrollbar': {
51
116
  display: 'none',
52
117
  },
53
118
  },
54
- },
119
+ }),
55
120
  { name: 'Scroller' },
56
121
  )
57
122
 
58
- export type ScrollableProps<TagName extends keyof ReactHTML = 'div'> = HTMLMotionProps<TagName> & {
59
- hideScrollbar?: boolean
123
+ export type ScrollableProps<TagName extends keyof ReactHTML = 'div'> = UseStyles<typeof useStyles> &
124
+ HTMLMotionProps<TagName> & { hideScrollbar?: boolean; grid?: boolean }
125
+
126
+ export function scrollSnapTypeDirection(scrollSnapType: ScrollSnapType) {
127
+ let smSnapDir = scrollSnapType.split(' ')[0]
128
+ smSnapDir = smSnapDir.replace('y', 'block')
129
+ smSnapDir = smSnapDir.replace('x', 'inline') as 'block' | 'inline' | 'both' | 'inline'
130
+ return smSnapDir
60
131
  }
61
132
 
62
133
  /** Make any HTML */
@@ -64,10 +135,11 @@ export function useScroller<TagName extends keyof ReactHTML = 'div'>(
64
135
  props: ScrollableProps<TagName>,
65
136
  forwardedRef: React.ForwardedRef<any>,
66
137
  ) {
138
+ const { hideScrollbar = false, children, grid = false, ...divProps } = props
139
+
67
140
  const { scrollSnap, scrollerRef, enableSnap, disableSnap, snap, registerChildren } =
68
141
  useScrollerContext()
69
142
 
70
- const { hideScrollbar, children, ...divProps } = props
71
143
  registerChildren(children)
72
144
 
73
145
  const scroll = useElementScroll(scrollerRef)
@@ -77,28 +149,28 @@ export function useScroller<TagName extends keyof ReactHTML = 'div'>(
77
149
  [scroll.xMax, scroll.yMax] as MotionValue<string | number>[],
78
150
  ([xMax, yMax]: number[]) => xMax || yMax,
79
151
  ),
80
- (v) => v,
152
+ (v) => !!v,
81
153
  )
82
154
 
83
155
  const isSnap = useMotionValueValue(snap, (v) => v)
84
156
 
85
157
  const classes = useStyles(scrollSnap)
86
158
 
87
- const animatePan = useVelocitySnapTo(scrollerRef)
159
+ const snapToVelocity = useVelocitySnapTo(scrollerRef)
160
+
88
161
  const [isPanning, setPanning] = useState(false)
89
162
 
163
+ /** If the scroller doesn't have snap enabled and the user is not panning, enable snap */
90
164
  useDomEvent(scrollerRef as React.RefObject<EventTarget>, 'wheel', (e) => {
91
165
  /**
92
166
  * Todo: this is actually incorrect because when enabling the snap points, the area jumps to the
93
167
  * nearest point a snap.
94
168
  *
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 :)
169
+ * What we SHOULD do is wait for the scroll position to be set exactly on a snappoint and then
170
+ * enable it. However, to do that then we need to know the position of all elements at all time,
171
+ * we now are lazy :)
98
172
  */
99
- if (!snap.get() && !isPanning && e instanceof WheelEvent) {
100
- enableSnap()
101
- }
173
+ if (!snap.get() && !isPanning && e instanceof WheelEvent) enableSnap()
102
174
  })
103
175
 
104
176
  const scrollStart = useConstant(() => ({ x: motionValue(0), y: motionValue(0) }))
@@ -127,7 +199,8 @@ export function useScroller<TagName extends keyof ReactHTML = 'div'>(
127
199
  if (!isHTMLMousePointerEvent(event)) return
128
200
 
129
201
  setPanning(false)
130
- animatePan(info, enableSnap)
202
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
203
+ snapToVelocity(info)
131
204
  }
132
205
 
133
206
  const ref: React.RefCallback<HTMLElement> = (el) => {
@@ -137,14 +210,22 @@ export function useScroller<TagName extends keyof ReactHTML = 'div'>(
137
210
  else if (forwardedRef) forwardedRef.current = el
138
211
  }
139
212
 
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
- )
213
+ const smSnapDir = scrollSnapTypeDirection(scrollSnap.scrollSnapTypeSm)
214
+ const mdSnapDir = scrollSnapTypeDirection(scrollSnap.scrollSnapTypeMd)
215
+
216
+ const smGridDir = grid && smSnapDir
217
+ const mdGridDir = grid && mdSnapDir
218
+
219
+ const className = classesPicker(classes, {
220
+ smSnapDir,
221
+ smGridDir,
222
+ mdSnapDir,
223
+ mdGridDir,
224
+ isSnap,
225
+ isPanning,
226
+ hideScrollbar,
227
+ canGrab,
228
+ })('root', props.className)
148
229
 
149
- return { ...divProps, ref, onPanStart, onPan, onPanEnd, className, children }
230
+ return { ...divProps, ref, onPanStart, onPan, onPanEnd, children, ...className }
150
231
  }
@@ -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.3",
3
+ "version": "1.1.2",
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.14",
20
- "@graphcommerce/image": "^2.105.3",
19
+ "@graphcommerce/framer-utils": "^2.103.16",
20
+ "@graphcommerce/image": "^2.105.5",
21
+ "@graphcommerce/next-ui": "^3.20.2",
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.4",
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": "1099718b92d4d472f32aeb6379f6167c6b550ccf"
39
+ "gitHead": "688a7042901bf36f8ab8bbf8e1886570a5350b9e"
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}`