Skip to content

Commit

Permalink
Export Tabs and other custom components (#102)
Browse files Browse the repository at this point in the history
* Move toggle tab components to ToggleProvider file

* Refactor out components into their own shadowable module

* Fix imports

* * as styles

* v0.1.21
  • Loading branch information
rogermparent authored Nov 7, 2022
1 parent 6d71d5e commit e9aed01
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 320 deletions.
2 changes: 1 addition & 1 deletion packages/gatsby-theme-iterative/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dvcorg/gatsby-theme-iterative",
"version": "0.1.20",
"version": "0.1.21",
"description": "",
"main": "index.js",
"types": "src/typings.d.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import cn from 'classnames'
import { nanoid } from 'nanoid'
import React, {
createContext,
PropsWithChildren,
useContext,
useEffect,
useRef,
useState
} from 'react'
import * as styles from './styles.module.css'

interface ITogglesData {
[key: string]: {
Expand Down Expand Up @@ -124,3 +129,101 @@ export const TogglesProvider: React.FC<
</TogglesContext.Provider>
)
}

const ToggleTab: React.FC<
PropsWithChildren<{
id: string
title: string
ind: number
onChange: () => void
checked: boolean
}>
> = ({ children, id, checked, ind, onChange, title }) => {
const inputId = `tab-${id}-${ind}`

return (
<>
<input
id={inputId}
type="radio"
name={`toggle-${id}`}
onChange={onChange}
checked={checked}
/>
<label className={styles.tabHeading} htmlFor={inputId}>
{title}
</label>
{children}
</>
)
}

export const Toggle: React.FC<{
height?: string
children: Array<{ props: { title: string } } | string>
}> = ({ height, children }) => {
const [toggleId, setToggleId] = useState('')
const {
addNewToggle = (): null => null,
updateToggleInd = (): null => null,
togglesData = {}
} = useContext(TogglesContext)
const tabs: Array<{ props: { title: string } } | string> = children.filter(
child => child !== '\n'
)
const tabsTitles = tabs.map(tab =>
typeof tab === 'object' ? tab.props.title : ''
)
const toggleEl = useRef<HTMLDivElement>(null)

useEffect(() => {
const tabParent =
toggleEl.current && toggleEl.current.closest('.toggle .tab')
const labelParentText =
tabParent &&
tabParent.previousElementSibling &&
tabParent.previousElementSibling.textContent

if (toggleId === '') {
const newId = nanoid()
addNewToggle(newId, tabsTitles, labelParentText)
setToggleId(newId)
}

if (toggleId && !togglesData[toggleId]) {
addNewToggle(toggleId, tabsTitles, labelParentText)
}
}, [togglesData])

return (
<div className={cn('toggle', styles.toggle)} ref={toggleEl}>
{tabs.map((tab, i) => (
<ToggleTab
ind={i}
key={i}
title={tabsTitles[i]}
id={toggleId}
checked={
i === (togglesData[toggleId] ? togglesData[toggleId].checkedInd : 0)
}
onChange={(): void => updateToggleInd(toggleId, i)}
>
<div
className={cn('tab', styles.tab)}
style={{
minHeight: height
}}
>
{tab as string}
</div>
</ToggleTab>
))}
</div>
)
}

export const Tab: React.FC<PropsWithChildren<Record<never, never>>> = ({
children
}) => {
return <React.Fragment>{children}</React.Fragment>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.toggle {
display: flex;
flex-wrap: wrap;
margin: 0 0 16px;

input {
height: 0;
opacity: 0;
position: absolute;
width: 0;
overflow: hidden;
}

input:checked + label {
color: var(--color-azure);
border-color: var(--color-azure);
}

input:checked + label + .tab {
height: initial;
opacity: initial;
position: static;
width: 100%;
overflow: visible;
}

.tabHeading {
padding: 12px 16px 10px;
background-color: transparent;
border: none;
border-bottom: 2px solid transparent;
font-weight: bold;
font-size: 16px;
font-family: var(--font-base);
order: -1;

&:hover {
cursor: pointer;
}
}
}

.tab {
margin: 0;
padding: 10px 10px 10px 20px;
background-color: rgb(27 31 35 / 5%);
height: 0;
opacity: 0;
position: absolute;
overflow: hidden;
width: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, {
PropsWithChildren,
ReactElement,
ReactNode,
useEffect,
useMemo,
useState
} from 'react'
import { useLocation } from '@reach/router'
import Collapsible from 'react-collapsible'
import Slugger from '../../../../utils/front/Slugger'
import { ReactComponent as LinkIcon } from '../../../../images/linkIcon.svg'
import Link from '../../../Link'
import Tooltip from '../Tooltip'
import * as styles from '../styles.module.css'

type RemarkNode = { props: { children: RemarkNode[] } } | string

export const Details: React.FC<
PropsWithChildren<{ slugger: Slugger; id: string }>
> = ({ slugger, children, id }) => {
const [isOpen, setIsOpen] = useState(false)
const location = useLocation()

const filteredChildren = (children as Array<RemarkNode>).filter(
child => child !== '\n'
)
const firstChild = filteredChildren[0] as JSX.Element

if (!/^h.$/.test(firstChild.type)) {
throw new Error('The first child of a details element must be a heading!')
}

/*
To work around auto-linked headings, the last child of the heading node
must be removed. The only way around this is the change the autolinker,
which we currently have as an external package.
*/
const triggerChildren: RemarkNode[] = firstChild.props.children.slice(
0,
firstChild.props.children.length - 1
)

const title = triggerChildren.reduce<string>((acc, cur) => {
return (acc +=
typeof cur === 'string'
? cur
: typeof cur === 'object'
? cur?.props?.children?.toString()
: '')
}, '')
id = useMemo(() => {
return id ? slugger.slug(id) : slugger.slug(title)
}, [id, title])

useEffect(() => {
if (location.hash === `#${id}`) {
setIsOpen(true)
}

return () => {
setIsOpen(false)
}
}, [location.hash])

/*
Collapsible's trigger type wants ReactElement, so we force a TS cast from
ReactNode here.
*/
return (
<div id={id} className="collapsableDiv">
<Link
href={`#${id}`}
aria-label={triggerChildren.toString()}
className="anchor after"
>
<LinkIcon />
</Link>
<Collapsible
open={isOpen}
trigger={triggerChildren as unknown as ReactElement}
transitionTime={200}
>
{filteredChildren.slice(1) as ReactNode}
</Collapsible>
</div>
)
}

export const Abbr: React.FC<Record<string, never>> = ({ children }) => {
return <Tooltip text={(children as string[])[0]} />
}

export const Cards: React.FC<PropsWithChildren<Record<never, never>>> = ({
children
}) => {
return <div className={styles.cards}>{children}</div>
}

export const InnerCard: React.FC<
PropsWithChildren<{
href?: string
className?: string
}>
> = ({ href, children, className }) =>
href ? (
<Link href={href} className={className}>
{children}
</Link>
) : (
<div className={className}>{children}</div>
)

export const Card: React.FC<
PropsWithChildren<{
icon?: string
heading?: string
href?: string
headingtag:
| string
| React.FC<
PropsWithChildren<{
className: string
}>
>
}>
> = ({ children, icon, heading, headingtag: Heading = 'h3', href }) => {
let iconElement

if (Array.isArray(children) && icon) {
const firstRealItemIndex = children.findIndex(x => x !== '\n')
iconElement = children[firstRealItemIndex]
children = children.slice(firstRealItemIndex + 1)
}

return (
<div className={styles.cardWrapper}>
<InnerCard href={href} className={styles.card}>
{iconElement && <div className={styles.cardIcon}>{iconElement}</div>}
<div className={styles.cardContent}>
{heading && (
<Heading className={styles.cardHeading}>{heading}</Heading>
)}
{children}
</div>
</InnerCard>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { PropsWithChildren } from 'react'
import Slugger from '../../../../utils/front/Slugger'
import { NoPreRedirectLink } from '../../../Link'
import Admonition from '../Admonition'
import { Tab, Toggle } from '../ToggleProvider'
import { Abbr, Card, Cards, Details } from './default'

export const getComponents = (slugger: Slugger) => ({
a: NoPreRedirectLink,
abbr: Abbr,
card: Card,
cards: Cards,
details: ({ id, children }: PropsWithChildren<{ id: string }>) => (
<Details slugger={slugger} id={id}>
{children}
</Details>
),
toggle: Toggle,
tab: Tab,
admon: Admonition,
admonition: Admonition
})
Loading

0 comments on commit e9aed01

Please sign in to comment.