Skip to content

Commit e9aed01

Browse files
authored
Export Tabs and other custom components (#102)
* Move toggle tab components to ToggleProvider file * Refactor out components into their own shadowable module * Fix imports * * as styles * v0.1.21
1 parent 6d71d5e commit e9aed01

File tree

7 files changed

+331
-320
lines changed

7 files changed

+331
-320
lines changed

packages/gatsby-theme-iterative/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dvcorg/gatsby-theme-iterative",
3-
"version": "0.1.20",
3+
"version": "0.1.21",
44
"description": "",
55
"main": "index.js",
66
"types": "src/typings.d.ts",

packages/gatsby-theme-iterative/src/components/Documentation/Markdown/ToggleProvider/index.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import cn from 'classnames'
2+
import { nanoid } from 'nanoid'
13
import React, {
24
createContext,
35
PropsWithChildren,
6+
useContext,
47
useEffect,
8+
useRef,
59
useState
610
} from 'react'
11+
import * as styles from './styles.module.css'
712

813
interface ITogglesData {
914
[key: string]: {
@@ -124,3 +129,101 @@ export const TogglesProvider: React.FC<
124129
</TogglesContext.Provider>
125130
)
126131
}
132+
133+
const ToggleTab: React.FC<
134+
PropsWithChildren<{
135+
id: string
136+
title: string
137+
ind: number
138+
onChange: () => void
139+
checked: boolean
140+
}>
141+
> = ({ children, id, checked, ind, onChange, title }) => {
142+
const inputId = `tab-${id}-${ind}`
143+
144+
return (
145+
<>
146+
<input
147+
id={inputId}
148+
type="radio"
149+
name={`toggle-${id}`}
150+
onChange={onChange}
151+
checked={checked}
152+
/>
153+
<label className={styles.tabHeading} htmlFor={inputId}>
154+
{title}
155+
</label>
156+
{children}
157+
</>
158+
)
159+
}
160+
161+
export const Toggle: React.FC<{
162+
height?: string
163+
children: Array<{ props: { title: string } } | string>
164+
}> = ({ height, children }) => {
165+
const [toggleId, setToggleId] = useState('')
166+
const {
167+
addNewToggle = (): null => null,
168+
updateToggleInd = (): null => null,
169+
togglesData = {}
170+
} = useContext(TogglesContext)
171+
const tabs: Array<{ props: { title: string } } | string> = children.filter(
172+
child => child !== '\n'
173+
)
174+
const tabsTitles = tabs.map(tab =>
175+
typeof tab === 'object' ? tab.props.title : ''
176+
)
177+
const toggleEl = useRef<HTMLDivElement>(null)
178+
179+
useEffect(() => {
180+
const tabParent =
181+
toggleEl.current && toggleEl.current.closest('.toggle .tab')
182+
const labelParentText =
183+
tabParent &&
184+
tabParent.previousElementSibling &&
185+
tabParent.previousElementSibling.textContent
186+
187+
if (toggleId === '') {
188+
const newId = nanoid()
189+
addNewToggle(newId, tabsTitles, labelParentText)
190+
setToggleId(newId)
191+
}
192+
193+
if (toggleId && !togglesData[toggleId]) {
194+
addNewToggle(toggleId, tabsTitles, labelParentText)
195+
}
196+
}, [togglesData])
197+
198+
return (
199+
<div className={cn('toggle', styles.toggle)} ref={toggleEl}>
200+
{tabs.map((tab, i) => (
201+
<ToggleTab
202+
ind={i}
203+
key={i}
204+
title={tabsTitles[i]}
205+
id={toggleId}
206+
checked={
207+
i === (togglesData[toggleId] ? togglesData[toggleId].checkedInd : 0)
208+
}
209+
onChange={(): void => updateToggleInd(toggleId, i)}
210+
>
211+
<div
212+
className={cn('tab', styles.tab)}
213+
style={{
214+
minHeight: height
215+
}}
216+
>
217+
{tab as string}
218+
</div>
219+
</ToggleTab>
220+
))}
221+
</div>
222+
)
223+
}
224+
225+
export const Tab: React.FC<PropsWithChildren<Record<never, never>>> = ({
226+
children
227+
}) => {
228+
return <React.Fragment>{children}</React.Fragment>
229+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.toggle {
2+
display: flex;
3+
flex-wrap: wrap;
4+
margin: 0 0 16px;
5+
6+
input {
7+
height: 0;
8+
opacity: 0;
9+
position: absolute;
10+
width: 0;
11+
overflow: hidden;
12+
}
13+
14+
input:checked + label {
15+
color: var(--color-azure);
16+
border-color: var(--color-azure);
17+
}
18+
19+
input:checked + label + .tab {
20+
height: initial;
21+
opacity: initial;
22+
position: static;
23+
width: 100%;
24+
overflow: visible;
25+
}
26+
27+
.tabHeading {
28+
padding: 12px 16px 10px;
29+
background-color: transparent;
30+
border: none;
31+
border-bottom: 2px solid transparent;
32+
font-weight: bold;
33+
font-size: 16px;
34+
font-family: var(--font-base);
35+
order: -1;
36+
37+
&:hover {
38+
cursor: pointer;
39+
}
40+
}
41+
}
42+
43+
.tab {
44+
margin: 0;
45+
padding: 10px 10px 10px 20px;
46+
background-color: rgb(27 31 35 / 5%);
47+
height: 0;
48+
opacity: 0;
49+
position: absolute;
50+
overflow: hidden;
51+
width: 0;
52+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React, {
2+
PropsWithChildren,
3+
ReactElement,
4+
ReactNode,
5+
useEffect,
6+
useMemo,
7+
useState
8+
} from 'react'
9+
import { useLocation } from '@reach/router'
10+
import Collapsible from 'react-collapsible'
11+
import Slugger from '../../../../utils/front/Slugger'
12+
import { ReactComponent as LinkIcon } from '../../../../images/linkIcon.svg'
13+
import Link from '../../../Link'
14+
import Tooltip from '../Tooltip'
15+
import * as styles from '../styles.module.css'
16+
17+
type RemarkNode = { props: { children: RemarkNode[] } } | string
18+
19+
export const Details: React.FC<
20+
PropsWithChildren<{ slugger: Slugger; id: string }>
21+
> = ({ slugger, children, id }) => {
22+
const [isOpen, setIsOpen] = useState(false)
23+
const location = useLocation()
24+
25+
const filteredChildren = (children as Array<RemarkNode>).filter(
26+
child => child !== '\n'
27+
)
28+
const firstChild = filteredChildren[0] as JSX.Element
29+
30+
if (!/^h.$/.test(firstChild.type)) {
31+
throw new Error('The first child of a details element must be a heading!')
32+
}
33+
34+
/*
35+
To work around auto-linked headings, the last child of the heading node
36+
must be removed. The only way around this is the change the autolinker,
37+
which we currently have as an external package.
38+
*/
39+
const triggerChildren: RemarkNode[] = firstChild.props.children.slice(
40+
0,
41+
firstChild.props.children.length - 1
42+
)
43+
44+
const title = triggerChildren.reduce<string>((acc, cur) => {
45+
return (acc +=
46+
typeof cur === 'string'
47+
? cur
48+
: typeof cur === 'object'
49+
? cur?.props?.children?.toString()
50+
: '')
51+
}, '')
52+
id = useMemo(() => {
53+
return id ? slugger.slug(id) : slugger.slug(title)
54+
}, [id, title])
55+
56+
useEffect(() => {
57+
if (location.hash === `#${id}`) {
58+
setIsOpen(true)
59+
}
60+
61+
return () => {
62+
setIsOpen(false)
63+
}
64+
}, [location.hash])
65+
66+
/*
67+
Collapsible's trigger type wants ReactElement, so we force a TS cast from
68+
ReactNode here.
69+
*/
70+
return (
71+
<div id={id} className="collapsableDiv">
72+
<Link
73+
href={`#${id}`}
74+
aria-label={triggerChildren.toString()}
75+
className="anchor after"
76+
>
77+
<LinkIcon />
78+
</Link>
79+
<Collapsible
80+
open={isOpen}
81+
trigger={triggerChildren as unknown as ReactElement}
82+
transitionTime={200}
83+
>
84+
{filteredChildren.slice(1) as ReactNode}
85+
</Collapsible>
86+
</div>
87+
)
88+
}
89+
90+
export const Abbr: React.FC<Record<string, never>> = ({ children }) => {
91+
return <Tooltip text={(children as string[])[0]} />
92+
}
93+
94+
export const Cards: React.FC<PropsWithChildren<Record<never, never>>> = ({
95+
children
96+
}) => {
97+
return <div className={styles.cards}>{children}</div>
98+
}
99+
100+
export const InnerCard: React.FC<
101+
PropsWithChildren<{
102+
href?: string
103+
className?: string
104+
}>
105+
> = ({ href, children, className }) =>
106+
href ? (
107+
<Link href={href} className={className}>
108+
{children}
109+
</Link>
110+
) : (
111+
<div className={className}>{children}</div>
112+
)
113+
114+
export const Card: React.FC<
115+
PropsWithChildren<{
116+
icon?: string
117+
heading?: string
118+
href?: string
119+
headingtag:
120+
| string
121+
| React.FC<
122+
PropsWithChildren<{
123+
className: string
124+
}>
125+
>
126+
}>
127+
> = ({ children, icon, heading, headingtag: Heading = 'h3', href }) => {
128+
let iconElement
129+
130+
if (Array.isArray(children) && icon) {
131+
const firstRealItemIndex = children.findIndex(x => x !== '\n')
132+
iconElement = children[firstRealItemIndex]
133+
children = children.slice(firstRealItemIndex + 1)
134+
}
135+
136+
return (
137+
<div className={styles.cardWrapper}>
138+
<InnerCard href={href} className={styles.card}>
139+
{iconElement && <div className={styles.cardIcon}>{iconElement}</div>}
140+
<div className={styles.cardContent}>
141+
{heading && (
142+
<Heading className={styles.cardHeading}>{heading}</Heading>
143+
)}
144+
{children}
145+
</div>
146+
</InnerCard>
147+
</div>
148+
)
149+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React, { PropsWithChildren } from 'react'
2+
import Slugger from '../../../../utils/front/Slugger'
3+
import { NoPreRedirectLink } from '../../../Link'
4+
import Admonition from '../Admonition'
5+
import { Tab, Toggle } from '../ToggleProvider'
6+
import { Abbr, Card, Cards, Details } from './default'
7+
8+
export const getComponents = (slugger: Slugger) => ({
9+
a: NoPreRedirectLink,
10+
abbr: Abbr,
11+
card: Card,
12+
cards: Cards,
13+
details: ({ id, children }: PropsWithChildren<{ id: string }>) => (
14+
<Details slugger={slugger} id={id}>
15+
{children}
16+
</Details>
17+
),
18+
toggle: Toggle,
19+
tab: Tab,
20+
admon: Admonition,
21+
admonition: Admonition
22+
})

0 commit comments

Comments
 (0)