@dbosoft/nextjs-uicore 1.0.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.
Files changed (38) hide show
  1. package/.eslintrc.js +4 -0
  2. package/CHANGELOG.md +12 -0
  3. package/package.json +40 -0
  4. package/src/global.d.ts +1 -0
  5. package/src/head/global.d.ts +1 -0
  6. package/src/head/index.tsx +119 -0
  7. package/src/subnav/helpers/useStuckRef.ts +50 -0
  8. package/src/subnav/index.tsx +133 -0
  9. package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.test.js +25 -0
  10. package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.ts +17 -0
  11. package/src/subnav/partials/CtaLinks/github-stars-link/index.tsx +84 -0
  12. package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.test.js +25 -0
  13. package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.ts +25 -0
  14. package/src/subnav/partials/CtaLinks/icons/github.svg +4 -0
  15. package/src/subnav/partials/CtaLinks/index.tsx +46 -0
  16. package/src/subnav/partials/MenuItemsDefault/index.tsx +63 -0
  17. package/src/subnav/partials/MenuItemsDefault/style.module.scss +71 -0
  18. package/src/subnav/partials/MenuItemsOverflow/index.tsx +50 -0
  19. package/src/subnav/partials/MenuItemsOverflow/style.module.scss +51 -0
  20. package/src/subnav/partials/TitleLink/index.tsx +25 -0
  21. package/src/subnav/partials/nav-item-text/index.tsx +29 -0
  22. package/src/subnav/partials/nav-item-text/style.module.scss +19 -0
  23. package/src/subnav/style.module.scss +13 -0
  24. package/src/tabs/hooks/use-scroll-left.ts +27 -0
  25. package/src/tabs/hooks/use-window-size.js +33 -0
  26. package/src/tabs/icons/chevron-right.svg +1 -0
  27. package/src/tabs/icons/tooltip.svg +1 -0
  28. package/src/tabs/index.tsx +102 -0
  29. package/src/tabs/partials/tab-trigger/index.tsx +66 -0
  30. package/src/tabs/partials/tab-trigger/style.module.scss +69 -0
  31. package/src/tabs/partials/tab-triggers/index.tsx +241 -0
  32. package/src/tabs/partials/tab-triggers/style.module.scss +193 -0
  33. package/src/tabs/partials/tooltip/index.tsx +112 -0
  34. package/src/tabs/partials/tooltip/style.module.scss +38 -0
  35. package/src/tabs/provider.js +18 -0
  36. package/src/tabs/style.module.css +4 -0
  37. package/src/tabs/utils/smooth-scroll.js +88 -0
  38. package/tsconfig.json +8 -0
