Skip to content

Commit

Permalink
Add heading anchors to @keystone-next/website (#4838)
Browse files Browse the repository at this point in the history
* add anchors and remove backticks from headings
* add click to copy functionality to anchors
* remove unnecessary imports

Co-authored-by: Jed Watson <[email protected]>
  • Loading branch information
gwyneplaine and JedWatson authored Feb 15, 2021
1 parent b224d47 commit f4163a0
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-parents-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/website': patch
---

Remove backticks from headings in docs
5 changes: 5 additions & 0 deletions .changeset/seven-icons-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/website': minor
---

Added anchoring and copy to clipboard functionality to headings
69 changes: 69 additions & 0 deletions docs-next/components/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/** @jsx jsx */
import { jsx } from '@keystone-ui/core';
import copy from 'copy-to-clipboard';
import { LinkIcon } from '@primer/octicons-react';
import { useToasts } from '@keystone-ui/toast';

export const CopyToClipboard = ({ value }: { value: string }) => {
const iconSize = 24; // arch-ui theme
const gridSize = 8; // arch-ui theme
const { addToast } = useToasts();

const onSuccess = () => {
addToast({ title: 'Copied to clipboard', tone: 'positive' });
};
const onFailure = () => {
addToast({ title: 'Faild to oopy to clipboard', tone: 'negative' });
};
const onClick = (event: React.SyntheticEvent) => {
event.preventDefault();
if (typeof value !== 'string' || typeof window === 'undefined') return;
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
const text = `${url}#${value}`;
if (navigator) {
// use the new navigator.clipboard API if it exists
navigator.clipboard.writeText(text).then(onSuccess, onFailure);
return;
} else {
// Fallback to a library that leverages document.execCommand
// for browser versions that dont' support the navigator object.
// As document.execCommand
try {
copy(text);
} catch (e) {
addToast({ title: 'Faild to oopy to clipboard', tone: 'negative' });
}

return;
}
};

return (
<a
href={`#`}
css={{
alignItems: 'center',
color: '#97A0AF', // colors.n40
display: 'flex',
fontSize: '1rem',
height: iconSize,
justifyContent: 'center',
marginTop: -iconSize / 2,
opacity: 0,
overflow: 'visible',
paddingRight: gridSize / 2,
position: 'absolute',
top: '50%',
transform: 'translateX(-100%)',
width: iconSize,

'&:hover': {
color: '#2684FF', // colors.primary
},
}}
onClick={onClick}
>
<LinkIcon />
</a>
);
};
125 changes: 125 additions & 0 deletions docs-next/components/Heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/** @jsx jsx */
import { useRef } from 'react';
import { jsx } from '@keystone-ui/core';
import slugify from '@sindresorhus/slugify';
import { CopyToClipboard } from './CopyToClipboard';

// offset header height for hash links
const hashLinkOffset = {
'::before': {
content: '" "',
height: 'calc(32px + 60px)',
display: 'block',
marginTop: -60,
},
};
const getAnchor = (text: string) => {
if (typeof text === 'string') {
return slugify(text);
}
return '';
};

// emotions JSX pragma appends the correct css prop type
// if the underlying component expects an optional className prop
interface StringOnlyChildren {
children: string;
className?: string;
}

interface HeadingProps extends StringOnlyChildren {
as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
}
const Heading = ({ as: Tag, children, ...props }: HeadingProps) => {
const headingRef = useRef(null);
const depth = parseInt(Tag.slice(1), 10);
const hasCopy = depth > 1 && depth < 5;
const id = getAnchor(children);
return (
<Tag
css={{
color: '#091E42', //N100 in arch
fontWeight: 600,
lineHeight: 1,
marginBottom: '0.66em',
marginTop: '1.66em',
}}
id={id}
{...props}
>
<span
ref={headingRef}
css={{
display: 'block',
position: 'relative',

'&:hover a, &:focus-within a': {
opacity: 1,
},
}}
>
{hasCopy && <CopyToClipboard value={id} />}
{children}
</span>
</Tag>
);
};

export const H1 = (props: StringOnlyChildren) => (
<Heading
css={{
fontSize: '2.4rem',
fontWeight: 500,
letterSpacing: '-0.025em',
marginTop: 0,
}}
{...props}
as="h1"
/>
);
export const H2 = (props: StringOnlyChildren) => (
<Heading
{...props}
css={{
fontSize: '1.8rem',
fontWeight: 500,
letterSpacing: '-0.025em',
marginTop: 0,
...hashLinkOffset,
}}
as="h2"
/>
);
export const H3 = (props: StringOnlyChildren) => (
<Heading
css={{
fontSize: '1.4rem',
fontWeight: 500,
letterSpacing: '-0.025em',
marginTop: 0,
...hashLinkOffset,
}}
{...props}
as="h3"
/>
);

export const H4 = (props: StringOnlyChildren) => (
<Heading
css={{
fontSize: '1.2rem',
...hashLinkOffset,
}}
{...props}
as="h4"
/>
);

export const H5 = (props: StringOnlyChildren) => (
<Heading css={{ fontSize: '1rem' }} as="h5" {...props} />
);
export const H6 = (props: StringOnlyChildren) => (
<Heading css={{ fontSize: '0.9rem' }} as="h6" {...props} />
);

export default Heading;
12 changes: 10 additions & 2 deletions docs-next/components/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { ReactNode } from 'react';
/** @jsx jsx */
import { ReactNode } from 'react';
import { jsx } from '@keystone-ui/core';
import { Code } from '../components/Code';
import { H1, H2, H3, H4, H5, H6 } from '../components/Heading';
import { MDXProvider } from '@mdx-js/react';
import cx from 'classnames';
import Link from 'next/link';

import { Navigation } from './Navigation';

export const Page = ({ children, isProse }: { children: ReactNode; isProse?: boolean }) => {
Expand Down Expand Up @@ -67,6 +69,12 @@ export const Page = ({ children, isProse }: { children: ReactNode; isProse?: boo

export const components = {
code: Code,
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6,
};

export const Markdown = ({ children }: { children: ReactNode }) => (
Expand Down
4 changes: 4 additions & 0 deletions docs-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
"@keystone-ui/fields": "^2.0.0",
"@keystone-ui/icons": "^2.0.0",
"@keystone-ui/notice": "^2.0.0",
"@keystone-ui/toast": "^2.0.0",
"@keystone-ui/tooltip": "^2.0.0",
"@mdx-js/loader": "^1.6.22",
"@mdx-js/react": "^1.6.22",
"@next/mdx": "^10.0.6",
"@preconstruct/next": "^2.0.0",
"@primer/octicons-react": "^11.3.0",
"@sindresorhus/slugify": "^1.1.0",
"@tailwindcss/typography": "^0.4.0",
"@types/classnames": "^2.2.11",
"@types/gtag.js": "^0.0.4",
Expand All @@ -28,6 +31,7 @@
"@types/webpack": "^4.41.26",
"autoprefixer": "^10.2.4",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.3.1",
"next": "10.0.5",
"next-compose-plugins": "^2.2.1",
"postcss": "^8.2.5",
Expand Down
13 changes: 11 additions & 2 deletions docs-next/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { useEffect } from 'react';
/** @jsx jsx */
import { jsx, Core } from '@keystone-ui/core';
import { useEffect } from 'react';
import type { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { ToastProvider } from '@keystone-ui/toast';

import { handleRouteChange } from '../lib/analytics';

Expand All @@ -16,5 +19,11 @@ export default function App({ Component, pageProps }: AppProps) {
};
}, [router.events]);

return <Component {...pageProps} />;
return (
<Core>
<ToastProvider>
<Component {...pageProps} />
</ToastProvider>
</Core>
);
}
Loading

1 comment on commit f4163a0

@vercel
Copy link

@vercel vercel bot commented on f4163a0 Feb 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.