design-comuni-plone-theme
Version:
Volto Theme for Italia design guidelines
211 lines (193 loc) • 6.24 kB
JSX
/* ************
Costruisce il sidemenu in base alle sections strutturate nella parte destra del contenuto
**************/
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable no-unused-expressions */
import { defineMessages, useIntl } from 'react-intl';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { throttle } from 'lodash';
import {
Progress,
Accordion,
AccordionBody,
AccordionHeader,
} from 'design-react-kit';
import cx from 'classnames';
const messages = defineMessages({
index: {
id: 'index',
defaultMessage: 'Indice della pagina',
},
contenuto: {
id: 'Contenuto',
defaultMessage: 'Contenuto',
},
sideMenuNavigation: {
id: 'sideMenuNavigation',
defaultMessage: 'Navigazione della pagina',
},
});
const extractHeaders = (elements, intl) => {
let item;
let headers = [];
for (var index = 0; index < elements.length; index++) {
item = elements[index];
if (item.id === 'text-body') {
headers.push({
id: item.id,
title:
item.getAttribute('menu_title') ||
intl.formatMessage(messages.contenuto),
item: item,
});
} else {
let item_header = item.querySelector('#header-' + item.id);
if (item_header) {
headers.push({
id: item.id,
title: item_header.textContent,
item: item,
});
}
}
}
return headers;
};
/**
* SideMenu view component class.
* @function SideMenu
* @params {object} content: Content object.
* @returns {string} Markup of the component.
*/
const SideMenu = ({ data, content_uid }) => {
const intl = useIntl();
const [headers, setHeaders] = useState([]);
const [activeSection, setActiveSection] = useState(null);
const [scrollY, setScrollY] = useState(0);
const [isClient, setIsClient] = useState(false);
const [isNavOpen, setIsNavOpen] = useState(
__CLIENT__ ? window.innerWidth >= 992 : false,
);
useEffect(() => {
setIsClient(true);
}, []);
const handleScroll = useCallback(() => {
const scrollOffset = 0.1 * window.innerHeight;
setScrollY(window.scrollY);
const headersHeights = headers
.map((section) => {
const element = document.getElementById(section.id);
return {
id: section.id,
top: element?.getBoundingClientRect()?.top,
};
})
.filter((section) => section.top <= scrollOffset);
if (headersHeights.length > 0) {
const section = headersHeights.reduce(
(prev, curr) => (prev.top > curr.top ? prev : curr),
headers[0],
);
setActiveSection(section.id);
}
}, [headers, activeSection]);
useEffect(() => {
if (data?.children) {
const extractedHeaders = extractHeaders(data.children, intl);
if (extractedHeaders.length > 0) {
setHeaders(extractedHeaders);
setActiveSection(extractedHeaders[0].id);
}
}
}, [data, content_uid]);
useEffect(() => {
if (headers.length > 0)
window.addEventListener('scroll', throttledHandleScroll, {
passive: true,
});
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
}, [headers]);
const throttledHandleScroll = throttle(handleScroll, 100);
const handleClickAnchor = (id) => (e) => {
e.preventDefault();
if (window.innerWidth < 992) {
setIsNavOpen(false);
}
// Blur a link
document.getElementById(`item-${id}`).blur();
// Focus on section
document.getElementById(id).focus({ preventScroll: true });
// Scroll to section
// setTimeout hack should wait for rerender after setIsNavOpen
setTimeout(() => {
document.getElementById(id)?.scrollIntoView?.({
behavior: 'smooth',
block: 'start',
});
}, 0);
};
const yCountEnd = isClient
? document.querySelector('#main-content-section')
: null;
const progressValue = useMemo(() => {
if (!isClient) return 0;
return (scrollY - yCountEnd.offsetTop) / yCountEnd.offsetHeight || 0;
}, [scrollY, isClient]);
return headers?.length > 0 ? (
<div className="sticky-wrapper navbar-wrapper page-side-menu">
<nav
className="navbar it-navscroll-wrapper navbar-expand-lg"
aria-label={intl.formatMessage(messages.sideMenuNavigation)}
>
<div className="menu-wrapper">
<div className="link-list-wrapper menu-link-list">
<div className="accordion-wrapper">
<Accordion>
<AccordionHeader
active={isNavOpen}
onToggle={() => {
setIsNavOpen(!isNavOpen);
}}
aria-controls="side-menu-body"
aria-expanded={isNavOpen}
>
<h2 className="h3">{intl.formatMessage(messages.index)}</h2>
</AccordionHeader>
<div className="mb-3">
<Progress
value={progressValue > 0 ? 100 * progressValue : 0}
role="progressbar"
/>
</div>
<AccordionBody active={isNavOpen} id="side-menu-body">
<ul className="link-list" data-element="page-index">
{headers.map((item, i) => {
return (
<li className="nav-item" key={item.id}>
<a
className={cx('nav-link', {
active: item.id === activeSection,
})}
href={`#${item.id}`}
onClick={handleClickAnchor(item.id)}
id={`item-${item.id}`}
>
<span>{item.title}</span>
</a>
</li>
);
})}
</ul>
</AccordionBody>
</Accordion>
</div>
</div>
</div>
</nav>
</div>
) : null;
};
export default SideMenu;