Last active
December 23, 2020 17:15
-
-
Save tak-dcxi/1a856c0cdb59a20251c5b07dff132042 to your computer and use it in GitHub Desktop.
My Accordion
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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