Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BD-46] feat: table of content for non-components #2013

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion example/src/MyComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Button, Form, Icon, Bubble } from '@edx/paragon'; // eslint-disable-line
import { Button, Form, Icon, Bubble, Skeleton } from '@edx/paragon'; // eslint-disable-line
import { FavoriteBorder } from '@edx/paragon/icons'; // eslint-disable-line

const MyComponent = () => {
Expand Down Expand Up @@ -27,6 +27,7 @@ const MyComponent = () => {
</Form.Group>
<Button onClick={handleClick}>Submit</Button>
</Form>
<Skeleton />
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion www/.env.development
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
SEGMENT_KEY=''
FEATURE_ENABLE_AXE='true'
FEATURE_ENABLE_AXE=''
3 changes: 2 additions & 1 deletion www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"rehype-slug": "^4.0.1",
"sass": "^1.53.0",
"sass-loader": "12.6.0",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
adamstankiewicz marked this conversation as resolved.
Show resolved Hide resolved
"slugify": "^1.6.5"
},
"keywords": [
"paragon",
Expand Down
132 changes: 132 additions & 0 deletions www/src/components/AutoToc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Sticky } from '~paragon-react';
import slugify from 'slugify';

interface IItems {
url?: string,
title?: string,
items?: Array<IItems>,
}

export interface IAutoToc {
className?: string,
tab?: string,
addAnchors?: boolean,
}

function createAnchor(slug: string): HTMLAnchorElement {
const anchor = document.createElement('a');
anchor.ariaHidden = 'true';
anchor.tabIndex = -1;
anchor.href = `#${slug}`;
const span = document.createElement('span');
span.className = 'pgn-doc__anchor';
span.innerText = '#';
anchor.appendChild(span);
return anchor;
}

function getNestedHeadingsData(headingsArray: NodeListOf<HTMLHeadElement>): IItems {
const result: IItems = { items: [] };
let parentHeadingLevel = 2;
headingsArray.forEach(heading => {
const headingLevel = parseInt(heading.tagName.slice(-1), 10);
const headingData = {
url: `#${heading.id}`,
title: heading.firstChild!.textContent!,
items: [],
};
if (!result.items!.length || headingLevel <= parentHeadingLevel) {
parentHeadingLevel = headingLevel;
result.items!.push(headingData);
} else {
const headingDepth = headingLevel - parentHeadingLevel;
let target = result.items![result.items!.length - 1];
for (let i = 1; i < headingDepth; i++) {
if (target?.items!.length) {
target = target.items[target.items.length - 1];
}
}
target.items!.push(headingData);
}
});
return result;
}

function AutoToc({ className, tab = '', addAnchors = true }: IAutoToc) {
const [active, setActive] = useState('');
const [headingsData, setHeadingsData] = useState<IItems>({ items: [] });
const observer = useRef<IntersectionObserver>();

useEffect(() => {
const handleObserver = (entries: IntersectionObserverEntry[]) => {
if (entries[0].intersectionRatio >= 0.5) {
setActive(entries[0].target.id);
}
};

observer.current = new IntersectionObserver(handleObserver, { rootMargin: '-50px 0px -80% 0px', threshold: 0.5 });
const elements = document.querySelectorAll<HTMLHeadElement>('main h2, main h3, main h4, main h5, main h6');
if (addAnchors) {
elements.forEach(el => {
if (el.textContent) {
el.classList.add('pgn-doc__heading');
const slug = slugify(el.textContent, { lower: true });
el.id = slug;
const anchor = createAnchor(slug);
el.appendChild(anchor);
}
});
}
const headings = getNestedHeadingsData(elements);
setHeadingsData(headings);
elements.forEach((elem) => observer.current?.observe(elem));

return () => observer.current?.disconnect();
}, [tab, addAnchors]);

const generateTree = (headings: { items?: Array<IItems> }) => (headings?.items?.length
? (
<ul className="pgn-doc__toc-list">
{headings.items.map(heading => (
<li key={heading.url}>
<a
href={heading.url}
className={classNames({ active: `#${active}` === heading.url })}
>
{heading.title}
</a>
{!!heading.items && generateTree(heading)}
</li>
))}
</ul>
) : null);

const tocTree = generateTree(headingsData);

return (
<Sticky
offset={6}
className={classNames('pgn-doc__toc', className)}
>
<p className="pgn-doc__toc-header">Contents</p>
{tocTree}
</Sticky>
);
}

AutoToc.propTypes = {
className: PropTypes.string,
tab: PropTypes.string,
addAnchors: PropTypes.bool,
};

AutoToc.defaultProps = {
className: undefined,
tab: undefined,
addAnchors: undefined,
};

export default AutoToc;
2 changes: 1 addition & 1 deletion www/src/components/IconsTable.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
flex-direction: column;
align-items: center;

h3 {
p {
margin-bottom: 0;
padding: 0 .25rem;
}
Expand Down
2 changes: 1 addition & 1 deletion www/src/components/IconsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ function IconsTable({ iconNames }) {
className="pgn-doc__icons-table__preview-title"
onClick={() => copyToClipboard(currentIcon)}
>
<h3 className="rounded">{currentIcon}</h3>
<p className="rounded h3">{currentIcon}</p>
<Icon
key="ContentCopy"
src={IconComponents.ContentCopy}
Expand Down
11 changes: 10 additions & 1 deletion www/src/components/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Settings from './Settings';
import Toc from './Toc';
import { SettingsContext } from '../context/SettingsContext';
import LeaveFeedback from './LeaveFeedback';
import AutoToc from './AutoToc';

if (process.env.NODE_ENV === 'development') {
/* eslint-disable-next-line global-require */
Expand All @@ -37,6 +38,8 @@ export interface ILayout {
hideFooterComponentMenu: boolean,
isMdx: boolean,
tocData: Array<number>,
tab?: string,
isAutoToc?: boolean,
}

function Layout({
Expand All @@ -45,6 +48,8 @@ function Layout({
hideFooterComponentMenu,
isMdx,
tocData,
isAutoToc,
tab,
}: ILayout) {
const isMobile = useMediaQuery({ maxWidth: breakpoints.extraLarge.minWidth });
const { settings } = useContext(SettingsContext);
Expand Down Expand Up @@ -89,8 +94,9 @@ function Layout({
<Col
xl={2}
lg={3}
as={Toc}
as={isAutoToc ? AutoToc : Toc}
data={tocData}
tab={tab}
className="d-none d-lg-block"
/>
</Row>
Expand Down Expand Up @@ -169,13 +175,16 @@ Layout.propTypes = {
showMinimizedTitle: PropTypes.bool,
hideFooterComponentMenu: PropTypes.bool,
isMdx: PropTypes.bool,
tab: PropTypes.string,
};

Layout.defaultProps = {
tocData: {},
showMinimizedTitle: false,
hideFooterComponentMenu: false,
isMdx: false,
tab: undefined,
isAutoToc: false,
};

export default Layout;
30 changes: 16 additions & 14 deletions www/src/pages/foundations/colors.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import React, { useContext } from 'react';
import { graphql } from 'gatsby';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Container } from '~paragon-react';
import Color from 'color';
import { Container } from '~paragon-react';
import SEO from '../../components/SEO';
import MeasuredItem from '../../components/MeasuredItem';
import Layout from '../../components/PageLayout';
import { SettingsContext } from '../../context/SettingsContext';

const utilityClasses = {
bg: (color: string, level: number) => (level ? `bg-${color}-${level}` : `bg-${color}`),
Expand Down Expand Up @@ -102,7 +103,7 @@ const renderColorRamp = (themeName: string, unusedLevels: number[]) => (
key={`${themeName}`}
style={{ flexBasis: '24%', marginRight: '1%', marginBottom: '2rem' }}
>
<h2 className="h5">{themeName}</h2>
<p className="h5">{themeName}</p>
{levels.map(level => (
<Swatch
key={`$${themeName}-${level}`}
Expand All @@ -124,13 +125,14 @@ export interface IColorsPage {

// eslint-disable-next-line react/prop-types
export default function ColorsPage({ data }: IColorsPage) {
const { settings } = useContext(SettingsContext);
parseColors(data.allCssUtilityClasses.nodes); // eslint-disable-line react/prop-types

return (
<Layout>
<Container size="md" className="py-5">
{/* eslint-disable-next-line react/jsx-pascal-case */}
<SEO title="Colors" />
<Layout isAutoToc>
{/* eslint-disable-next-line react/jsx-pascal-case */}
<SEO title="Colors" />
<Container size={settings.containerWidth} className="py-5">
<h1>Colors</h1>
<div className="d-flex flex-wrap">
{colors
Expand All @@ -143,7 +145,7 @@ export default function ColorsPage({ data }: IColorsPage) {
marginBottom: '2rem',
}}
>
<h2 className="h5">accents</h2>
<p className="h5">accents</p>

<Swatch name="$accent-a" colorClassName="bg-accent-a" />
<Swatch name="$accent-b" colorClassName="bg-accent-b" />
Expand Down Expand Up @@ -351,9 +353,9 @@ export default function ColorsPage({ data }: IColorsPage) {
backgrounds.
</p>
<div className="d-flex rounded overflow-hidden mb-3">
<h4 className="mb-0 w-100">Lighter Text</h4>
<h4 className="mb-0 w-100">Regular Text</h4>
<h4 className="mb-0 w-100">Darker Text</h4>
<p className="mb-0 w-100 h4">Lighter Text</p>
<p className="mb-0 w-100 h4">Regular Text</p>
<p className="mb-0 w-100 h4">Darker Text</p>
</div>
<div className="d-flex">
{[500, 700, 900].map(level => (
Expand Down Expand Up @@ -381,13 +383,13 @@ export default function ColorsPage({ data }: IColorsPage) {
<div>
<div className="d-flex rounded overflow-hidden mb-3">
<div className="w-100">
<h4 className="mb-0">Default State</h4>
<p className="mb-0 h4">Default State</p>
</div>
<div className="w-100">
<h4 className="mb-0">Hover State</h4>
<p className="mb-0 h4">Hover State</p>
</div>
<div className="w-100">
<h4 className="mb-0">Active State</h4>
<p className="mb-0 h4">Active State</p>
</div>
</div>
{colors.map(({ themeName }) => {
Expand Down
23 changes: 13 additions & 10 deletions www/src/pages/foundations/elevation.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import {
Container,
Button,
Form,
Container,
Input,
Toast,
Icon,
Expand All @@ -12,6 +12,7 @@ import {
import { Close, WbSunny, DoDisturb } from '~paragon-icons';
import SEO from '../../components/SEO';
import Layout from '../../components/PageLayout';
import { SettingsContext } from '../../context/SettingsContext';

const boxShadowSides = ['down', 'up', 'right', 'left', 'centered'];
const boxShadowLevels = [1, 2, 3, 4, 5];
Expand Down Expand Up @@ -268,23 +269,25 @@ function BoxShadowGenerator() {
}

export default function ElevationPage() {
const { settings } = useContext(SettingsContext);

const levelTitle = boxShadowLevels.map(level => (
<h3 key={level} className="pgn-doc__box-shadow-level-title">
<p key={level} className="pgn-doc__box-shadow-level-title h3">
Level {level}
</h3>
</p>
));

const sideTitle = boxShadowSides.map(side => (
<h3 key={side} className="pgn-doc__box-shadow-side-title">
<p key={side} className="pgn-doc__box-shadow-side-title h3">
{side.charAt(0).toUpperCase() + side.substring(1)}
</h3>
</p>
));

return (
<Layout>
<Container className="py-5" size="md">
{/* eslint-disable-next-line react/jsx-pascal-case */}
<SEO title="Elevation" />
<Layout isAutoToc>
{/* eslint-disable-next-line react/jsx-pascal-case */}
<SEO title="Elevation" />
<Container size={settings.containerWidth} className="py-5">
<h1 className="mb-3">Elevation & Shadow</h1>
<p className="mb-5">
You can quickly add a <code>box-shadow</code> with the Clickable Box-Shadow Grid.
Expand Down
Loading
Loading