検索

Accordion

このページは現在調整中です
Lism UI (@lism-css/ui) はまだ準備中です。

アコーディオン要素を作成できるUIコンポーネントです。

実装方法について
  • details/summary を採用。
  • grid1fr を使ったトランジションを採用。

基本構造

details.d--accordion
summary.d--accordion_header
span.d--accordion_label ...Header label...
span.d--accordion_icon
svg.a--icon
div.d--accordion_body
div.d--accordion_inner.l--flow ...Contents...

CSS

_style.css
@layer lism.modules {
	.d--accordion {
		--duration: var(--acc-duration, 0.4s);
	}
	.d--accordion[data-opened] {
		--_notOpen: ;
	}
	.d--accordion:not([data-opened]) {
		--_isOpen: ;
	}

	.d--accordion_header {
		display: grid;
		grid: auto / 1fr auto;
		gap: 0.5em;
		align-items: center;
		outline-offset: -1px; /* overflow:clip|hidden; で見えなくなってしまうのを防ぐ */

		/* Safariで表示されるデフォルトの三角形アイコンを消す */
		&::-webkit-details-marker {
			display: none;
		}
	}

	.d--accordion_body {
		display: grid;
		grid: 1fr / auto;
		transition-property: margin-block, padding-block, opacity, grid-template;
		transition-duration: var(--duration);
	}

	/* ※ 正常な animation には必須 */
	.d--accordion_inner {
		overflow: hidden;
	}

	/* 閉じている時 */
	.d--accordion:not([data-opened]) > .d--accordion_body {
		grid: 0fr / auto;
		padding-block: 0 !important;
		margin-block: 0 !important;
	}

	/* アコーディオンブロックのネスト時、別のアイコンタイプにすると表示が崩れるがそこまでは考慮しない。 */
	.d--accordion_icon {
		display: grid;
	}

	/* .d--accordion_icon 自体にborderつけたりすると回転が見えてしまうので、中にある .a--icon を回転させる。 */
	.d--accordion_icon .a--icon {
		transition-duration: var(--duration);
		rotate: var(--_isOpen, -180deg);
	}
}

JavaScript

setAccordion.js
// open 属性付与からクラスの付与まで、ほんの少しだけ遅らせた方が動作が安定する
const DELAY = 5;

// モーダルのアニメーションが完了するのを待つ.
const waitAnimation = (element) => {
	return Promise.all(element.getAnimations().map((a) => a.finished));
};

// animationTime: [ms]
const clickedEvent = async (details, force = false) => {
	// アニメーション中かどうか
	if (details.dataset.animating && !force) return;
	details.dataset.animating = '1';

	const body = details.querySelector('.d--accordion_body');

	// オープン / クローズ 処理
	if (!details.open) {
		details.open = true;
		// 少しだけ遅らせた方が動作が安定する
		setTimeout(async () => {
			details.setAttribute('data-opened', ''); // クラスの追加

			// アニメーション完了後に dataset を除去。
			await waitAnimation(body);
			delete details.dataset.animating;
		}, DELAY);
	} else if (details.open) {
		details.removeAttribute('data-opened'); // クラスを削除

		// アニメーション完了後に open属性 を除去。
		await waitAnimation(body);

		delete details.dataset.animating;
		details.open = false;
	}
};

const toggleEvent = (e, details) => {
	// e.preventDefault();
	// console.log('toggleEvent', e.target, e.currentTarget);

	const hasOpen = details.open;
	const hasOpenedClass = details.hasAttribute('data-opened');

	// open はセットされたのに data-opened がついてない時
	if (hasOpen && !hasOpenedClass) {
		details.setAttribute('data-opened', '');
	}
	// open は削除されたのに data-opened がまだついている時
	if (!hasOpen && hasOpenedClass) {
		details.removeAttribute('data-opened');
	}
};

