@dbosoft/nextjs-uicore 1.1.0 → 1.2.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 +47 -36
- package/package.json +7 -6
- package/src/subnav/index.tsx +5 -5
- package/src/tabs/Tabs.tsx +50 -0
- package/src/tabs/TabsClient.tsx +75 -0
- package/src/tabs/index.ts +3 -0
- package/src/tabs/server.ts +2 -0
- package/src/themeselector/index.tsx +12 -12
- package/src/tabs/hooks/use-scroll-left.ts +0 -27
- package/src/tabs/hooks/use-window-size.js +0 -33
- package/src/tabs/icons/chevron-right.svg +0 -1
- package/src/tabs/icons/tooltip.svg +0 -1
- package/src/tabs/index.tsx +0 -102
- package/src/tabs/partials/tab-trigger/index.tsx +0 -66
- package/src/tabs/partials/tab-trigger/style.module.scss +0 -69
- package/src/tabs/partials/tab-triggers/index.tsx +0 -241
- package/src/tabs/partials/tab-triggers/style.module.scss +0 -193
- package/src/tabs/partials/tooltip/index.tsx +0 -112
- package/src/tabs/partials/tooltip/style.module.scss +0 -38
- package/src/tabs/provider.js +0 -18
- package/src/tabs/style.module.css +0 -4
- package/src/tabs/utils/smooth-scroll.js +0 -88
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import React, { MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
-
import clsx from 'clsx'
|
|
3
|
-
import InlineSvg from '@dbosoft/react-uicore/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={clsx(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={clsx(s.scrollContainer, {
|
|
118
|
-
[s.centered]: centered,
|
|
119
|
-
[s.hasOverflow]: hasOverflow,
|
|
120
|
-
})}
|
|
121
|
-
ref={scrollRef}
|
|
122
|
-
>
|
|
123
|
-
<div
|
|
124
|
-
className={clsx(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={clsx(s.prevArrowScrim, {
|
|
180
|
-
[s.hasOverflow]: hasOverflow,
|
|
181
|
-
[s.hidden]: hiddenArrows.prev,
|
|
182
|
-
})}
|
|
183
|
-
/>
|
|
184
|
-
<div
|
|
185
|
-
className={clsx(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={clsx(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={clsx(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={clsx(s.bottomBorder, s.forDefault, {
|
|
226
|
-
[s.hasOverflow]: hasOverflow,
|
|
227
|
-
[s.fullWidthBorder]: fullWidthBorder,
|
|
228
|
-
})}
|
|
229
|
-
></div>
|
|
230
|
-
</div>
|
|
231
|
-
<div
|
|
232
|
-
className={clsx(s.bottomBorder, s.forOverflow, {
|
|
233
|
-
[s.hasOverflow]: hasOverflow,
|
|
234
|
-
[s.fullWidthBorder]: fullWidthBorder,
|
|
235
|
-
})}
|
|
236
|
-
></div>
|
|
237
|
-
</>
|
|
238
|
-
)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export default TabTriggers
|
|
@@ -1,193 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
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
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
}
|
package/src/tabs/provider.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
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
|