@dbosoft/nextjs-uicore 1.0.3 → 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 +17 -0
- package/package.json +10 -6
- package/src/subnav/index.tsx +14 -28
- package/src/subnav/partials/CtaLinks/index.tsx +2 -2
- package/src/subnav/partials/MenuItemsDefault/index.tsx +1 -12
- package/src/subnav/partials/MenuItemsOverflow/index.tsx +3 -2
- package/src/subnav/partials/nav-item-text/index.tsx +2 -2
- 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 +119 -0
- 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,66 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react'
|
|
2
|
-
import TooltipIcon from '../../icons/tooltip.svg'
|
|
3
|
-
import Tooltip from '../tooltip'
|
|
4
|
-
import { useTabGroups } from '../../provider.js'
|
|
5
|
-
import s from './style.module.scss'
|
|
6
|
-
import classNames from 'classnames'
|
|
7
|
-
|
|
8
|
-
export interface TabTriggerType {
|
|
9
|
-
index: number
|
|
10
|
-
group: string
|
|
11
|
-
heading: string
|
|
12
|
-
tooltip?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface TabTriggerProps {
|
|
16
|
-
tab: TabTriggerType
|
|
17
|
-
hasOverflow: boolean
|
|
18
|
-
activeTabIdx: number
|
|
19
|
-
setActiveTab: (tabIndex: number, tabGroup?: string) => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function TabTrigger({
|
|
23
|
-
tab,
|
|
24
|
-
hasOverflow,
|
|
25
|
-
activeTabIdx,
|
|
26
|
-
setActiveTab,
|
|
27
|
-
}: TabTriggerProps): React.ReactElement {
|
|
28
|
-
const groupCtx = useTabGroups()
|
|
29
|
-
const activeGroup = groupCtx?.activeTabGroup
|
|
30
|
-
const isInActiveGroup = groupCtx && tab.group && tab.group === activeGroup
|
|
31
|
-
const isActiveIndex = tab.index === activeTabIdx
|
|
32
|
-
const isActiveTab = isInActiveGroup || isActiveIndex ? true : false
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
// if the tab is active based on group and the
|
|
36
|
-
// index doesn't match, update the active index
|
|
37
|
-
if (isInActiveGroup) !isActiveIndex && setActiveTab(tab.index)
|
|
38
|
-
}, [isInActiveGroup, isActiveIndex, setActiveTab, tab.index])
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<button
|
|
42
|
-
className={classNames(s.root, {
|
|
43
|
-
[s.isActiveTab]: isActiveTab,
|
|
44
|
-
[s.hasOverflow]: hasOverflow,
|
|
45
|
-
})}
|
|
46
|
-
data-tabindex={tab.index}
|
|
47
|
-
onMouseDown={(e) => e.preventDefault()}
|
|
48
|
-
onClick={() => setActiveTab(tab.index, tab.group)}
|
|
49
|
-
>
|
|
50
|
-
<span className={s.inner}>
|
|
51
|
-
<span className="g-type-body-strong">{tab.heading}</span>
|
|
52
|
-
{tab.tooltip && (
|
|
53
|
-
<Tooltip label={tab.tooltip} aria-label={tab.tooltip}>
|
|
54
|
-
<span
|
|
55
|
-
data-testid="tooltip-icon"
|
|
56
|
-
className={s.tooltipTrigger}
|
|
57
|
-
dangerouslySetInnerHTML={{ __html: TooltipIcon }}
|
|
58
|
-
/>
|
|
59
|
-
</Tooltip>
|
|
60
|
-
)}
|
|
61
|
-
</span>
|
|
62
|
-
</button>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export default TabTrigger
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
.root {
|
|
2
|
-
--inner-text-color: var(--gray-3);
|
|
3
|
-
--inner-border-color: transparent;
|
|
4
|
-
--inner-decoration: none;
|
|
5
|
-
|
|
6
|
-
display: flex;
|
|
7
|
-
align-items: center;
|
|
8
|
-
background: none;
|
|
9
|
-
border: none;
|
|
10
|
-
outline: none;
|
|
11
|
-
padding: 0 16px;
|
|
12
|
-
|
|
13
|
-
@media (--medium-up) {
|
|
14
|
-
padding: 0 24px;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
&:first-of-type {
|
|
18
|
-
padding-left: 0;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
&:last-of-type {
|
|
22
|
-
padding-right: 0;
|
|
23
|
-
|
|
24
|
-
/* With overflow, add
|
|
25
|
-
a bit of space to ensure tooltips
|
|
26
|
-
are still easily hover-able */
|
|
27
|
-
&.hasOverflow {
|
|
28
|
-
padding-right: 24px;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
&:hover {
|
|
33
|
-
cursor: pointer;
|
|
34
|
-
|
|
35
|
-
--inner-text-color: var(--black);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
&:focus {
|
|
39
|
-
--inner-text-color: var(--black);
|
|
40
|
-
--inner-decoration: underline;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
&.isActiveTab {
|
|
44
|
-
--inner-border-color: var(--black);
|
|
45
|
-
--inner-text-color: var(--black);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.inner {
|
|
50
|
-
display: flex;
|
|
51
|
-
align-items: center;
|
|
52
|
-
padding: 18px 0 15px 0;
|
|
53
|
-
border-bottom: 3px solid;
|
|
54
|
-
border-color: var(--inner-border-color);
|
|
55
|
-
color: var(--inner-text-color);
|
|
56
|
-
transition: color 0.2s;
|
|
57
|
-
white-space: nowrap;
|
|
58
|
-
text-decoration: var(--inner-decoration);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.tooltipTrigger {
|
|
62
|
-
margin-left: 0.5rem;
|
|
63
|
-
margin-top: 1px;
|
|
64
|
-
width: 1.125rem;
|
|
65
|
-
|
|
66
|
-
& svg {
|
|
67
|
-
width: 100%;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
@@ -1,241 +0,0 @@
|
|
|
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
|
|
@@ -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
|
-
}
|