Last active
May 7, 2024 02:17
-
-
Save tak-dcxi/098898621e93045e42cc8fd8962466a8 to your computer and use it in GitHub Desktop.
initializeAccordion
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
export type AccordionOptions = { | |
buttonSelector: string | undefined | |
panelSelector: string | undefined | |
duration?: number | |
easing?: string | |
printAll?: boolean | |
} | |
const defaultOptions: AccordionOptions = { | |
buttonSelector: undefined, | |
panelSelector: undefined, | |
duration: 300, | |
easing: 'ease-in-out', | |
printAll: false, | |
} | |
const initializeAccordion = (element: HTMLElement, options: AccordionOptions = defaultOptions): void => { | |
if (!element) return | |
const mergedOptions = { ...defaultOptions, ...options } | |
const button = element.querySelector(`${mergedOptions.buttonSelector}`) as HTMLAnchorElement | |
const panel = element.querySelector(`${mergedOptions.panelSelector}`) as HTMLElement | |
if (!button || !panel) { | |
console.error('initializeAccordion: button or panel is not found.') | |
return | |
} | |
const panelId = panel.getAttribute('id') | |
if (!panelId) { | |
console.error('initializeAccordion: panel id is required.') | |
return | |
} | |
setAttribute(button, panel, panelId) | |
button.addEventListener('click', (event) => handleClick(event, element, button, panel, mergedOptions), false) | |
button.addEventListener('keydown', (event) => handleKeyDown(event, element, button, panel, mergedOptions), false) | |
panel.addEventListener('beforematch', () => handleBeforeMatch(element, button, panel, mergedOptions), true) | |
if (mergedOptions.printAll) { | |
window.addEventListener('beforeprint', () => handleBeforePrint(element, button, panel)) | |
window.addEventListener('afterprint', () => handleAfterPrint(element, button, panel)) | |
} | |
} | |
const setAttribute = (button: HTMLAnchorElement, panel: HTMLElement, panelId: string): void => { | |
button.setAttribute('role', 'button') | |
button.setAttribute('aria-expanded', String(!panel.hasAttribute('hidden'))) | |
button.setAttribute('aria-controls', panelId) | |
} | |
const isOpened = (button: HTMLAnchorElement): boolean => { | |
return button.getAttribute('aria-expanded') === 'true' | |
} | |
let isAnimating: boolean = false | |
type AnimationOptions = Omit<AccordionOptions, 'buttonSelector' | 'panelSelector' | 'printAll'> | |
const toggleAccordion = ( | |
button: HTMLAnchorElement, | |
panel: HTMLElement, | |
options: AccordionOptions, | |
show: boolean, | |
): void => { | |
if (isOpened(button) === show) return | |
isAnimating = true | |
if (show) panel.removeAttribute('hidden') | |
button.setAttribute('aria-expanded', String(show)) | |
panel.style.willChange = 'max-block-size' | |
panel.style.overflow = 'clip' | |
const { blockSize } = window.getComputedStyle(panel) | |
const keyframes = show | |
? [{ maxBlockSize: '0' }, { maxBlockSize: blockSize }] | |
: [{ maxBlockSize: blockSize }, { maxBlockSize: '0' }] | |
const isPrefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches | |
const animationOptions: AnimationOptions = { | |
duration: isPrefersReduced ? 0 : Math.max(0, options.duration || 0), | |
easing: options.easing, | |
} | |
const onAnimationEnd = () => { | |
requestAnimationFrame(() => { | |
panel.style.willChange = '' | |
panel.style.overflow = '' | |
if (!show) panel.setAttribute('hidden', 'until-found') | |
isAnimating = false | |
}) | |
} | |
requestAnimationFrame(() => { | |
const animation = panel.animate(keyframes, animationOptions) | |
animation.addEventListener('finish', onAnimationEnd) | |
}) | |
} | |
const hideOtherAccordion = (element: HTMLElement, options: AccordionOptions, animation = true) => { | |
const accordionName = element.getAttribute('data-name') | |
if (!accordionName) return | |
const allAccordions = [...document.querySelectorAll(`[data-name="${accordionName}"]`)] as HTMLElement[] | |
const otherAccordions = allAccordions.filter( | |
(otherAccordion) => | |
otherAccordion !== element && | |
isOpened(otherAccordion.querySelector(`${options.buttonSelector}`) as HTMLAnchorElement), | |
) | |
otherAccordions.forEach((otherAccordion) => { | |
const otherButton = otherAccordion.querySelector(`${options.buttonSelector}`) as HTMLAnchorElement | |
const otherPanel = otherAccordion.querySelector(`${options.panelSelector}`) as HTMLElement | |
if (!(otherButton || otherPanel)) return | |
if (animation) { | |
toggleAccordion(otherButton, otherPanel, options, false) | |
} else { | |
otherButton.setAttribute('aria-expanded', 'false') | |
otherPanel.setAttribute('hidden', 'until-found') | |
} | |
}) | |
} | |
const handleClick = ( | |
event: MouseEvent | KeyboardEvent, | |
element: HTMLElement, | |
button: HTMLAnchorElement, | |
panel: HTMLElement, | |
options: AccordionOptions, | |
): void => { | |
event.preventDefault() | |
if (isAnimating) return | |
toggleAccordion(button, panel, options, !isOpened(button)) | |
if (isOpened(button)) hideOtherAccordion(element, options) | |
} | |
const handleKeyDown = ( | |
event: KeyboardEvent, | |
element: HTMLElement, | |
button: HTMLAnchorElement, | |
panel: HTMLElement, | |
options: AccordionOptions, | |
): void => { | |
if (event.key === ' ') { | |
handleClick(event, element, button, panel, options) | |
} | |
} | |
const handleBeforeMatch = ( | |
element: HTMLElement, | |
button: HTMLAnchorElement, | |
panel: HTMLElement, | |
options: AccordionOptions, | |
): void => { | |
button.setAttribute('aria-expanded', 'true') | |
hideOtherAccordion(element, options, false) | |
} | |
const openStatusAttribute = 'data-open-status' | |
const handleBeforePrint = (element: HTMLElement, button: HTMLAnchorElement, panel: HTMLElement): void => { | |
if (!element) return | |
const isStatusOpen = button.getAttribute('aria-expanded') === 'true' | |
element.setAttribute(openStatusAttribute, String(isStatusOpen)) | |
button.setAttribute('aria-expanded', 'true') | |
panel.removeAttribute('hidden') | |
} | |
const handleAfterPrint = (element: HTMLElement, button: HTMLAnchorElement, panel: HTMLElement): void => { | |
if (!element) return | |
const isStatusOpen = element.getAttribute(openStatusAttribute) === 'true' | |
button.setAttribute('aria-expanded', String(isStatusOpen)) | |
if (!isStatusOpen) panel.setAttribute('hidden', 'until-found') | |
element.removeAttribute(openStatusAttribute) | |
} | |
export default initializeAccordion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment