From 6692b5dfcadd5d60135d3f23dc770878bd71f2c6 Mon Sep 17 00:00:00 2001 From: monteri <36768631+monteri@users.noreply.github.com> Date: Fri, 18 Aug 2023 14:44:40 +0300 Subject: [PATCH] docs: [BD-46] table of content for non-components (#2013) --- example/src/MyComponent.jsx | 3 +- package-lock.json | 1 + www/.env.development | 2 +- www/package.json | 3 +- www/src/components/AutoToc.tsx | 132 ++++++++++++++++++ www/src/components/IconsTable.scss | 2 +- www/src/components/IconsTable.tsx | 2 +- www/src/components/PageLayout.tsx | 11 +- www/src/pages/foundations/colors.tsx | 30 ++-- www/src/pages/foundations/elevation.jsx | 23 +-- www/src/pages/foundations/responsive.jsx | 14 +- www/src/pages/foundations/spacing.tsx | 16 ++- www/src/pages/foundations/typography.tsx | 13 +- www/src/pages/insights.tsx | 24 ++-- www/src/pages/status.tsx | 13 +- .../templates/default-mdx-page-template.tsx | 9 +- 16 files changed, 231 insertions(+), 67 deletions(-) create mode 100644 www/src/components/AutoToc.tsx diff --git a/example/src/MyComponent.jsx b/example/src/MyComponent.jsx index 3bfab4ddcdf..2b91c20d778 100644 --- a/example/src/MyComponent.jsx +++ b/example/src/MyComponent.jsx @@ -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 = () => { @@ -27,6 +27,7 @@ const MyComponent = () => { + ); }; diff --git a/package-lock.json b/package-lock.json index 36474224c27..244d01ad0cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39254,6 +39254,7 @@ "rehype-slug": "^4.0.1", "sass": "^1.53.0", "sass-loader": "12.6.0", + "slugify": "^1.6.5", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/www/.env.development b/www/.env.development index d036c5d85b9..68b75c7e782 100644 --- a/www/.env.development +++ b/www/.env.development @@ -1,2 +1,2 @@ SEGMENT_KEY='' -FEATURE_ENABLE_AXE='true' +FEATURE_ENABLE_AXE='' diff --git a/www/package.json b/www/package.json index 6648636c990..dcc24ca2da8 100644 --- a/www/package.json +++ b/www/package.json @@ -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", + "slugify": "^1.6.5" }, "keywords": [ "paragon", diff --git a/www/src/components/AutoToc.tsx b/www/src/components/AutoToc.tsx new file mode 100644 index 00000000000..cb95c3919ef --- /dev/null +++ b/www/src/components/AutoToc.tsx @@ -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, +} + +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): 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({ items: [] }); + const observer = useRef(); + + 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('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 }) => (headings?.items?.length + ? ( +
    + {headings.items.map(heading => ( +
  • + + {heading.title} + + {!!heading.items && generateTree(heading)} +
  • + ))} +
+ ) : null); + + const tocTree = generateTree(headingsData); + + return ( + +

Contents

+ {tocTree} +
+ ); +} + +AutoToc.propTypes = { + className: PropTypes.string, + tab: PropTypes.string, + addAnchors: PropTypes.bool, +}; + +AutoToc.defaultProps = { + className: undefined, + tab: undefined, + addAnchors: undefined, +}; + +export default AutoToc; diff --git a/www/src/components/IconsTable.scss b/www/src/components/IconsTable.scss index f5fe6064bab..498e08121c2 100644 --- a/www/src/components/IconsTable.scss +++ b/www/src/components/IconsTable.scss @@ -54,7 +54,7 @@ flex-direction: column; align-items: center; - h3 { + p { margin-bottom: 0; padding: 0 .25rem; } diff --git a/www/src/components/IconsTable.tsx b/www/src/components/IconsTable.tsx index 3d53f833503..a3f6a2a8c7e 100644 --- a/www/src/components/IconsTable.tsx +++ b/www/src/components/IconsTable.tsx @@ -133,7 +133,7 @@ function IconsTable({ iconNames }) { className="pgn-doc__icons-table__preview-title" onClick={() => copyToClipboard(currentIcon)} > -

{currentIcon}

+

{currentIcon}

, + tab?: string, + isAutoToc?: boolean, } function Layout({ @@ -45,6 +48,8 @@ function Layout({ hideFooterComponentMenu, isMdx, tocData, + isAutoToc, + tab, }: ILayout) { const isMobile = useMediaQuery({ maxWidth: breakpoints.extraLarge.minWidth }); const { settings } = useContext(SettingsContext); @@ -89,8 +94,9 @@ function Layout({ @@ -169,6 +175,7 @@ Layout.propTypes = { showMinimizedTitle: PropTypes.bool, hideFooterComponentMenu: PropTypes.bool, isMdx: PropTypes.bool, + tab: PropTypes.string, }; Layout.defaultProps = { @@ -176,6 +183,8 @@ Layout.defaultProps = { showMinimizedTitle: false, hideFooterComponentMenu: false, isMdx: false, + tab: undefined, + isAutoToc: false, }; export default Layout; diff --git a/www/src/pages/foundations/colors.tsx b/www/src/pages/foundations/colors.tsx index dcb15659a8d..dac54930079 100644 --- a/www/src/pages/foundations/colors.tsx +++ b/www/src/pages/foundations/colors.tsx @@ -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}`), @@ -102,7 +103,7 @@ const renderColorRamp = (themeName: string, unusedLevels: number[]) => ( key={`${themeName}`} style={{ flexBasis: '24%', marginRight: '1%', marginBottom: '2rem' }} > -

{themeName}

+

{themeName}

{levels.map(level => ( - - {/* eslint-disable-next-line react/jsx-pascal-case */} - + + {/* eslint-disable-next-line react/jsx-pascal-case */} + +

Colors

{colors @@ -143,7 +145,7 @@ export default function ColorsPage({ data }: IColorsPage) { marginBottom: '2rem', }} > -

accents

+

accents

@@ -351,9 +353,9 @@ export default function ColorsPage({ data }: IColorsPage) { backgrounds.

-

Lighter Text

-

Regular Text

-

Darker Text

+

Lighter Text

+

Regular Text

+

Darker Text

{[500, 700, 900].map(level => ( @@ -381,13 +383,13 @@ export default function ColorsPage({ data }: IColorsPage) {
-

Default State

+

Default State

-

Hover State

+

Hover State

-

Active State

+

Active State

{colors.map(({ themeName }) => { diff --git a/www/src/pages/foundations/elevation.jsx b/www/src/pages/foundations/elevation.jsx index e61aafcc3db..52dac1b8166 100644 --- a/www/src/pages/foundations/elevation.jsx +++ b/www/src/pages/foundations/elevation.jsx @@ -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, @@ -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]; @@ -268,23 +269,25 @@ function BoxShadowGenerator() { } export default function ElevationPage() { + const { settings } = useContext(SettingsContext); + const levelTitle = boxShadowLevels.map(level => ( -

+

Level {level} -

+

)); const sideTitle = boxShadowSides.map(side => ( -

+

{side.charAt(0).toUpperCase() + side.substring(1)} -

+

)); return ( - - - {/* eslint-disable-next-line react/jsx-pascal-case */} - + + {/* eslint-disable-next-line react/jsx-pascal-case */} + +

Elevation & Shadow

You can quickly add a box-shadow with the Clickable Box-Shadow Grid. diff --git a/www/src/pages/foundations/responsive.jsx b/www/src/pages/foundations/responsive.jsx index 44ff50b7d1c..4e97c975ccb 100644 --- a/www/src/pages/foundations/responsive.jsx +++ b/www/src/pages/foundations/responsive.jsx @@ -1,12 +1,13 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { - DataTable, Container, breakpoints, OverlayTrigger, Tooltip, Icon, + DataTable, breakpoints, OverlayTrigger, Tooltip, Icon, Container, } from '~paragon-react'; import { QuestionMark } from '~paragon-icons'; import SEO from '../../components/SEO'; import Layout from '../../components/PageLayout'; import CodeBlock from '../../components/CodeBlock'; +import { SettingsContext } from '../../context/SettingsContext'; const BREAKPOINT_DESCRIPTIONS = { extraSmall: { name: 'Extra small', identifier: 'xs' }, @@ -49,6 +50,7 @@ function MaxWidthCell({ row }) { } function Responsive() { + const { settings } = useContext(SettingsContext); const breakpointsData = Object.keys(breakpoints).map(breakpoint => { const { minWidth, maxWidth } = breakpoints[breakpoint]; const breakpointData = getBreakpointDescription(breakpoint); @@ -58,10 +60,10 @@ function Responsive() { }); return ( - - - {/* eslint-disable-next-line react/jsx-pascal-case */} - + + {/* eslint-disable-next-line react/jsx-pascal-case */} + +

Responsive

Available breakpoints

diff --git a/www/src/pages/foundations/spacing.tsx b/www/src/pages/foundations/spacing.tsx index 8b0d4ce4b09..be2a639df16 100644 --- a/www/src/pages/foundations/spacing.tsx +++ b/www/src/pages/foundations/spacing.tsx @@ -1,10 +1,11 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { Form, Container, DataTable } from '~paragon-react'; +import { Form, DataTable, Container } from '~paragon-react'; import SEO from '../../components/SEO'; import Layout from '../../components/PageLayout'; import MeasuredItem from '../../components/MeasuredItem'; +import { SettingsContext } from '../../context/SettingsContext'; const directions = [ { key: '', name: 'all' }, @@ -83,6 +84,7 @@ SpaceBlock.defaultProps = { }; export default function SpacingPage() { + const { settings } = useContext(SettingsContext); const [size, setSize] = useState(3); const [direction, setDirection] = useState('r'); @@ -94,10 +96,10 @@ export default function SpacingPage() { })); return ( - - - {/* eslint-disable-next-line react/jsx-pascal-case */} - + + {/* eslint-disable-next-line react/jsx-pascal-case */} + +

Spacing

Spacing according to pixels

-

Direction

+

Direction

{directions.map(({ key, name }) => ( - - {/* eslint-disable-next-line react/jsx-pascal-case */} - + + {/* eslint-disable-next-line react/jsx-pascal-case */} + +

Typography

diff --git a/www/src/pages/insights.tsx b/www/src/pages/insights.tsx index 3799aa28a05..44128bd1b6d 100644 --- a/www/src/pages/insights.tsx +++ b/www/src/pages/insights.tsx @@ -2,9 +2,9 @@ import React, { useContext } from 'react'; import { navigate } from 'gatsby'; import PropTypes from 'prop-types'; import { + Container, Tabs, Tab, - Container, } from '~paragon-react'; import SEO from '../components/SEO'; import Layout from '../components/PageLayout'; @@ -21,6 +21,7 @@ import dependentProjectsAnalysis from '../../../dependent-usage.json'; // eslint import { INSIGHTS_TABS, INSIGHTS_PAGES } from '../config'; import componentsUsage from '../utils/componentsUsage'; import { IInsightsContext } from '../types/types'; +import { SettingsContext } from '../context/SettingsContext'; const { lastModified: analysisLastUpdated, @@ -34,6 +35,7 @@ interface TabsDataType { } export default function InsightsPage({ pageContext: { tab } }: { pageContext: { tab: string } }) { + const { settings } = useContext(SettingsContext); const { paragonTypes = {}, isParagonIcon = () => false } = useContext(InsightsContext) as IInsightsContext; const { components, hooks, utils, icons, @@ -59,10 +61,10 @@ export default function InsightsPage({ pageContext: { tab } }: { pageContext: { } }; return ( - - - {/* eslint-disable-next-line react/jsx-pascal-case */} - + + {/* eslint-disable-next-line react/jsx-pascal-case */} + +

Usage Insights

Last updated: {new Date(analysisLastUpdated).toLocaleDateString()}

@@ -74,32 +76,32 @@ export default function InsightsPage({ pageContext: { tab } }: { pageContext: { > {tab === INSIGHTS_TABS.SUMMARY && ( - + )} {tab === INSIGHTS_TABS.PROJECTS && ( - + )} {tab === INSIGHTS_TABS.COMPONENTS && ( - + )} {tab === INSIGHTS_TABS.HOOKS && ( - + )} {tab === INSIGHTS_TABS.UTILS && ( - + )} {tab === INSIGHTS_TABS.ICONS && ( - + )} diff --git a/www/src/pages/status.tsx b/www/src/pages/status.tsx index 860cf144d73..07710b57cfb 100644 --- a/www/src/pages/status.tsx +++ b/www/src/pages/status.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { StaticQuery, graphql } from 'gatsby'; import { Table, Container } from '~paragon-react'; import { ComponentStatus } from '../components/doc-elements'; import SEO from '../components/SEO'; import Layout from '../components/PageLayout'; +import { SettingsContext } from '../context/SettingsContext'; export interface IComponents { frontmatter?: string, @@ -11,11 +12,13 @@ export interface IComponents { } export default function StatusPage() { + const { settings } = useContext(SettingsContext); + return ( - - - {/* eslint-disable-next-line react/jsx-pascal-case */} - + + {/* eslint-disable-next-line react/jsx-pascal-case */} + +

Library Status

Components Status

diff --git a/www/src/templates/default-mdx-page-template.tsx b/www/src/templates/default-mdx-page-template.tsx index c6739e5af08..7742495ce4c 100644 --- a/www/src/templates/default-mdx-page-template.tsx +++ b/www/src/templates/default-mdx-page-template.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { MDXProvider } from '@mdx-js/react'; import { Link } from 'gatsby'; @@ -7,6 +7,7 @@ import CodeBlock from '../components/CodeBlock'; import Layout from '../components/PageLayout'; import SEO from '../components/SEO'; import LinkedHeading from '../components/LinkedHeading'; +import { SettingsContext } from '../context/SettingsContext'; const shortcodes = { h1: (props: HTMLHeadingElement) => , @@ -32,11 +33,13 @@ export interface IPageTemplateType { } export default function PageTemplate({ children, pageContext }: IPageTemplateType) { + const { settings } = useContext(SettingsContext); + return ( - + {/* eslint-disable-next-line react/jsx-pascal-case */} - + {children}