export const setEvent = (currentRef) => {
	const details = currentRef;
	// トリガーが明示的に指定されていない場合は、<summary> 要素をトリガーとする
	const clickBtn = details.querySelector(`[data-role="trigger"]`) || details.querySelector('summary');

	if (!clickBtn) return;

	// 複数展開を許可するかどうかを、親要素の [data-accordion-multiple] でチェック.
	let allowMultiple = false;
	const parent = details.parentNode;
	if (null != parent) {
		const dataMultiple = parent.dataset.accordionMultiple;
		allowMultiple = 'disallow' !== dataMultiple;
	}

	const _clickedEvent = (e) => {
		// すぐに open 属性が切り替わらないようにする
		e.preventDefault();

		// 複数展開が禁止されている場合、(開く処理の直前で)他の開いているアイテムがあれば閉じる
		if (!allowMultiple && !details.open) {
			const openedItem = parent.querySelector(`[data-opened]`);
			if (null != openedItem) clickedEvent(openedItem, true);
		}

		// 自身のクリック処理
		clickedEvent(details);
	};
	const _toggleEvent = (e) => {
		toggleEvent(e, details);
	};

	// <summary> 'click' イベント
	clickBtn.addEventListener('click', _clickedEvent);

	// <details> の'toggle' イベントで、ページ内検索時にも開閉されるようにする
	details.addEventListener('toggle', _toggleEvent);

	// useEffectでアンマウントされた時にremoveEventListenerしないと2重でイベントが登録してしまう。
	return () => {
		clickBtn.removeEventListener('click', _clickedEvent);
		details.removeEventListener('toggle', _toggleEvent);
	};
};

const setAccordion = () => {
	const detailsAll = document.querySelectorAll('.d--accordion');
	detailsAll.forEach((details) => {
		setEvent(details);
	});
};
export default setAccordion;

@lism-css/ui パッケージで提供しています。

Import

import { Accordion } from '@lism-css/ui/react';

以下のコンポーネントが利用できます。

  • <Accordion.Root>
  • <Accordion.Header>
  • <Accordion.Label>
  • <Accordion.Icon>
  • <Accordion.Body>

また、.HeaderLabel を使うと、.Header, .Label, .Icon をまとめて配置できます。

<Accordion.HeaderLabel {...props}>Text</Accordion.HeaderLabel>
↓ これは以下のように内部で展開されます
<Accordion.Header {...props}>
<Accordion.Label>Text</Accordion.Label>
<Accordion.Icon/>
</Accordion.Header>

Props

プロパティ説明
<Accordion.Icon>
icon
内部で呼び出される<Icon> に渡す icon を指定できます。ただし、<Accordion.Icon>に子要素が配置されている場合は無視されます。
<Accordion.Icon>
isTrigger
アイコンが buttonタグでの出力となり[data-role="trigger"]が付与されます。
<Accordion.Body>
flow
__innerに渡されます。

Usage

Preview
Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

<Accordion.Root p='15'>
<Accordion.HeaderLabel>Accordion Label</Accordion.HeaderLabel>
<Accordion.Body my-s='15'>
<Dummy length='l' />
</Accordion.Body>
</Accordion.Root>
ラベルのHTMLタグをh3タグにする

Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

<Accordion.Root p='15'>
<Accordion.Header>
<Accordion.Label tag='h3' f='inherit' fw='bold'>Accordion Label</Accordion.Label>
<Accordion.Icon/>
</Accordion.Header>
<Accordion.Body my-s='15'>
<Dummy length='l' />
</Accordion.Body>
</Accordion.Root>

アイコンを変更する

<Accordion.Label>に指定するiconは、内部で<Icon>に渡されます。

外部コンポーネントを指定する
Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

import { CaretDoubleDown } from "@phosphor-icons/react";
<Accordion.Root p='15'>
<Accordion.Header>
<Accordion.Label>Accordion Label</Accordion.Label>
<Accordion.Icon icon={CaretDoubleDown}/>
</Accordion.Header>
<Accordion.Body my-s='15'>
<Dummy length='l' />
</Accordion.Body>
</Accordion.Root>

アコーディオンの同時開閉を禁止する

複数の<Accordion.Root>(.d--accordion)を含む親要素に [data-accordion-multiple="disallow"]を指定すると、複数のアコーディオンを同時に開くことを禁止し、どれかが開くとその兄弟アコーディオンを閉じるようになります。

Preview
Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint. Occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis undeomnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam.

Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

