最初に
このコードは2024-09-23時点でSvelte5がリリース前のため、Svelte4で書いています。
Svelte5でも少し記述を変更するとそのまま動きます。
見出しレベル
見出しレベルですが、特に「コンポーネント内部で見出しタグが使用されている場合」などで想定外の場所で使われてしまってよくズレます。
思わぬ場所で使われることは避けられません。
また、そうなった場合おそらく「このコンポーネントが表示されたのでOK!」になってしまうことが多く、レビューで指摘されないことも多いと思います。
人の手で見出しレベルを手書きしてしまうことが原因の一つにあるので、そもそも人の手で書かないようにしましょう。
リポジトリ
説明
簡単に説明します。
Section.svelte
<script context="module" lang="ts">
import { getContext, setContext } from 'svelte';
import Heading from './Heading.svelte';
const KEY = Symbol('heading');
const setHeadingContext = (v: number) => setContext(KEY, v);
export const getHeadingContext = () => getContext<number>(KEY);
</script>
<script>
let level = getHeadingContext();
$: if ($$slots.heading) {
setHeadingContext(level ? level + 1 : 2);
}
</script>
<section>
{#if $$slots.heading}
<Heading>
<slot name="heading" />
</Heading>
{/if}
<slot />
</section>
getHeadingContext()
でContextから値(見出しレベルのnumber)を取得します。
さらに、値があればそれに+1した値をsetContextします。
sectionタグ
マークアップに関してはsectionタグをそのまま使っても特にマークアップ上メリットはないのですが、
どこからどこまでが一まとまりなのかソースを読む時すごくわかりやすくなるので筆者は積極的に使っています。
HeadingコンポーネントはContextで取得できた値を使って見出しレベルをインクリメントしているため、
必ずSectionコンポーネントで囲わないと見出しレベルを自動でインクリメントできません。
そのため、Sectionコンポーネント内でHeadingコンポーネントを使うようにします。
(Sectionコンポーネント外でHeadingコンポーネントを使われてしまうとインクリメントできない問題があるので、ここは後ほど縛ります。)
このとき、見出しがない場合はインクリメントするとずれてしまうのでインクリメントしないようにif文で囲っています。
\$: if ($$slots.heading) {
setHeadingContext(level ? level + 1 : 2);
}
Heading.svelte
<script lang="ts">
import { getHeadingContext } from './Section.svelte';
let level = getHeadingContext();
$: tag = level ? `h${level}` : 'h2';
</script>
<svelte:element this="{tag}">
<slot />
</svelte:element>
getHeadingContext()
でContextから値(見出しレベルのnumber)を取得します。
このnumberにh
を連結すれば見出しタグ名になるので、あとはsvelte:element
のthis
に渡すだけです。
h7
までいくことは…多分ないでしょう。
もしあるようであればレベルに上限を設けるか、aria-level
を使ってください。
そのレベルまで実装する意味はあまりないのでh6
までを上限としてもいいと思います。
使い方
<script>
import Section from '$lib/Section.svelte';
</script>
<h1>見出し1</h1>
<Section>
<svelte:fragment slot="heading">見出し2</svelte:fragment>
<ul>
<li>リスト</li>
</ul>
<Section>
<svelte:fragment slot="heading">見出し3</svelte:fragment>
<Section>
<svelte:fragment slot="heading">見出し4</svelte:fragment>
</Section>
</Section>
<Section>
<svelte:fragment slot="heading">見出し3</svelte:fragment>
<Section>
<svelte:fragment slot="heading">見出し4</svelte:fragment>
</Section>
</Section>
</Section>
<Section>
<svelte:fragment slot="heading">見出し2</svelte:fragment>
<Section>
<Section>
<svelte:fragment slot="heading">見出し3</svelte:fragment>
見出しがないSectionを作ってしまってもレベルは ずれない
</Section>
</Section>
<Section>
<svelte:fragment slot="heading">見出し3</svelte:fragment>
</Section>
</Section>
こんな感じです。簡単ですね!
見出しタグを禁止する
このままではh{number}
タグを自由に使えてしまいます。
見出しタグを使われてしまうとわざわざSectionやHeadingコンポーネントを作った意味がないですね…レビューで漏れることが問題だったのでこれだとちょっと困ります。
ということで見出しタグを禁止してしまいましょう。
eslint-plugin-svelte
にsvelte/no-restricted-html-elements
というruleがあるので、こちらに見出しタグを指定し、エラーメッセージに「Sectionコンポーネントのslot="heading"を使用してください。」という文言を追加します。
export default [
...svelteConfig,
{
rules: {
'svelte/no-restricted-html-elements': [
'error',
{
elements: ['h2', 'h3', 'h4', 'h5', 'h6'],
message:
'Sectionコンポーネントのslot="heading"を使用してください。',
},
],
},
},
];
これで見出しタグは使えなくなりました。
🤖< h
タグがあればいいのに…
あと一つ、HeadingコンポーネントをSectionコンポーネント以外で使われると困ります。
こちらはMarkuplintで縛ってしまいましょう。
{
"parser": {
".svelte$": "@markuplint/svelte-parser",
".html$": "@markuplint/svelte-parser/kit"
},
"specs": {
".svelte$": "@markuplint/svelte-spec"
},
"rules": {
"disallowed-element": [
"Heading"
]
},
"overrides": {
"./src/lib/Section.svelte": {
"rules": {
"disallowed-element": []
}
}
}
}
まずdisallowed-element
でHeadingタグを禁止します。
これにより、アプリケーション全体でHeadingコンポーネントが使用できなくなります。
これだと困るので、Sectionコンポーネント内でのみ使用を許可するようにoverrides
します。
Section的なコンポーネントはもう少し柔軟にコンテンツを表示できるようにして、追加しないようにした方がいいでしょう。
まとめ
今回紹介したコードは以下のリポジトリにあります。参考にしてみてください。
https://github.com/shamokit/section-heading-component
見出しレベルを自分で書いてミスるくらいなら自分で書かないようにしましょう。