diff --git a/packages/website/package.json b/packages/website/package.json
index f83efc0a9f..e8404c7d79 100644
--- a/packages/website/package.json
+++ b/packages/website/package.json
@@ -126,6 +126,7 @@
"cache-loader": "^4.1.0",
"compare-versions": "^3.5.1",
"css-loader": "0.23.x",
+ "extend": "^3.0.2",
"glob": "^7.1.4",
"json-stringify-pretty-compact": "^2.0.0",
"less-loader": "^4.1.0",
@@ -149,6 +150,7 @@
"unist-util-find-after": "^2.0.4",
"unist-util-modify-children": "^1.1.4",
"unist-util-select": "^2.0.2",
+ "unist-util-visit": "^2.0.0",
"unist-util-visit-parents": "^3.0.0",
"webpack": "^4.39.2",
"webpack-cli": "3.3.7",
diff --git a/packages/website/ts/components/docs/mdx/headings.tsx b/packages/website/ts/components/docs/mdx/headings.tsx
index 429e374a2f..7f5370a088 100644
--- a/packages/website/ts/components/docs/mdx/headings.tsx
+++ b/packages/website/ts/components/docs/mdx/headings.tsx
@@ -1,7 +1,35 @@
import styled from 'styled-components';
import { Heading } from 'ts/components/text';
-const H1 = styled(Heading).attrs({
+const MDXHeading = styled(Heading)`
+ position: relative;
+
+ &:hover {
+ .heading-link-icon {
+ opacity: 1;
+ }
+ }
+
+ .heading-link-icon {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+
+ position: absolute;
+ transform: translateY(-50%);
+ top: 50%;
+ left: -26px;
+ padding-right: 26px;
+
+ opacity: 0;
+ transition: opacity 200ms ease-in-out;
+
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3M8 12h8'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ }
+`;
+
+const H1 = styled(MDXHeading).attrs({
size: 34,
asElement: 'h1',
marginBottom: '1rem',
@@ -12,32 +40,32 @@ const H1 = styled(Heading).attrs({
}
`;
-const H2 = styled(Heading).attrs({
+const H2 = styled(MDXHeading).attrs({
size: 'default',
asElement: 'h2',
marginBottom: '1rem',
})``;
-const H3 = styled(Heading).attrs({
+const H3 = styled(MDXHeading).attrs({
size: 'small',
asElement: 'h3',
fontWeight: '300',
marginBottom: '1rem',
})``;
-const H4 = styled(Heading).attrs({
+const H4 = styled(MDXHeading).attrs({
asElement: 'h4',
fontWeight: '300',
marginBottom: '1rem',
})``;
-const H5 = styled(Heading).attrs({
+const H5 = styled(MDXHeading).attrs({
asElement: 'h5',
fontWeight: '300',
marginBottom: '1rem',
})``;
-const H6 = styled(Heading).attrs({
+const H6 = styled(MDXHeading).attrs({
asElement: 'h6',
fontWeight: '300',
marginBottom: '1rem',
diff --git a/packages/website/webpack.config.js b/packages/website/webpack.config.js
index a8fcb9f548..536638e016 100644
--- a/packages/website/webpack.config.js
+++ b/packages/website/webpack.config.js
@@ -5,6 +5,7 @@ const TerserPlugin = require('terser-webpack-plugin');
const RollbarSourceMapPlugin = require('rollbar-sourcemap-webpack-plugin');
const childProcess = require('child_process');
const remarkSlug = require('remark-slug');
+const remarkAutolinkHeadings = require('./webpack/remark_autolink_headings');
const remarkSectionizeHeadings = require('./webpack/remark_sectionize_headings');
const mdxTableOfContents = require('./webpack/mdx_table_of_contents');
@@ -65,7 +66,7 @@ const config = {
{
loader: '@mdx-js/loader',
options: {
- remarkPlugins: [remarkSlug, remarkSectionizeHeadings],
+ remarkPlugins: [remarkSlug, remarkAutolinkHeadings, remarkSectionizeHeadings],
compilers: [mdxTableOfContents],
},
},
diff --git a/packages/website/webpack/mdx_table_of_contents.js b/packages/website/webpack/mdx_table_of_contents.js
index 8dfa845902..aa5faa3bd7 100644
--- a/packages/website/webpack/mdx_table_of_contents.js
+++ b/packages/website/webpack/mdx_table_of_contents.js
@@ -61,11 +61,14 @@ function isSlugifiedSection(node) {
}
function toFragment(nodes) {
- if (nodes.length === 1 && nodes[0].type === 'text') {
- return JSON.stringify(nodes[0].value);
- } else {
- return '' + nodes.map(toJSX).join('') + '';
+ // Because of autolinking headings earlier (at remark stage), the headings (nodes)
+ // contain an anchor tag next to the title, we only want to render the text.
+ // Unless there is no text, then we render whatever nodes we get in a Fragment
+ const textNode = nodes.find(node => node.type === 'text');
+ if (textNode) {
+ return JSON.stringify(textNode.value);
}
+ return '' + nodes.map(toJSX).join('') + '';
}
function tableOfContentsListSerializer(nodes, indent = 0) {
diff --git a/packages/website/webpack/remark_autolink_headings.js b/packages/website/webpack/remark_autolink_headings.js
new file mode 100644
index 0000000000..d9000f33e1
--- /dev/null
+++ b/packages/website/webpack/remark_autolink_headings.js
@@ -0,0 +1,45 @@
+const visit = require('unist-util-visit');
+const extend = require('extend');
+
+const content = {
+ type: 'element',
+ tagName: 'i',
+ properties: { className: ['heading-link-icon'] },
+};
+
+const linkProperties = { ariaHidden: 'true' };
+
+const hChildren = Array.isArray(content) ? content : [content];
+
+module.exports = plugin;
+
+function plugin() {
+ return transform;
+}
+
+function transform(tree) {
+ visit(tree, 'heading', visitor);
+}
+
+function visitor(node) {
+ const { data } = node;
+ const id = data && data.hProperties && data.hProperties.id;
+ const url = '#' + id;
+
+ if (id) {
+ inject(node, url);
+ }
+}
+
+function inject(node, url) {
+ node.children.unshift({
+ type: 'link',
+ url,
+ title: null,
+ children: [],
+ data: {
+ hProperties: extend(true, {}, linkProperties),
+ hChildren: extend(true, [], hChildren),
+ },
+ });
+}
diff --git a/yarn.lock b/yarn.lock
index 7bdb5d6d8f..afa58a90d0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8903,9 +8903,10 @@ extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
-extend@~3.0.2:
+extend@^3.0.2, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
external-editor@^2.0.4:
version "2.2.0"
@@ -20723,7 +20724,7 @@ unist-util-visit-parents@^3.0.0:
"@types/unist" "^2.0.3"
unist-util-is "^4.0.0"
-unist-util-visit@2.0.0:
+unist-util-visit@2.0.0, unist-util-visit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.0.tgz#1fdae5ea88251651bfe49b7e84390d664fc227c5"
integrity sha512-kiTpWKsF54u/78L/UU/i7lxrnqGiEWBgqCpaIZBYP0gwUC+Akq0Ajm4U8JiNIoQNfAioBdsyarnOcTEAb9mLeQ==