<Stack g='1px' data-accordion-multiple='disallow'>
<Accordion.Root>
<Accordion.HeaderLabel p='20' bgc='text' c='base'>Accordion Label</Accordion.HeaderLabel>
<Accordion.Body p='20'>
<Dummy length='l' />
</Accordion.Body>
</Accordion.Root>
<Accordion.Root>
<Accordion.HeaderLabel p='20' bgc='text' c='base'>Accordion Label</Accordion.HeaderLabel>
<Accordion.Body p='20'>
<Dummy length='xl' />
</Accordion.Body>
</Accordion.Root>
...
</Stack>

開閉トランジションの時間を変更する

アコーディオン用のトランジションは--acc-durationで秒数を管理できます。

親要素に変数をセットすると一括管理できます。

Preview
Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

<Stack g='20' style={{'--acc-duration': '.25s'}}>
<Accordion.Root bgc='base-2' bxsh='10' bdrs='20'>
<Accordion.HeaderLabel p='20'>Accordion Label</Accordion.HeaderLabel>
<Accordion.Body p='20' bd-t>
<Dummy length='l' />
</Accordion.Body>
</Accordion.Root>
<Accordion.Root bgc='base-2' bxsh='10' bdrs='20'>
<Accordion.HeaderLabel p='20'>Accordion Label</Accordion.HeaderLabel>
<Accordion.Body p='20' bd-t>
<Dummy length='l' />
</Accordion.Body>
</Accordion.Root>
...
</Stack>

CSSで:root--acc-durationを上書きすると、デフォルトの開閉速度を変更できます。

Opt-in 機能

追加でCSSが必要な機能

開閉時のアイコンを分ける

追加で少しCSSを加える必要がありますが、開閉時にアイコンを切り替える例も紹介します。

Example
Accordion Label

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut. Aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint.

<Accordion.Root p='15'>
<Accordion.Header>
<Accordion.Label>Accordion Label</Accordion.Label>
<Accordion.Icon>
<Icon setTransition ga='1/1' icon={{as:Plus, weight:'bold'}} />
<Icon setTransition ga='1/1' icon={{as:Minus, weight:'bold'}} />
</Accordion.Icon>
</Accordion.Header>
<Accordion.Body my-s='15'>
<Dummy length='l' />
</Accordion.Body>
</Accordion.Root>

アコーディオントリガーをアイコンのみにする

<Accordion.Icon>isTrigger を指定すると、buttonタグでの出力となり[data-role="trigger"]が付与されます。
この時、アコーディオンを開閉するためのclickイベントは summary(アコーディオンヘッダー)全体ではなくアイコン部分に対して登録されます。

これにより、アコーディオンヘッダー内部のテキストリンクをクリックできるようになります。

Preview
メニューリンク

Lorem ipsum dolor sit amet. Consectetur adipiscing elit, sed do eiusmod tempor Incididunt ut. Labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut.

<Accordion.Root p='15'>
<Accordion.Header trigger='icon'>
<Accordion.Label><a href="#acc-innerlink">メニューリンク</a></Accordion.Label>
<Accordion.Icon isTrigger bd bdrs='5' bgc='inherit' p='10' />
</Accordion.Header>
<Accordion.Body my-s='15'>...</Accordion.Body>
</Accordion.Root>

上記の追加CSSを適用することで、アイコン部分だけをクリックできるように制限することもできます。

上記の追加CSSを適用しても、Tabキーでのカーソル操作時におけるsummaryタグ全体の選択を回避することはできません。

スクリプトの処理の内容について

おおまかな処理ステップは以下の通りです。

  • d--accordionsummary要素、または(data-role="trigger"があればその要素)に対してクリックイベント・トグルイベントを登録
  • クリック時、開閉状態に応じて、open属性とdata-opened属性を操作。
    • アニメーションの終了を待ってから状態を確定する。
    • アコーディオンが開く時、親要素にdata-accordion-multiple="disallow" があるかチェックし、あれば他の兄弟要素を閉じる。
  • トグル時の処理
    • open属性とdata-opened属性が食い違った場合に修正。
  • イベント解除(コンポーネントのアンマウント時に登録イベントを解除。)

© Lism CSS. All rights reserved.