Skip to content

Instantly share code, notes, and snippets.

@tak-dcxi
Last active December 23, 2020 17:15
Show Gist options
  • Save tak-dcxi/1a856c0cdb59a20251c5b07dff132042 to your computer and use it in GitHub Desktop.
Save tak-dcxi/1a856c0cdb59a20251c5b07dff132042 to your computer and use it in GitHub Desktop.
My Accordion
.my-accordion {
font-size: 1rem;
max-width: 640px;
word-break: break-all;
}
.my-accordion__item {
border: #ddd 1px solid;
& + & {
margin-top: 1em;
}
}
.my-accordion__headline {
background-color: #555;
color: #fefefe;
font-size: 1em;
font-weight: normal;
}
.my-accordion__tab {
$_activeColor: #858585;
align-items: center;
appearance: none;
background-color: transparent;
border: 0;
color: inherit;
cursor: pointer;
display: flex;
font: inherit;
justify-content: space-between;
padding: 1em 1.5em;
text-align: left;
transition: background-color .3s;
width: 100%;
&[aria-expanded='true'],
&:focus-visible {
background-color: $_activeColor;
}
@media (hover) {
&:hover {
background-color: $_activeColor;
}
}
}
.my-accordion__tabicon {
color: inherit;
display: inline-block;
height: 1em;
margin-left: 1em;
pointer-events: none;
position: relative;
width: 1em;
&::before,
&::after {
background-color: currentColor;
bottom: 0;
content: '';
display: inline-block;
height: 2px;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
width: 100%;
}
&::after {
transform: rotate(90deg);
transition: transform .3s;
}
.my-accordion__tab[aria-expanded='true'] & {
&::after {
transform: rotate(0);
}
}
}
.my-accordion__panel-content {
line-height: 1.75;
padding: 1.5em;
}
<div class="my-accordion js-accordion">
<section class="my-accordion__item">
<h2 class="my-accordion__headline">
<button type="button" class="my-accordion__tab js-accordion-tab">
見出し
<span class="my-accordion__tabicon"></span>
</button>
</h2>
<div class="my-accordion__panel js-accordion-panel">
<div class="my-accordion__panel-content">
<p>テキスト</p>
</div>
</div>
</section>
</div>
/**
* @param {CSS selectors} element ['.js-accordion'] - ルートに指定するclass名です。
* @param {CSS selectors} tabs ['.js-accordion-tab'] - タブに指定するclass名です。
* @param {CSS selectors} tabpanels ['.js-accordion-panel'] - パネルに指定するクラス名です。
* @param {String} timingFunction - パネル開閉時のイージングを指定します。
* @param {String} duration - パネル開閉時のアニメーション時間を指定します。
* @param {boolean} multielectable - 複数の要素を同時に開くのを許可します。
*
* @use
* Array.from(document.querySelectorAll('.js-accordion')).forEach((el) => {
* const accordion = new Accordion(el, {
* tabs: '.js-accordion-tab',
* panels: '.js-accordion-panel',
* });
* accordion.toggleItem(0, true, { noTransition: true });
* });
*
* @codepen https://codepen.io/tak-dcxi/pen/yLaaJYj
**/
const defaultOptions = {
// アコーディオンのパネルの開閉時のeasingを指定
timingFunction: 'ease-out',
// アコーディオンのパネルの開閉時のアニメーション時間を指定
duration: '.3s',
// 複数開けるようにするか
multiSelectable: false,
};
export default class Accordion {
constructor(element, options) {
const mergedOptions = Object.assign({}, defaultOptions, options);
// 必須オプションをチェックする
if (!options.tabs) {
throw TypeError('tabs オプションは必須です');
}
if (!options.panels) {
throw TypeError('panels オプションは必須です');
}
// タブとパネルを取ってくる
const tabs = Array.from(element.querySelectorAll(options.tabs));
const panels = Array.from(element.querySelectorAll(options.panels));
// イベントハンドラを設定する
const subscriptions = [
...tabs.map((tab) => attachEvent(tab, 'click', this.handleTabClick.bind(this))),
attachEvent(window, 'resize', this.handleResize.bind(this)),
]
this.element = element;
this.tabs = tabs;
this.panels = panels;
this.options = mergedOptions;
this.subscriptions = subscriptions;
this.expanded = new Set();
this.prepareAttributes();
}
destroy() {
this.subscriptions.forEach((subscription) => {
subscription.unsubscribe();
})
}
handleTabClick(event) {
const tab = event.currentTarget;
const tabIndex = this.tabs.indexOf(tab);
this.toggleItem(tabIndex, !this.expanded.has(tabIndex));
event.preventDefault();
}
handleResize() {
this.bindWindowResizeHandler();
}
prepareAttributes() {
const randomId = 'accordion-' + Math.random().toString(36).slice(2);
// アコーディオンコンポーネントのタブに付与する属性
this.tabs.forEach((tab, index) => {
tab.setAttribute('id', `${randomId}-tab-${index}`);
tab.setAttribute('aria-expanded', "false");
tab.setAttribute('aria-controls', `${randomId}-panel-${index}`);
});
// アコーディオンコンポーネントのパネルに付与する属性
this.panels.forEach((panel, index) => {
panel.setAttribute('id', `${randomId}-panel-${index}`);
panel.setAttribute('aria-hidden', "true");
panel.style.boxSizing = 'border-box';
panel.style.overflow = 'hidden';
panel.style.maxHeight = '0px';
});
}
toggleItem(itemIndex, expand, {noTransition = false} = {}) {
const isExpanded = this.expanded.has(itemIndex);
if (expand === isExpanded) {
return;
}
const updateItemAttribute = (itemIndex, expand) => {
const targetTab = this.tabs[itemIndex];
const targetPanel = this.panels[itemIndex];
targetTab.setAttribute('aria-expanded', String(expand));
targetPanel.setAttribute('aria-hidden', String(!expand));
targetPanel.style.maxHeight = expand ? targetPanel.children[0].clientHeight + 'px' : '0px';
targetPanel.style.visibility = expand ? 'visible' : 'hidden';
targetPanel.style.transition = noTransition ?
'' :
`max-height ${this.options.timingFunction} ${this.options.duration}, visibility ${this.options.duration}`;
this.expanded[expand ? 'add' : 'delete'](itemIndex);
}
// 複数選択可能の設定がされていない時は開いているパネルをすべて閉じる
if (!this.options.multiSelectable && !isExpanded) {
this.expanded.forEach((index) => updateItemAttribute(index, false));
}
updateItemAttribute(itemIndex, expand);
}
bindWindowResizeHandler() {
// 開いているパネルの高さを再計算する
this.expanded.forEach((index) => {
const panel = this.panels[index];
const resizedHeight = panel.children[0].clientHeight;
panel.style.maxHeight = resizedHeight + 'px';
});
}
}
function attachEvent(element, event, handler, options) {
element.addEventListener(event, handler, options);
return {
unsubscribe() {
element.removeEventListener(event, handler);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment