@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.
- package/.eslintrc.js +4 -0
- package/CHANGELOG.md +12 -0
- package/package.json +40 -0
- package/src/global.d.ts +1 -0
- package/src/head/global.d.ts +1 -0
- package/src/head/index.tsx +119 -0
- package/src/subnav/helpers/useStuckRef.ts +50 -0
- package/src/subnav/index.tsx +133 -0
- package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.test.js +25 -0
- package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.ts +17 -0
- package/src/subnav/partials/CtaLinks/github-stars-link/index.tsx +84 -0
- package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.test.js +25 -0
- package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.ts +25 -0
- package/src/subnav/partials/CtaLinks/icons/github.svg +4 -0
- package/src/subnav/partials/CtaLinks/index.tsx +46 -0
- package/src/subnav/partials/MenuItemsDefault/index.tsx +63 -0
- package/src/subnav/partials/MenuItemsDefault/style.module.scss +71 -0
- package/src/subnav/partials/MenuItemsOverflow/index.tsx +50 -0
- package/src/subnav/partials/MenuItemsOverflow/style.module.scss +51 -0
- package/src/subnav/partials/TitleLink/index.tsx +25 -0
- package/src/subnav/partials/nav-item-text/index.tsx +29 -0
- package/src/subnav/partials/nav-item-text/style.module.scss +19 -0
- package/src/subnav/style.module.scss +13 -0
- package/src/tabs/hooks/use-scroll-left.ts +27 -0
- package/src/tabs/hooks/use-window-size.js +33 -0
- package/src/tabs/icons/chevron-right.svg +1 -0
- package/src/tabs/icons/tooltip.svg +1 -0
- package/src/tabs/index.tsx +102 -0
- package/src/tabs/partials/tab-trigger/index.tsx +66 -0
- package/src/tabs/partials/tab-trigger/style.module.scss +69 -0
- package/src/tabs/partials/tab-triggers/index.tsx +241 -0
- package/src/tabs/partials/tab-triggers/style.module.scss +193 -0
- package/src/tabs/partials/tooltip/index.tsx +112 -0
- package/src/tabs/partials/tooltip/style.module.scss +38 -0
- package/src/tabs/provider.js +18 -0
- package/src/tabs/style.module.css +4 -0
- package/src/tabs/utils/smooth-scroll.js +88 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
padding: 0 32px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
align-items: center;
|
|
6
|
+
list-style: none;
|
|
7
|
+
|
|
8
|
+
&.alignRight {
|
|
9
|
+
margin-right: 0;
|
|
10
|
+
padding-right: 16px;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.listItem {
|
|
15
|
+
position: relative;
|
|
16
|
+
white-space: nowrap;
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.navLink {
|
|
22
|
+
composes: g-type-body-small-strong from global;
|
|
23
|
+
position: relative;
|
|
24
|
+
padding: 0 16px;
|
|
25
|
+
line-height: 2.5rem;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
white-space: nowrap;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.submenuItem {
|
|
32
|
+
composes: g-type-body-small-strong from global;
|
|
33
|
+
color: var(--black);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.submenuModal {
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
|
|
39
|
+
background: var(--white);
|
|
40
|
+
z-index: 1;
|
|
41
|
+
display: block;
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: 100%;
|
|
44
|
+
margin: 8px 0 0 0;
|
|
45
|
+
padding: 24px;
|
|
46
|
+
left: 50%;
|
|
47
|
+
transform: translateX(-50%);
|
|
48
|
+
list-style: none;
|
|
49
|
+
|
|
50
|
+
& li {
|
|
51
|
+
& a:hover .text {
|
|
52
|
+
text-decoration: underline;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
& + li {
|
|
56
|
+
margin-top: 6px;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&.isCollapsed {
|
|
61
|
+
display: none;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.verticalDivider {
|
|
66
|
+
background: var(--gray-5);
|
|
67
|
+
height: 1.75rem;
|
|
68
|
+
width: 1px;
|
|
69
|
+
margin: 0 8px;
|
|
70
|
+
display: block;
|
|
71
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import LinkWrap from '@dbosoft/react-uicore/link-wrap'
|
|
4
|
+
import CtaLinks from '../CtaLinks'
|
|
5
|
+
import type { ICtaItem, MenuItem } from '../..'
|
|
6
|
+
import s from './style.module.scss'
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
interface MenuItemsOverflowProps {
|
|
11
|
+
menuItems: MenuItem[],
|
|
12
|
+
ctaLinks: ICtaItem[],
|
|
13
|
+
hideGithubStars: boolean,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export default function MenuItemsOverflow({ menuItems, ctaLinks, hideGithubStars }: MenuItemsOverflowProps) {
|
|
18
|
+
return (<><ul className={s.ulElem}>
|
|
19
|
+
{menuItems.map((menuItem, stableIdx) => {
|
|
20
|
+
if (menuItem === 'divider') return null
|
|
21
|
+
|
|
22
|
+
const { text, url } = menuItem
|
|
23
|
+
return (
|
|
24
|
+
<SubmenuItem
|
|
25
|
+
active={menuItem.active || false}
|
|
26
|
+
key={stableIdx}
|
|
27
|
+
text={text}
|
|
28
|
+
url={url}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
})}
|
|
32
|
+
</ul>
|
|
33
|
+
<CtaLinks
|
|
34
|
+
hideGithubStars={hideGithubStars}
|
|
35
|
+
isInDropdown
|
|
36
|
+
links={ctaLinks}
|
|
37
|
+
/></>)
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function SubmenuItem({ url, text, active }: { url: string; text: string; active: boolean }) {
|
|
42
|
+
return (
|
|
43
|
+
<li>
|
|
44
|
+
<LinkWrap Link={Link} className={s.submenuItem} href={url} title={text}>
|
|
45
|
+
{text}
|
|
46
|
+
</LinkWrap>
|
|
47
|
+
</li>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
position: relative;
|
|
3
|
+
margin-left: auto;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.dropdown {
|
|
7
|
+
background: var(--white);
|
|
8
|
+
border-radius: 4px;
|
|
9
|
+
box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
|
|
10
|
+
margin-top: 8px;
|
|
11
|
+
max-height: calc(100vh - 300%);
|
|
12
|
+
overflow-y: auto;
|
|
13
|
+
padding: 12px 24px 0 24px;
|
|
14
|
+
position: absolute;
|
|
15
|
+
right: 0;
|
|
16
|
+
top: 100%;
|
|
17
|
+
width: 256px;
|
|
18
|
+
z-index: 1;
|
|
19
|
+
|
|
20
|
+
&.isCollapsed {
|
|
21
|
+
display: none;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ulElem {
|
|
26
|
+
padding: 0;
|
|
27
|
+
margin: 0;
|
|
28
|
+
list-style: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.submenuTitle {
|
|
32
|
+
composes: g-type-label from global;
|
|
33
|
+
margin: 12px 0 8px 0;
|
|
34
|
+
color: var(--gray-3);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.divider {
|
|
38
|
+
margin: 12px 0;
|
|
39
|
+
border: 0;
|
|
40
|
+
padding: 0;
|
|
41
|
+
height: 1px;
|
|
42
|
+
background: var(--gray-5);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.submenuItem {
|
|
46
|
+
composes: g-type-body-small-strong from global;
|
|
47
|
+
display: block;
|
|
48
|
+
padding: 4px 0;
|
|
49
|
+
line-height: 1.6em;
|
|
50
|
+
color: var(--black);
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FC, ReactElement, ReactNode } from 'react'
|
|
2
|
+
import LinkWrap from '@dbosoft/react-uicore/link-wrap'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const TitleLink: FC<{
|
|
7
|
+
text: string,
|
|
8
|
+
url: string,
|
|
9
|
+
children?: ReactNode
|
|
10
|
+
}> = ({
|
|
11
|
+
text,
|
|
12
|
+
url, children }) => {
|
|
13
|
+
return (
|
|
14
|
+
<LinkWrap
|
|
15
|
+
Link={Link}
|
|
16
|
+
className="font-bold text-gray-900 tracking-widest text-lg inline"
|
|
17
|
+
href={url}
|
|
18
|
+
title={text}
|
|
19
|
+
>{text}
|
|
20
|
+
{children}
|
|
21
|
+
</LinkWrap>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default TitleLink
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* A span of styled text with
|
|
7
|
+
* an active state that adds a thick
|
|
8
|
+
* bottom border.
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
function NavItemText({
|
|
12
|
+
isActive,
|
|
13
|
+
text,
|
|
14
|
+
}: {
|
|
15
|
+
/** If true, item will be rendered with a thick bottom border below the text. */
|
|
16
|
+
isActive: boolean
|
|
17
|
+
/** Plain text to render within the styled <span /> */
|
|
18
|
+
text: string
|
|
19
|
+
}): React.ReactElement {
|
|
20
|
+
return (
|
|
21
|
+
<span
|
|
22
|
+
className={classNames("text-black text-sm font-semibold", isActive ? "border-b-2 pb-2 border-black" : "")}
|
|
23
|
+
>
|
|
24
|
+
{text}
|
|
25
|
+
</span>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default NavItemText
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
color: var(--black);
|
|
3
|
+
position: relative;
|
|
4
|
+
|
|
5
|
+
&::after {
|
|
6
|
+
content: '';
|
|
7
|
+
position: absolute;
|
|
8
|
+
left: 0;
|
|
9
|
+
right: 0;
|
|
10
|
+
bottom: 0;
|
|
11
|
+
height: 2px;
|
|
12
|
+
background: var(--black);
|
|
13
|
+
opacity: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&.isActive::after {
|
|
17
|
+
opacity: 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
--gray-6-transparent: rgba(174, 176, 183, 0.45);
|
|
3
|
+
position: sticky;
|
|
4
|
+
top: -1px;
|
|
5
|
+
z-index: 800;
|
|
6
|
+
border-top: 1px solid var(--gray-6-transparent);
|
|
7
|
+
border-bottom: 1px solid transparent;
|
|
8
|
+
transition: border-bottom-color 0.8s;
|
|
9
|
+
|
|
10
|
+
&.isSticky {
|
|
11
|
+
border-bottom-color: var(--gray-6-transparent);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, MutableRefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
export default function useScrollLeft(): [
|
|
4
|
+
scrollRef: MutableRefObject<$TSFixMe>,
|
|
5
|
+
scrollLeft: number
|
|
6
|
+
] {
|
|
7
|
+
const scrollRef = useRef(null)
|
|
8
|
+
const [scrollLeft, setScrollLeft] = useState()
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const scrollElem = scrollRef.current as $TSFixMe;
|
|
12
|
+
// Handler to call when scrollLeft may be affected
|
|
13
|
+
function handleScroll() {
|
|
14
|
+
if (!scrollRef.current) return null
|
|
15
|
+
// Set scroll data to state
|
|
16
|
+
setScrollLeft((scrollRef.current as $TSFixMe).scrollLeft)
|
|
17
|
+
}
|
|
18
|
+
// Add event listener
|
|
19
|
+
scrollElem.addEventListener('scroll', handleScroll)
|
|
20
|
+
// Call handler right away so state gets updated with initial scroll position
|
|
21
|
+
handleScroll()
|
|
22
|
+
// Remove event listener on cleanup
|
|
23
|
+
return () => scrollElem.removeEventListener('scroll', handleScroll)
|
|
24
|
+
}, [scrollRef])
|
|
25
|
+
|
|
26
|
+
return [scrollRef, 0]
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
// https://usehooks.com/useWindowSize/
|
|
4
|
+
export default function useWindowSize() {
|
|
5
|
+
// Initialize state with undefined width/height so server and client renders match
|
|
6
|
+
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
|
7
|
+
const [windowSize, setWindowSize] = useState({
|
|
8
|
+
width: undefined,
|
|
9
|
+
height: undefined,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// Handler to call on window resize
|
|
14
|
+
function handleResize() {
|
|
15
|
+
// Set window width/height to state
|
|
16
|
+
setWindowSize({
|
|
17
|
+
width: window.innerWidth,
|
|
18
|
+
height: window.innerHeight,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Add event listener
|
|
23
|
+
window.addEventListener('resize', handleResize)
|
|
24
|
+
|
|
25
|
+
// Call handler right away so state gets updated with initial window size
|
|
26
|
+
handleResize()
|
|
27
|
+
|
|
28
|
+
// Remove event listener on cleanup
|
|
29
|
+
return () => window.removeEventListener('resize', handleResize)
|
|
30
|
+
}, []) // Empty array ensures that effect is only run on mount
|
|
31
|
+
|
|
32
|
+
return windowSize
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 18l6-6-6-6" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" stroke="var(--gray-4)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path clip-rule="evenodd" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" stroke="var(--gray-4)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 17.387a.379.379 0 100-.758.379.379 0 000 .758z" fill="var(--gray-4)" stroke="var(--gray-4)" stroke-width="1.5" stroke-linejoin="round"/></svg>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { TabTriggerType } from './partials/tab-trigger'
|
|
3
|
+
import TabTriggers from './partials/tab-triggers'
|
|
4
|
+
import TabProvider, { useTabGroups } from './provider'
|
|
5
|
+
import s from './style.module.css'
|
|
6
|
+
|
|
7
|
+
interface TabChildProps {
|
|
8
|
+
/** Renders the tab contents. */
|
|
9
|
+
children: React.ReactElement
|
|
10
|
+
/** Plain text used for the tab heading. */
|
|
11
|
+
heading: string
|
|
12
|
+
/** Accepts a string such that, when the tab is active, other Tab elements outside the instance with a matching `group` value will automatically be shown. Note that `TabProvider` is required in order for this feature to function.v */
|
|
13
|
+
group?: string
|
|
14
|
+
/** Plain text displayed in a tooltip beside the tab heading. */
|
|
15
|
+
tooltip?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Tab({ children }: TabChildProps): React.ReactElement {
|
|
19
|
+
return <>{children}</>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TabsProps {
|
|
23
|
+
/** Children to be displayed as tabs. Each child accepts the props `{ children: ReactElement, heading: string, tooltip?: string, group?: string }` */
|
|
24
|
+
children: Array<React.ReactElement<TabChildProps>>
|
|
25
|
+
/** If set to true, the tabs are centered in their container. Default is left-aligned. */
|
|
26
|
+
centered?: boolean
|
|
27
|
+
/** Optional className to add to the root element. */
|
|
28
|
+
className?: string
|
|
29
|
+
/** Set the default tab by its index. If not set, or if out of range, will default to the first tab, at index 0. */
|
|
30
|
+
defaultTabIdx?: number
|
|
31
|
+
/** If set to true, the bottom border underneath the tabs will fill the available width. Default border fills the constrained `.g-grid-container`. */
|
|
32
|
+
fullWidthBorder?: boolean
|
|
33
|
+
/** Optional callback which is executed when a new tab is selected. */
|
|
34
|
+
onChange?: (targetTabIdx: number, targetTabGroup?: string) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Tabs({
|
|
38
|
+
children,
|
|
39
|
+
className,
|
|
40
|
+
defaultTabIdx = 0,
|
|
41
|
+
centered = false,
|
|
42
|
+
fullWidthBorder = false,
|
|
43
|
+
onChange,
|
|
44
|
+
}: TabsProps): React.ReactElement {
|
|
45
|
+
// Ensures a single child object converts to an array
|
|
46
|
+
children = Array.prototype.concat(children)
|
|
47
|
+
|
|
48
|
+
const isDefaultOutOfBounds =
|
|
49
|
+
defaultTabIdx >= children.length || defaultTabIdx < 0
|
|
50
|
+
|
|
51
|
+
const [activeTabIdx, setActiveTabIdx] = useState(
|
|
52
|
+
// if specified default is out of bounds (ie, it's determined at runtime),
|
|
53
|
+
// fallback to 0 to avoid throwing an error
|
|
54
|
+
isDefaultOutOfBounds ? 0 : defaultTabIdx
|
|
55
|
+
)
|
|
56
|
+
const groupCtx = useTabGroups()
|
|
57
|
+
|
|
58
|
+
function setActiveTab(targetIdx : number, groupId?:string) {
|
|
59
|
+
setActiveTabIdx(targetIdx)
|
|
60
|
+
if (onChange) onChange(targetIdx, groupId)
|
|
61
|
+
if (groupCtx) groupCtx.setActiveTabGroup(groupId)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const hasGroups = children.filter((tab) => tab.props.group).length > 0
|
|
66
|
+
if (
|
|
67
|
+
process.env.NODE_ENV !== 'production' &&
|
|
68
|
+
hasGroups &&
|
|
69
|
+
groupCtx === undefined
|
|
70
|
+
) {
|
|
71
|
+
console.warn(
|
|
72
|
+
'@dbosoft/react-tabs: The `TabProvider` cannot be accessed. Make sure it wraps the `Tabs` components so Tab Groups can work properly.'
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}, [children, groupCtx])
|
|
76
|
+
|
|
77
|
+
if (!children) {
|
|
78
|
+
process.env.NODE_ENV !== 'production' &&
|
|
79
|
+
console.warn(
|
|
80
|
+
'@dbosoft/react-tabs: There are no `Tab` children for the `Tabs` component to render.'
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<section className={className}>
|
|
86
|
+
<TabTriggers
|
|
87
|
+
tabs={children.map((tab, index) => {
|
|
88
|
+
const { heading, group, tooltip } = tab.props
|
|
89
|
+
return { index, heading, group, tooltip } as TabTriggerType
|
|
90
|
+
})}
|
|
91
|
+
centered={centered}
|
|
92
|
+
fullWidthBorder={fullWidthBorder}
|
|
93
|
+
activeTabIdx={activeTabIdx}
|
|
94
|
+
setActiveTab={setActiveTab}
|
|
95
|
+
/>
|
|
96
|
+
<div className={s.content}>{children[activeTabIdx].props.children}</div>
|
|
97
|
+
</section>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default Tabs
|
|
102
|
+
export { TabProvider, useTabGroups, Tab }
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
}
|