From 3fd86056af2fb9290a3ca291709c9a83be9bea71 Mon Sep 17 00:00:00 2001 From: Matthieu Bergel Date: Tue, 30 Apr 2024 12:57:31 +0000 Subject: [PATCH] poc: horizontal toc --- site/TableOfContents.tsx | 185 ++++++++++----- site/css/sidebar.scss | 193 +++++++--------- site/gdocs/components/centered-article.scss | 240 ++++++++++---------- site/gdocs/pages/GdocPost.tsx | 2 +- 4 files changed, 327 insertions(+), 293 deletions(-) diff --git a/site/TableOfContents.tsx b/site/TableOfContents.tsx index 1b283bb4f83..5ea87adc144 100644 --- a/site/TableOfContents.tsx +++ b/site/TableOfContents.tsx @@ -1,10 +1,10 @@ -import React, { useState, useEffect, useRef } from "react" +import React, { useState, useEffect, useRef, useCallback } from "react" import ReactDOM from "react-dom" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" -import { faBars, faTimes } from "@fortawesome/free-solid-svg-icons" -import { useTriggerWhenClickOutside } from "./hooks.js" +import { faArrowUp, faBars, faTimes } from "@fortawesome/free-solid-svg-icons" +import { useScrollDirection, useTriggerWhenClickOutside } from "./hooks.js" import { wrapInDiv, TocHeading } from "@ourworldindata/utils" -import classNames from "classnames" +import cx from "classnames" const TOC_WRAPPER_CLASSNAME = "toc-wrapper" @@ -46,23 +46,42 @@ export const TableOfContents = ({ }, }: TableOfContentsData) => { const [isOpen, setIsOpen] = useState(false) - const [activeHeading, setActiveHeading] = useState("") + const [activeHeading, setActiveHeading] = useState(null) const { primary, secondary } = headingLevels const tocRef = useRef(null) const toggleIsOpen = () => { setIsOpen(!isOpen) } + + const close = () => { + setIsOpen(false) + } // The Gdocs sidebar can't rely on the same CSS logic that old-style entries use, so we need to // explicitly trigger these toggles based on screen width - const toggleIsOpenOnMobile = () => { - if (window.innerWidth < 1536) { - toggleIsOpen() - } - } + // const toggleIsOpenOnMobile = () => { + // if (window.innerWidth < 1536) { + // toggleIsOpen() + // } + // } + + const setActiveHeadingFromSlug = useCallback( + (slug: string) => { + // Find the heading with the given slug + const heading = headings.find((h) => h.slug === slug) + if (!heading) { + setActiveHeading(null) + return + } + setActiveHeading(heading) + }, + [headings] + ) useTriggerWhenClickOutside(tocRef, isOpen, setIsOpen) + const scrollDirection = useScrollDirection() + // Open the sidebar on desktop by default when mounting useEffect(() => { setIsOpen(window.innerWidth >= 1536) @@ -94,7 +113,7 @@ export const TableOfContents = ({ ) if (currentHeadingRecord) { - setActiveHeading(currentHeadingRecord.target.id) + setActiveHeadingFromSlug(currentHeadingRecord.target.id) } else { // Target headings going up nextHeadingRecord = records.find( @@ -103,7 +122,7 @@ export const TableOfContents = ({ record.intersectionRatio === 1 ) if (nextHeadingRecord) { - setActiveHeading( + setActiveHeadingFromSlug( getPreviousHeading( nextHeadingRecord, previousHeadings @@ -116,7 +135,7 @@ export const TableOfContents = ({ (record) => record.boundingClientRect.top < 0 ) - setActiveHeading( + setActiveHeadingFromSlug( currentHeadingRecord?.target.id || "" ) } @@ -124,7 +143,7 @@ export const TableOfContents = ({ init = false }, { - rootMargin: "-10px", // 10px offset to trigger intersection when landing exactly at the border when clicking an anchor + rootMargin: "-90px", // 10px offset to trigger intersection when landing exactly at the border when clicking an anchor threshold: new Array(11).fill(0).map((v, i) => i / 10), } ) @@ -149,61 +168,54 @@ export const TableOfContents = ({ return () => observer.disconnect() } return - }, [headings, hideSubheadings, primary, secondary]) + }, [ + headings, + hideSubheadings, + setActiveHeadingFromSlug, + primary, + secondary, + ]) return ( -
+
) diff --git a/site/css/sidebar.scss b/site/css/sidebar.scss index 146f6479eb0..17e9c25ac27 100644 --- a/site/css/sidebar.scss +++ b/site/css/sidebar.scss @@ -1,147 +1,116 @@ /* Page Navigation Sidebar --------------------------------------------- */ -.sidebar-root { - margin-top: -16px; -} .toc-wrapper { - @include xxlg-down { - height: 2rem; // HACK, prevent "Contents" button to run over content when page hasn't been scrolled yet. - margin-bottom: 1rem; - } - @include xxlg-up { - height: 100%; + &.toc-wrapper--sticky { + position: sticky; } + top: 0; + z-index: $zindex-sidebar; } -.entry-sidebar { - width: $sidebar-content-width + 2 * $padding-x-md; - z-index: $zindex-sidebar; - border-right: 1px solid rgba(0, 0, 0, 0.1); - background: $white; +.topic-page-header + .toc-wrapper { + margin-top: -48px; + margin-bottom: 48px; + background-color: $blue-10; +} - .entry-toc { - top: 0; - height: 100vh; - position: sticky; - line-height: 1.3em; - padding: $vertical-spacing 0; - overflow-y: auto; +.centered-article-header + .toc-wrapper { + margin-bottom: 8px; + background-color: $white; + &.toc-wrapper--sticky { + background-color: $blue-10; } +} - li { - list-style-type: none; - &.section { - margin-top: $vertical-spacing; - color: #111; - font-weight: 700; - } +.table-of-contents { + &.table-of-contents--open { + display: flex; + flex-direction: column; + height: 100vh; + } - &.subsection { - font-size: 0.8rem; - } + .toc-header { + display: flex; + align-items: center; + } + .toc-header__active-heading, + .toc-header__page-title { + flex: 1 1 auto; + padding: 8px 0; a { - display: block; - color: $grey-text-color; - font-size: 0.9rem; - border-left: 0.5rem solid transparent; - padding: $vertical-spacing * 0.25 $padding-x-md; - + color: $blue-90; + text-underline-offset: 4px; &:hover { - background-color: $blue-10; + text-decoration: underline; } } + } - &.active a { - color: $blue-100; - background-color: $blue-10; - border-left-color: $oxford-blue; - } - - &:first-child a { - font-size: 1rem; - background: none; - color: $grey-text-color; - border-left-color: transparent; + .toc-header__page-title { + margin: 0; + font-weight: normal; + font-size: 20px; + } - &:hover { - color: $blue-100; - } + .toc-header-button { + margin: 8px 0 8px 16px; + padding: 8px 16px; + @include body-3-medium; + background: transparent; + color: $blue-90; + border: 1px solid $blue-90; + cursor: pointer; + + &:hover { + color: $blue-60; + border-color: $blue-60; } } - @include xxlg-down { - position: absolute; - top: 0; - bottom: 0; - margin-left: -($sidebar-content-width + 2 * $padding-x-md) + $sidebar-closed-drawer-width; - @include sm-only { - margin-left: -($sidebar-content-width + 2 * $padding-x-md); + .toc-header-button--toggle { + flex: 0 0 auto; + .toc-header-button__label { + margin-left: 8px; } - transition: margin 300ms ease; - - .toggle-toc { - position: absolute; - top: 0; - bottom: 0; - right: 0; - transform: translateX(calc(100% + $padding-x-md)); - transition: transform 300ms ease; - padding: $vertical-spacing 0; - pointer-events: none; - - @include md-down { - transform: translateX(calc(100% + $padding-x-sm)); - } - - @include md-up { - margin-left: $padding-x-md; - } - button { - @include popover-box-button; - z-index: $zindex-sidebar; - position: sticky; - top: $vertical-spacing; - pointer-events: auto; - white-space: nowrap; + @include sm-only { + .toc-header-button__label--collapsed-sm { + display: none; } } + } - ul { - // Hide ToC content for smoother looking transition - display: none; - } - - &.entry-sidebar--is-open { - margin-left: 0; - @include block-shadow; + .toc-nav { + overflow-y: scroll; + flex: 1; + margin-bottom: 8px; - .toggle-toc { - transform: translateX(-16px); + li { + list-style-type: none; + border-left: 4px solid transparent; - @include sm-only { - svg { - margin: 0 0.25rem; - } - .label { - display: none; - } - } + &.subsection { + margin-left: 16px; } - ul { - display: block; + &.active { + border-left-color: $oxford-blue; + a { + color: $blue-90; + } } } - } - - @include xxlg-up { - position: relative; - height: 100%; + a { + display: block; + padding: 8px; + color: $blue-60; + text-underline-offset: 4px; - .toggle-toc { - display: none; + &:hover { + color: $blue-90; + text-decoration: underline; + } } } } diff --git a/site/gdocs/components/centered-article.scss b/site/gdocs/components/centered-article.scss index cc065014de3..a2d604b7cc8 100644 --- a/site/gdocs/components/centered-article.scss +++ b/site/gdocs/components/centered-article.scss @@ -31,124 +31,128 @@ } } - .toc-wrapper { - position: sticky; - top: 0; - height: 0; - // Above explorer chrome - z-index: 3; - margin-top: -48px; - .entry-sidebar { - height: 100vh; - position: absolute; - transition: margin 300ms ease; - width: 400px; - margin-left: -400px; - box-shadow: none; - @include sm-only { - width: 100vw; - margin-left: -100vw; - } - @include sm-up { - ul { - margin-left: 32px; - } - } - - li { - &:first-child { - margin-top: 36px; - } - - &.section { - margin-top: 20px; - } - &.subsection a { - color: $blue-60; - margin-left: 16px; - line-height: 1.125em; - } - &.active a { - border-left-color: $vermillion; - background: unset; - font-weight: bold; - // Counteract the font-weight so that the text doesn't wrap when active - letter-spacing: -0.09px; - } - a { - padding-left: 16px; - color: $blue-90; - border-width: 4px; - padding-right: 32px; - margin-left: 0; - font-weight: 400; - - &:hover { - background: none; - text-decoration: underline; - } - } - } - - .toggle-toc { - margin-left: 0; - transform: translateX(calc(100% + 16px)); - position: absolute; - top: 0; - bottom: 0; - right: 0; - padding: 16px 0; - pointer-events: none; - display: unset; - transition: transform 300ms ease; - button { - @include popover-box-button; - z-index: 20; - position: sticky; - top: 16px; - pointer-events: auto; - white-space: nowrap; - box-shadow: none; - background: #fff; - border: 1px solid $blue-20; - line-height: 1.25; - padding: 6px; - border-radius: 4px; - - &:hover { - background: #fff; - svg { - color: $blue-100; - } - } - svg { - margin-right: 0; - color: $blue-90; - height: 12px; - } - - span { - color: $blue-90; - margin-left: 5px; - position: relative; - top: 1px; - } - } - } - &.entry-sidebar--is-open { - margin-left: 0; - .toggle-toc { - transform: translateX(-16px); - button { - border: none; - span { - display: none; - } - } - } - } - } - } + [id] { + scroll-margin-top: 80px; + } + + // .toc-wrapper { + // position: sticky; + // top: 0; + // height: 0; + // // Above explorer chrome + // z-index: 3; + // margin-top: -48px; + // .entry-sidebar { + // height: 100vh; + // position: absolute; + // transition: margin 300ms ease; + // width: 400px; + // margin-left: -400px; + // box-shadow: none; + // @include sm-only { + // width: 100vw; + // margin-left: -100vw; + // } + // @include sm-up { + // ul { + // margin-left: 32px; + // } + // } + + // li { + // &:first-child { + // margin-top: 36px; + // } + + // &.section { + // margin-top: 20px; + // } + // &.subsection a { + // color: $blue-60; + // margin-left: 16px; + // line-height: 1.125em; + // } + // &.active a { + // border-left-color: $vermillion; + // background: unset; + // font-weight: bold; + // // Counteract the font-weight so that the text doesn't wrap when active + // letter-spacing: -0.09px; + // } + // a { + // padding-left: 16px; + // color: $blue-90; + // border-width: 4px; + // padding-right: 32px; + // margin-left: 0; + // font-weight: 400; + + // &:hover { + // background: none; + // text-decoration: underline; + // } + // } + // } + + // .toggle-toc { + // margin-left: 0; + // transform: translateX(calc(100% + 16px)); + // position: absolute; + // top: 0; + // bottom: 0; + // right: 0; + // padding: 16px 0; + // pointer-events: none; + // display: unset; + // transition: transform 300ms ease; + // button { + // @include popover-box-button; + // z-index: 20; + // position: sticky; + // top: 16px; + // pointer-events: auto; + // white-space: nowrap; + // box-shadow: none; + // background: #fff; + // border: 1px solid $blue-20; + // line-height: 1.25; + // padding: 6px; + // border-radius: 4px; + + // &:hover { + // background: #fff; + // svg { + // color: $blue-100; + // } + // } + // svg { + // margin-right: 0; + // color: $blue-90; + // height: 12px; + // } + + // span { + // color: $blue-90; + // margin-left: 5px; + // position: relative; + // top: 1px; + // } + // } + // } + // &.entry-sidebar--is-open { + // margin-left: 0; + // .toggle-toc { + // transform: translateX(-16px); + // button { + // border: none; + // span { + // display: none; + // } + // } + // } + // } + // } + // } } $banner-height: 200px; diff --git a/site/gdocs/pages/GdocPost.tsx b/site/gdocs/pages/GdocPost.tsx index 40d302d7dc8..b08d7a083f3 100644 --- a/site/gdocs/pages/GdocPost.tsx +++ b/site/gdocs/pages/GdocPost.tsx @@ -87,7 +87,7 @@ export function GdocPost({ publishedAt={publishedAt} breadcrumbs={breadcrumbs ?? undefined} /> - {hasSidebarToc && content.toc ? ( + {content.toc ? (