UNPKG

react-headless-accordion

Version:

A headless nested accordion made with react

195 lines (189 loc) 7.47 kB
import React, { createContext, useRef, useState, useMemo, useContext, useEffect } from 'react'; const AccordionContext = createContext({ accordionRef: null, items: {}, setItems: (data) => { }, transition: null, alwaysOpen: false }); const Accordion = ({ children, as = "div", className = "", transition = undefined, alwaysOpen = false }) => { const accordionRef = useRef(null); const [items, setItems] = useState({}); const TagName = useMemo(() => { if (as) { return as; } return "div"; }, [as]); const value = useMemo(() => { return { accordionRef, items, setItems, transition, alwaysOpen }; }, [alwaysOpen, items, transition]); return (React.createElement(AccordionContext.Provider, { value: value }, React.createElement(TagName, { className: className }, children))); }; const AccordionItemContext = createContext({ accordionRef: null, active: false, items: {}, hash: "", transition: null, alwaysOpen: false, toggle: () => { }, isActive: false, }); const AccordionItem = ({ children, isActive = false }) => { const { accordionRef, items, setItems, transition, alwaysOpen } = useContext(AccordionContext); const [active, setActive] = useState(false); const hash = useMemo(() => { return Math.random().toString(36).substring(2, 9); }, []); useEffect(() => { if (!(hash in items)) { setItems({ ...items, [hash]: setActive }); } }, [items]); const value = useMemo(() => { return { accordionRef, active, toggle: () => setActive(!active), items, hash, transition, alwaysOpen, isActive }; }, [accordionRef, active, alwaysOpen, hash, isActive, items, transition]); return (React.createElement(AccordionItemContext.Provider, { value: value }, typeof children === "function" ? children({ open: active }) : children)); }; const AccordionHeader = ({ children, as = "button", className = "", href = "", onClick }) => { const { hash, toggle, items, alwaysOpen, isActive } = useContext(AccordionItemContext); const ref = useRef(null); const TagName = useMemo(() => { if (as) { return as; } return "button"; }, [as]); useEffect(() => { if (isActive && ref && ref.current) { toggle(); const button = ref.current; button.setAttribute("aria-expanded", "true"); const content = document.querySelector(`#${button.getAttribute('aria-controls')}`); if (content) { content.style.maxHeight = "none"; } } }, []); useEffect(() => { const toggleButton = (button) => { let ariaExpanded = button.getAttribute('aria-expanded'); button.setAttribute('aria-expanded', ariaExpanded === "false" ? "true" : "false"); }; const toggleContent = (content) => { if (content) { const transitionEnd = () => { if (content.style.maxHeight !== "0px") { content.style.maxHeight = "none"; } content.removeEventListener('transitionend', transitionEnd); }; content.addEventListener('transitionend', transitionEnd); if (content.style.maxHeight === "0px") { content.style.maxHeight = content.scrollHeight + "px"; } else { content.style.maxHeight = content.scrollHeight + "px"; content.style.maxHeight = content.scrollHeight + "px"; content.style.maxHeight = "0px"; } } }; if (ref && ref.current) { const button = ref?.current; const showAccordion = (e) => { // Pervent default if (TagName === "a") { e.preventDefault(); } toggle(); if (!alwaysOpen) { // Close content already open const buttons = button.parentNode?.querySelectorAll(`:scope > ${TagName}[aria-expanded='true']`); if (buttons) { buttons.forEach(item => { if (item && item !== button) { const id = item.id.split("-")[1]; items[id](false); toggleButton(item); const content = document.querySelector(`#${item.getAttribute('aria-controls')}`); if (content) { toggleContent(content); } } }); } } // Toggle Button toggleButton(button); // Toggle Content const content = document.querySelector(`#${button.getAttribute('aria-controls')}`); toggleContent(content); if (onClick) { onClick(e); } }; if (button) { button.addEventListener('click', showAccordion); } return () => { if (button) { button.removeEventListener('click', showAccordion); } }; } return () => { }; }, [TagName, alwaysOpen, items, onClick, toggle]); if (TagName === "a") { return (React.createElement("a", { ref: ref, id: `button-${hash}`, href: href, "aria-expanded": "false", className: className, "aria-controls": `content-${hash}` }, children)); } return (React.createElement(TagName, { ref: ref, id: `button-${hash}`, "aria-expanded": "false", className: className, "aria-controls": `content-${hash}` }, children)); }; const AccordionBody = ({ children, as = "div", className = "" }) => { const { hash, transition } = useContext(AccordionItemContext); const TagName = useMemo(() => { if (as) { return as; } return "div"; }, [as]); const transitionData = useMemo(() => { const defaultData = { duration: "300ms", timingFunction: "cubic-bezier(0, 0, 0.2, 1)" }; if (transition && ("duration" in transition) && transition.duration) { defaultData.duration = transition.duration; } if (transition && ("timingFunction" in transition) && transition.timingFunction) { defaultData.timingFunction = transition.timingFunction; } return defaultData; }, [transition]); return (React.createElement(TagName, { id: `content-${hash}`, "aria-labelledby": `button-${hash}`, className: className, style: { maxHeight: "0px", transitionProperty: "max-height", overflow: "hidden", transitionDuration: transitionData.duration, transitionTimingFunction: transitionData.timingFunction } }, children)); }; export { Accordion, AccordionBody, AccordionHeader, AccordionItem }; //# sourceMappingURL=index.esm.js.map