@@ -0,0 +1,241 @@
1
+ import React, { MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react'
2
+ import classNames from 'classnames'
3
+ import InlineSvg from '@dbosoft/react-inline-svg'
4
+ import TabTrigger, { TabTriggerType } from '../tab-trigger'
5
+ import SvgChevronRight from '../../icons/chevron-right.svg'
6
+ import smoothScroll from '../../utils/smooth-scroll.js'
7
+ import useWindowSize from '../../hooks/use-window-size'
8
+ import useScrollLeft from '../../hooks/use-scroll-left'
9
+ import s from './style.module.scss'
10
+
11
+ interface TabTriggersProps {
12
+ tabs: TabTriggerType[]
13
+ activeTabIdx: number
14
+ setActiveTab: (tabIndex: number, tabGroup?: string) => void
15
+ centered: boolean
16
+ fullWidthBorder: boolean
17
+ }
18
+
19
+ function TabTriggers({
20
+ tabs,
21
+ activeTabIdx,
22
+ setActiveTab,
23
+ centered,
24
+ fullWidthBorder,
25
+ }: TabTriggersProps): React.ReactElement {
26
+ const overflowBaseRef = useRef(null) as $TSFixMe
27
+ const overflowContentRef = useRef(null) as $TSFixMe
28
+ const windowSize = useWindowSize()
29
+ const [scrollRef, scrollLeft] = useScrollLeft()
30
+ const [hiddenArrows, setHiddenArrows] = useState({
31
+ prev: true,
32
+ next: true,
33
+ })
34
+ const [hasOverflow, setHasOverflow] = useState(false)
35
+
36
+ /**
37
+ * update hasOverflow when window is resized
38
+ */
39
+ useEffect(() => {
40
+ // If content width exceeds available space,
41
+ // set to overflow-friendly styling
42
+ const contentWidth = overflowContentRef.current.offsetWidth
43
+ const availableSpace = overflowBaseRef.current.offsetWidth
44
+ setHasOverflow(contentWidth > availableSpace)
45
+ }, [scrollRef, windowSize])
46
+
47
+ /**
48
+ * update visibility of next & prev arrows.
49
+ *
50
+ * depends on both scroll position, as
51
+ * fully scrolling to one end should hide
52
+ * the arrow at that end of the container,
53
+ *
54
+ * and depends on window size, as
55
+ * window size changes can affect both
56
+ * overflow and scroll position
57
+ */
58
+ useEffect(() => {
59
+ // Determine which arrows to show
60
+ const { scrollLeft, scrollWidth, offsetWidth } = scrollRef.current
61
+ const maxScrollLeft = scrollWidth - offsetWidth
62
+ const hidePrev = scrollLeft === 0
63
+ const hideNext = scrollLeft >= maxScrollLeft
64
+ setHiddenArrows({ prev: hidePrev, next: hideNext })
65
+ }, [scrollLeft, scrollRef, windowSize])
66
+
67
+ /**
68
+ * smooth scroll to the active tab.
69
+ * this is done both when the activeTabIdx updates,
70
+ * and when the next or previous arrow is clicked but
71
+ * does not cause an activeTabIdx update
72
+ */
73
+ const updateScrollOffset = useCallback(
74
+ (targetTabIdx) => {
75
+ const scrollElem = scrollRef.current
76
+ // Determine where to scroll to
77
+ let newScrollLeft
78
+ if (targetTabIdx === 0) {
79
+ // If first tab, scroll to start of container
80
+ newScrollLeft = -1
81
+ } else {
82
+ // Otherwise, calculate the midpoint of the active tab trigger
83
+ const targetSelector = `[data-tabindex='${targetTabIdx}']`
84
+ const targetElem = scrollElem.querySelector(targetSelector)
85
+ const targetMidpoint =
86
+ targetElem.offsetLeft + targetElem.offsetWidth / 2
87
+ newScrollLeft = targetMidpoint - scrollElem.offsetWidth / 2
88
+ }
89
+ // Update the scroll position
90
+ const windowElem = scrollElem.closest('html').parentNode.defaultView
91
+ smoothScroll(windowElem, scrollElem, { x: newScrollLeft })
92
+ },
93
+ [scrollRef]
94
+ )
95
+
96
+ /**
97
+ * automatically smoothly scroll to center
98
+ * the active tab in the scroll-able area
99
+ */
100
+ useEffect(() => {
101
+ if (!hasOverflow) return
102
+ updateScrollOffset(activeTabIdx)
103
+ }, [hasOverflow, activeTabIdx, updateScrollOffset, scrollRef])
104
+
105
+ return (
106
+ <div
107
+ className={classNames(s.root, { [s.fullWidthBorder]: fullWidthBorder })}
108
+ >
109
+ <div className="g-grid-container">
110
+ {/* Note: the overflowBaseRef element has zero height, but is still "visible".
111
+ It is used to determine when tabs are overflowing, and updates hasOverflow */}
112
+ <div ref={overflowBaseRef}></div>
113
+ </div>
114
+ <div className={s.borderAdjuster}>
115
+ <NextPrevScrims hasOverflow={hasOverflow} hiddenArrows={hiddenArrows} />
116
+ <div
117
+ className={classNames(s.scrollContainer, {
118
+ [s.centered]: centered,
119
+ [s.hasOverflow]: hasOverflow,
120
+ })}
121
+ ref={scrollRef}
122
+ >
123
+ <div
124
+ className={classNames(s.tabsWidthContainer, {
125
+ [s.centered]: centered,
126
+ [s.hasOverflow]: hasOverflow,
127
+ })}
128
+ ref={overflowContentRef}
129
+ >
130
+ {tabs.map((tab, stableIdx) => (
131
+ <TabTrigger
132
+ // This array is stable, so we can use index as key
133
+ // eslint-disable-next-line react/no-array-index-key
134
+ key={stableIdx}
135
+ hasOverflow={hasOverflow}
136
+ activeTabIdx={activeTabIdx}
137
+ setActiveTab={(targetIdx, groupId) => {
138
+ setActiveTab(targetIdx, groupId)
139
+ updateScrollOffset(targetIdx)
140
+ }}
141
+ tab={tab}
142
+ />
143
+ ))}
144
+ </div>
145
+ </div>
146
+ <NextPrevArrows
147
+ hasOverflow={hasOverflow}
148
+ hiddenArrows={hiddenArrows}
149
+ onPrev={() => {
150
+ const target = activeTabIdx - 1
151
+ if (target >= 0) {
152
+ setActiveTab(target, tabs[target].group)
153
+ } else {
154
+ updateScrollOffset(activeTabIdx)
155
+ }
156
+ }}
157
+ onNext={() => {
158
+ const target = activeTabIdx + 1
159
+ if (target < tabs.length) {
160
+ setActiveTab(target, tabs[target].group)
161
+ } else {
162
+ updateScrollOffset(activeTabIdx)
163
+ }
164
+ }}
165
+ />
166
+ </div>
167
+ <BottomBorder
168
+ hasOverflow={hasOverflow}
169
+ fullWidthBorder={fullWidthBorder}
170
+ />
171
+ </div>
172
+ )
173
+ }
174
+
175
+ function NextPrevScrims({ hasOverflow, hiddenArrows }: { hasOverflow: $TSFixMe, hiddenArrows: $TSFixMe }) {
176
+ return (
177
+ <div className={s.scrimContainer}>
178
+ <div
179
+ className={classNames(s.prevArrowScrim, {
180
+ [s.hasOverflow]: hasOverflow,
181
+ [s.hidden]: hiddenArrows.prev,
182
+ })}
183
+ />
184
+ <div
185
+ className={classNames(s.nextArrowScrim, {
186
+ [s.hasOverflow]: hasOverflow,
187
+ [s.hidden]: hiddenArrows.next,
188
+ })}
189
+ />
190
+ </div>
191
+ )
192
+ }
193
+
194
+ function NextPrevArrows({ hasOverflow, hiddenArrows, onPrev, onNext }
195
+ : { hasOverflow: $TSFixMe, hiddenArrows: $TSFixMe, onPrev: MouseEventHandler, onNext: MouseEventHandler }) {
196
+ return (
197
+ <>
198
+ <div
199
+ className={classNames(s.prevArrow, {
200
+ [s.hasOverflow]: hasOverflow,
201
+ [s.hidden]: hiddenArrows.prev,
202
+ })}
203
+ onClick={onPrev}
204
+ >
205
+ <InlineSvg src={SvgChevronRight} />
206
+ </div>
207
+ <div
208
+ className={classNames(s.nextArrow, {
209
+ [s.hasOverflow]: hasOverflow,
210
+ [s.hidden]: hiddenArrows.next,
211
+ })}
212
+ onClick={onNext}
213
+ >
214
+ <InlineSvg src={SvgChevronRight} />
215
+ </div>
216
+ </>
217
+ )
218
+ }
219
+
220
+ function BottomBorder({ hasOverflow, fullWidthBorder }: { hasOverflow: $TSFixMe, fullWidthBorder: $TSFixMe }) {
221
+ return (
222
+ <>
223
+ <div className="g-grid-container">
224
+ <div
225
+ className={classNames(s.bottomBorder, s.forDefault, {
226
+ [s.hasOverflow]: hasOverflow,
227
+ [s.fullWidthBorder]: fullWidthBorder,
228
+ })}
229
+ ></div>
230
+ </div>
231
+ <div
232
+ className={classNames(s.bottomBorder, s.forOverflow, {
233
+ [s.hasOverflow]: hasOverflow,
234
+ [s.fullWidthBorder]: fullWidthBorder,
235
+ })}
236
+ ></div>
237
+ </>
238
+ )
239
+ }
240
+
241
+ export default TabTriggers
@@ -0,0 +1,193 @@
1
+ .root {
2
+ --height: 64px;
3
+
4
+ background: var(--white);
5
+ position: relative;
6
+
7
+ &.fullWidthBorder {
8
+ border-bottom: 1px solid var(--gray-5);
9
+ }
10
+ }
11
+
12
+ .bottomBorder {
13
+ width: 100%;
14
+ display: block;
15
+ position: relative;
16
+ z-index: 0;
17
+ border-bottom: 1px solid var(--gray-5);
18
+
19
+ &.forDefault {
20
+ display: block;
21
+ &.hasOverflow {
22
+ display: none;
23
+ }
24
+ }
25
+
26
+ &.forOverflow {
27
+ display: none;
28
+ &.hasOverflow {
29
+ display: block;
30
+ }
31
+ }
32
+
33
+ /* If we're using a full-width border,
34
+ hide this elements border no matter what */
35
+ &.fullWidthBorder {
36
+ display: none;
37
+
38
+ &.hasOverflow {
39
+ display: none;
40
+ }
41
+ }
42
+ }
43
+
44
+ .borderAdjuster {
45
+ position: relative;
46
+ margin-bottom: -1px;
47
+ z-index: 1;
48
+ }
49
+
50
+ .scrollContainer {
51
+ composes: g-grid-container from global;
52
+ overflow: scroll;
53
+ white-space: nowrap;
54
+ -webkit-overflow-scrolling: touch;
55
+ scrollbar-width: none;
56
+
57
+ &::-webkit-scrollbar {
58
+ display: none;
59
+ }
60
+ }
61
+
62
+ .tabsWidthContainer {
63
+ display: flex;
64
+ min-width: max-content;
65
+
66
+ &.centered {
67
+ justify-content: center;
68
+ }
69
+ }
70
+
71
+ /*
72
+ Arrows are positioned based on
73
+ on the tab container, so they always
74
+ appear by the container edge if
75
+ there is overflow.
76
+ */
77
+ .arrow {
78
+ --icon-color: var(--gray-3);
79
+
80
+ align-items: center;
81
+ bottom: 3px;
82
+ display: none;
83
+ justify-content: center;
84
+ opacity: 1;
85
+ position: absolute;
86
+ top: 0;
87
+ transition: opacity 0.6s;
88
+ user-select: none;
89
+ width: 56px;
90
+ z-index: 1;
91
+
92
+ &.hasOverflow {
93
+ display: flex;
94
+ }
95
+
96
+ &.hidden {
97
+ opacity: 0;
98
+ }
99
+
100
+ & svg {
101
+ display: block;
102
+ width: 20px;
103
+ height: 20px;
104
+ & [fill] {
105
+ fill: var(--icon-color);
106
+ }
107
+ & [stroke] {
108
+ stroke: var(--icon-color);
109
+ }
110
+ }
111
+
112
+ &:hover {
113
+ --icon-color: var(--gray-1);
114
+
115
+ cursor: pointer;
116
+ }
117
+ }
118
+
119
+ .prevArrow {
120
+ composes: arrow;
121
+ left: 0;
122
+
123
+ & svg {
124
+ transform: rotate(180deg);
125
+ margin-right: 20px;
126
+ }
127
+ }
128
+
129
+ .nextArrow {
130
+ composes: arrow;
131
+ right: 0;
132
+
133
+ & svg {
134
+ margin-left: 20px;
135
+ }
136
+ }
137
+
138
+ /*
139
+ Scrims are positioned based on
140
+ g-grid-container, to align with
141
+ the edge of the scrolling container.
142
+ */
143
+ .scrimContainer {
144
+ composes: g-grid-container from global;
145
+ position: absolute;
146
+ top: 0;
147
+ left: 0;
148
+ bottom: 3px;
149
+ right: 0;
150
+ }
151
+
152
+ .arrowScrim {
153
+ position: absolute;
154
+ top: 0;
155
+ pointer-events: none;
156
+ transition: opacity 0.6s;
157
+ bottom: 0;
158
+ width: 56px;
159
+ display: none;
160
+ opacity: 1;
161
+ user-select: none;
162
+ z-index: 1;
163
+
164
+ &.hasOverflow {
165
+ display: flex;
166
+ }
167
+
168
+ &.hidden {
169
+ opacity: 0;
170
+ }
171
+ }
172
+
173
+ .prevArrowScrim {
174
+ composes: arrowScrim;
175
+ left: -1px;
176
+ background: linear-gradient(
177
+ 90deg,
178
+ rgba(255, 255, 255, 1) 30%,
179
+ rgba(255, 255, 255, 0.85) 60%,
180
+ rgba(255, 255, 255, 0) 100%
181
+ );
182
+ }
183
+
184
+ .nextArrowScrim {
185
+ composes: arrowScrim;
186
+ right: -1px;
187
+ background: linear-gradient(
188
+ -90deg,
189
+ rgba(255, 255, 255, 1) 30%,
190
+ rgba(255, 255, 255, 0.85) 60%,
191
+ rgba(255, 255, 255, 0) 100%
192
+ );
193
+ }
@@ -0,0 +1,112 @@
1
+ import React from 'react'
2
+ import Portal from '@reach/portal'
3
+ import { useTooltip, TooltipPopup } from '@reach/tooltip'
4
+ import s from './style.module.scss'
5
+
6
+ interface TooltipProps {
7
+ /** Element that, when hovered, will display the tooltip. */
8
+ children: React.ReactElement
9
+ /** Plain text for the tooltip to render */
10
+ label: string
11
+ /** What the screen reader announces */
12
+ 'aria-label'?: string
13
+ /** Minimum spacing from viewport edge */
14
+ collisionBuffer?: number
15
+ }
16
+
17
+ function Tooltip({
18
+ children,
19
+ label,
20
+ collisionBuffer = 8,
21
+ 'aria-label': ariaLabel,
22
+ }: TooltipProps): React.ReactElement {
23
+ const [trigger, tooltip] = useTooltip()
24
+ const { isVisible, triggerRect } = tooltip
25
+
26
+ return (
27
+ <React.Fragment>
28
+ {React.cloneElement(children, trigger)}
29
+ {isVisible && (
30
+ <Portal>
31
+ <Arrow triggerRect={triggerRect!} collisionBuffer={collisionBuffer} />
32
+ </Portal>
33
+ )}
34
+ <TooltipPopup
35
+ {...tooltip}
36
+ className={s.box}
37
+ label={label}
38
+ aria-label={ariaLabel}
39
+ position={(triggerRect, tooltipRect) =>
40
+ centeringFunction(triggerRect as DOMRect, tooltipRect as DOMRect, collisionBuffer)
41
+ }
42
+ />
43
+ </React.Fragment>
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Given the bounding rectangle for both
49
+ * the tooltip trigger and tooltip popup,
50
+ * render the tooltip centered and below
51
+ * the trigger.
52
+ *
53
+ * Allow viewport collisions to override
54
+ * the centered position where needed,
55
+ * using the collisionBuffer argument
56
+ * to inset the collision area so the tooltip
57
+ * doesn't appear at the very edge of the
58
+ * viewport.
59
+ */
60
+ function centeringFunction(triggerRect: DOMRect, tooltipRect: DOMRect, collisionBuffer:number) {
61
+ const triggerCenter = triggerRect.left + triggerRect.width / 2
62
+ const left = triggerCenter - tooltipRect.width / 2
63
+ const maxLeft = window.innerWidth - tooltipRect.width - collisionBuffer
64
+ return {
65
+ left: Math.min(Math.max(collisionBuffer, left), maxLeft) + window.scrollX,
66
+ top: triggerRect.bottom + collisionBuffer + window.scrollY,
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Given the bounding rectangle for
72
+ * the tooltip trigger, render a small
73
+ * triangular arrow.
74
+ *
75
+ * This arrow is centered relative to
76
+ * the trigger, but accounts for possible
77
+ * viewport collisions, as we would prefer
78
+ * to have the arrow connected to the popup
79
+ * (which is bound by the viewport) rather
80
+ * than have it perfectly centered but
81
+ * disconnected from the popup.
82
+ */
83
+ function Arrow({ triggerRect, collisionBuffer }: {triggerRect:DOMRect, collisionBuffer:number}) {
84
+ const arrowThickness = 10
85
+ const arrowLeft = triggerRect
86
+ ? `${Math.min(
87
+ // Centered position, covers most use cases
88
+ triggerRect.left - arrowThickness + triggerRect.width / 2,
89
+ // Ensure the arrow is not rendered even partially offscreen,
90
+ // as it will look disconnected from our tooltip body,
91
+ // which must be rendered within the viewport
92
+ window.innerWidth - arrowThickness * 2 - collisionBuffer
93
+ )}px`
94
+ : 'auto'
95
+ const arrowTop = triggerRect
96
+ ? `${triggerRect.bottom + window.scrollY}px`
97
+ : 'auto'
98
+
99
+ return (
100
+ <div
101
+ className={s.arrow}
102
+ style={
103
+ {
104
+ '--left': arrowLeft,
105
+ '--top': arrowTop,
106
+ } as React.CSSProperties
107
+ }
108
+ />
109
+ )
110
+ }
111
+
112
+ export default Tooltip
@@ -0,0 +1,38 @@
1
+ /* Additional composition is helpful here,
2
+ as .arrow and .root can't use CSS custom properties
3
+ nicely since .arrow is rendered into a Portal. */
4
+ .theme {
5
+ --background-color: var(--gray-2);
6
+ --foreground-color: var(--white);
7
+ }
8
+
9
+ .arrow {
10
+ composes: theme;
11
+ position: absolute;
12
+
13
+ /* --top and --left are set in JS, to allow
14
+ for dynamic, collision-free positioning. */
15
+ top: var(--top);
16
+ left: var(--left);
17
+ width: 0;
18
+ height: 0;
19
+ border-left: 10px solid transparent;
20
+ border-right: 10px solid transparent;
21
+ border-bottom: 10px solid var(--background-color);
22
+ }
23
+
24
+ .box {
25
+ composes: theme;
26
+ composes: g-type-body-small from global;
27
+ font-size: 0.875rem;
28
+ background: var(--background-color);
29
+ box-shadow: 2px 2px 10px hsla(0, 0%, 0%, 0.1);
30
+ color: var(--foreground-color);
31
+ padding: 0.5em 1em;
32
+ pointer-events: none;
33
+ position: absolute;
34
+ z-index: 1;
35
+ border-radius: 3px;
36
+ max-width: 75vw;
37
+ max-width: min(75vw, 20em);
38
+ }
@@ -0,0 +1,18 @@
1
+ import { createContext, useState, useContext, useMemo } from 'react'
2
+
3
+ export function useTabGroups() {
4
+ return useContext(TabContext)
5
+ }
6
+
7
+ const TabContext = createContext()
8
+
9
+ export default function TabProvider({ children }) {
10
+ const [activeTabGroup, setActiveTabGroup] = useState()
11
+ const contextValue = useMemo(() => ({ activeTabGroup, setActiveTabGroup }), [
12
+ activeTabGroup,
13
+ ])
14
+
15
+ return (
16
+ <TabContext.Provider value={contextValue}>{children}</TabContext.Provider>
17
+ )
18
+ }
@@ -0,0 +1,4 @@
1
+ .content {
2
+ composes: g-grid-container from global;
3
+ color: var(--gray-2);
4
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Animate a smooth scroll to the target element.
3
+ *
4
+ * Process:
5
+ * 1. Determine target distance change, using current and target positions passed
6
+ * Determine target duration of scroll animation, using minDuration and speedPxPerSecond
7
+ * 2. Set startTime of animation using current millis.
8
+ * 3. Request animation frame for step update
9
+ * 4. On step update, calculate elapsed time (t) - currentMillis - startTime
10
+ * Plug t, startPosn, distance, and duration into easeFunction to determine current position.
11
+ * Update scroll position of target element (X and Y)
12
+ * Request another animation frame
13
+ * Repeat step 3 & 4 until animation is complete
14
+ *
15
+ * @param {*} window Window element
16
+ * @param {*} elem target elem to scroll to (HTML Element)
17
+ * @param {object} targetPosnInput {x: number, y: number}
18
+ * @param {object} [optionsIn] {speedPxPerSecond: number, minDuration: number}
19
+ */
20
+ function smoothScroll(window, elem, targetPosnInput, optionsIn) {
21
+ const elemIsWindow = elem.toString() === '[object Window]'
22
+
23
+ // Parse options or fall back to defaults
24
+ const options = {
25
+ speedPxPerSecond: (optionsIn && optionsIn.speedPxPerSecond) || 900,
26
+ minDuration: (optionsIn && optionsIn.minDuration) || 200,
27
+ maxDuration: (optionsIn && optionsIn.maxDuration) || 600,
28
+ }
29
+ const { speedPxPerSecond, minDuration, maxDuration } = options
30
+ // Determine target distance change, using current and target positions passed
31
+ const startPosn = {
32
+ x: elemIsWindow ? elem.scrollX : elem.scrollLeft,
33
+ y: elemIsWindow ? elem.scrollY : elem.scrollTop,
34
+ }
35
+ const targetPosn = {
36
+ x: targetPosnInput.x || startPosn.x,
37
+ y: targetPosnInput.y || startPosn.y,
38
+ }
39
+ const { x, y } = targetPosn
40
+ const deltaX = x - startPosn.x
41
+ const deltaY = y - startPosn.y
42
+ // Determine target duration of scroll animation, using minDuration and speedPxPerSecond
43
+ const durationCalc =
44
+ (Math.max(Math.abs(deltaX), Math.abs(deltaY)) * 1000) / speedPxPerSecond
45
+ // Account for minDuration option
46
+ const duration = Math.min(Math.max(durationCalc, minDuration), maxDuration)
47
+ // Set startTime of animation using current millis.
48
+ const startTime = Date.now()
49
+ // Define step function
50
+ function smoothScrollStep() {
51
+ const elapsedTime = Date.now() - startTime
52
+ const atEnd = elapsedTime >= duration
53
+ const targetXPosn = atEnd
54
+ ? startPosn.x + deltaX
55
+ : easeInOutQuad(elapsedTime, startPosn.x, deltaX, duration)
56
+ const targetYPosn = atEnd
57
+ ? startPosn.y + deltaY
58
+ : easeInOutQuad(elapsedTime, startPosn.y, deltaY, duration)
59
+ if (elemIsWindow) {
60
+ elem.scroll(targetXPosn, targetYPosn)
61
+ } else {
62
+ elem.scrollLeft = targetXPosn
63
+ elem.scrollTop = targetYPosn
64
+ }
65
+ if (!atEnd) {
66
+ window.requestAnimationFrame(smoothScrollStep)
67
+ }
68
+ }
69
+ // Request animation frame for initial step
70
+ window.requestAnimationFrame(smoothScrollStep)
71
+ }
72
+
73
+ /**
74
+ * Quadratic easing function
75
+ * From https://github.com/danro/jquery-easing/blob/master/jquery.easing.js
76
+ *
77
+ * @param {number} t current time
78
+ * @param {number} b beginning value
79
+ * @param {number} c change in value
80
+ * @param {number} d duration
81
+ * @returns {number} eased value
82
+ */
83
+ const easeInOutQuad = function (t, b, c, d) {
84
+ if ((t /= d / 2) < 1) return (c / 2) * t * t + b
85
+ return (-c / 2) * (--t * (t - 2) - 1) + b
86
+ }
87
+
88
+ export default smoothScroll
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@dbosoft/typescript-config/nextjs.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist"
5
+ },
6
+ "include": ["src"],
7
+ "exclude": ["node_modules", "dist"]
8
+ }