astro-notion-blogの目次を折りたためるようにする

Featured image of the post

目次

↑これが完成品です。

まえがき

トグル見出しのCSSを考えて試行錯誤していたところ、detailsとsummaryを使えばJavaScript無しでアコーディオンを作れると知りました。

作りたい目次の形は前々から考えていたので、覚えた知識を早速使って作ってみました。

完成した目次の確認用に見出し多めでお届けします。

要件定義

JavaScript不使用

CSSだけで作れるのでそうします。JavaScriptを使った目次はサイドバーに搭載したいと考えている「現在地を追従してハイライトする目次」で使えばいいので、今回はとにかく軽くてシンプルにします。

JavaScriptにまだまだ自信がないので避けているだけではありません。

アニメーション不使用

開閉するだけなのでアニメーションは不要です。

Notionの目次ブロックのカスタマイズに留める

これはNotionの目次ブロックをそのまま使うものです。互換性を保ちます。文章の途中に入れても同じものが生成されます。

左寄せを中央に配置

目次の箱自体は中央寄せにして、その中に左寄せの見出しテキスト郡を配置します。見れば分かります。よくあるやつです。

アイコンで開閉の状態を表示

astro-Iconで開閉の状態が分かるようにします。

マウスの当たり判定は端まで隙間なく

神は細部に宿ると信じます。

完成品↓

目次

目次は同期ブロックで全記事の最初に挿入してあるので今更感ありますね。デフォルトでは開いており、クリック(タップ)で閉じます。

こだわりポイント

見出しのテキストは端の端まで当たり判定あり

こだわったと言うほどでもないことですが、端まで伸ばし切ってあります。

Image in a image block

見出しレベルに合わせてインデントを設定してありますが、テキストだけに当たり判定があるとカーソル移動が面倒くさいため、雑にマウスを動かしてもどれが選ばれているのか分かるようになっています。

そして折り返すほど長い見出し文章だと右で折り返すのですが、折り返す際の端の隙間は見出しレベルに関係なく一定です。

こういうのって作らないと意識しないので面白いですね。

summaryの下線を打ち消すpaddingでガタツキ防止

目次の開閉に応じてsummaryに下線を足しているのですが、その1pxが悪さをしているのか、開閉時に1pxのガタツキがありました。

Image in a image block
「目次」の文字とアイコンが上下に動いています

打ち消すにはpaddingだろうと思って同じ1pxを追加したら上手くいきました。

微動だにしません。

Image in a image block
文字もアイコンも動かなくなりました

ソース

src\components\notion-blocks\TableOfContents.astro

---
import * as interfaces from '../../lib/interfaces.ts'
import { buildHeadingId } from '../../lib/blog-helpers.ts'
import { snakeToKebab } from '../../lib/style-helpers.ts'
import '../../styles/notion-color.css'
import { Icon } from 'astro-icon'

export interface Props {
  block: interfaces.Block
  headings: interfaces.Block[]
}

const { block, headings } = Astro.props
---

<details class="toggle" open>
  <summary>
    目次
    <Icon name="ph:plus" />
    <Icon name="ph:minus" />
  </summary>
  <div class="table-of-contents">
    {
      headings.map((headingBlock: interfaces.Block) => {
        const heading =
          headingBlock.Heading1 ||
          headingBlock.Heading2 ||
          headingBlock.Heading3

        let indentClass = ''
        if (headingBlock.Type === 'heading_2') {
          indentClass = 'indent-1'
        } else if (headingBlock.Type === 'heading_3') {
          indentClass = 'indent-2'
        }

        return (
          <a
            href={`#${buildHeadingId(heading)}`}
            class={`table-of-contents ${snakeToKebab(
              block.TableOfContents.Color
            )} ${indentClass}`}
          >
            {heading.RichTexts.map(
              (richText: interfaces.RichText) => richText.PlainText
            ).join('')}
          </a>
        )
      })
    }
  </div>
</details>

<style>
  .toggle {
    display: block;
    width: 80%;
    margin-inline-start: auto;
    margin-inline-end: auto;
    margin-block-end: 1rem;
    border-radius: 0.5rem;
    border: 1px solid grey;
  }
  @media (max-width: 844px) {
    .toggle {
      width: 100%;
    }
  }
  .toggle > summary {
    font-size: 1rem;
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 4rem;
    position: relative;
    &::-webkit-details-marker {
      display: none;
    }
  }
  @media (hover: hover) {
    .toggle > summary:hover {
      text-decoration: underline;
    }
  }
  .toggle[open] > summary {
    border-block-end: 1px solid grey;
    padding-block-start: 1px;
  }
  .toggle > summary > [astro-icon] {
    position: absolute;
    height: 2rem;
    left: 100%;
    translate: -120%;
    &:first-child {
      display: block;
    }
    &:not(:first-child) {
      display: none;
    }
  }
  .toggle[open] > summary > [astro-icon] {
    &:first-child {
      display: none;
    }
    &:not(:first-child) {
      display: block;
    }
  }
  .table-of-contents {
    margin-block-start: 1rem;
    margin-block-end: 1rem;
  }
  .table-of-contents > a {
    display: block;
    font-size: 1rem;
    color: var(--fg);
    font-weight: 600;
    line-height: 1.8rem;
    margin: 0;
    padding-inline-start: 1rem;
    padding-inline-end: 1rem;
  }
  @media (hover: hover) {
    .table-of-contents > a:hover {
      text-decoration: underline;
      text-underline-offset: 0.1rem;
    }
  }
  .table-of-contents > a.indent-1 {
    font-size: 0.9rem;
    line-height: 1.6rem;
    font-weight: 400;
    padding-inline-start: 1.5rem;
  }
  .table-of-contents > a.indent-2 {
    font-size: 0.8rem;
    line-height: 1.4rem;
    font-weight: normal;
    padding-inline-start: 3rem;
  }
</style>
src\components\notion-blocks\TableOfContents.astro

HTML部分は殆ど変更することなく作り変えることができました。