Skip to content

Commit

Permalink
feat: add dropdown menu for items in toc getting out of viewport - re…
Browse files Browse the repository at this point in the history
…fs #254302
  • Loading branch information
MihaelaCretu11 committed Jul 24, 2023
1 parent 6b9c132 commit db39fec
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 8 deletions.
51 changes: 51 additions & 0 deletions src/customizations/volto/components/manage/Blocks/ToC/Schema.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const TableOfContentsSchema = ({ data }) => {
const { variation = 'default' } = data;

return {
title: 'Table of Contents',
fieldsets: [
{
id: 'default',
title: 'Default',
fields: [
'title',
'hide_title',
...(variation === 'default' ? ['ordered'] : ['sticky']),
'levels',
],
},
],
properties: {
title: {
title: 'Block title',
},
hide_title: {
title: 'Hide title',
type: 'boolean',
},
levels: {
title: 'Entries',
isMulti: true,
choices: [
['h1', 'h1'],
['h2', 'h2'],
['h3', 'h3'],
['h4', 'h4'],
['h5', 'h5'],
['h6', 'h6'],
],
},
ordered: {
title: 'Ordered',
type: 'boolean',
},
sticky: {
title: 'Sticky',
type: 'boolean',
},
},
required: [],
};
};

export default TableOfContentsSchema;
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
/**
* View toc block.
* @module components/manage/Blocks/ToC/View
*/

import React from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { map } from 'lodash';
import { Menu } from 'semantic-ui-react';
import { Menu, Dropdown } from 'semantic-ui-react';
import { FormattedMessage, injectIntl } from 'react-intl';
import AnchorLink from 'react-anchor-link-smooth-scroll';
import Slugger from 'github-slugger';

import './horizontal-menu.less';

const RenderMenuItems = ({ items }) => {
return map(items, (item) => {
const { id, level, title, override_toc, plaintext } = item;
Expand All @@ -36,6 +33,131 @@ const RenderMenuItems = ({ items }) => {
* @extends Component
*/
const View = ({ data, tocEntries }) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// When the page is resized to prevent items from the TOC from going out of the viewport,
// a dropdown menu is added containing all the items that don't fit.
const handleResize = () => {
const menuElement = document.querySelector('.responsive-menu');
const containerWidth = menuElement.offsetWidth;

// Get all divs that contain the items from the TOC, except the dropdown button
const nested = document.querySelectorAll(
'.responsive-menu .item:not(.toc-dropdown)',
);
const nestedArray = Object.values(nested);
const middle = Math.ceil(nestedArray.length / 2);
const firstHalfNested = nestedArray.slice(0, middle);
const secondHalfNested = nestedArray.slice(middle);

const dropdown = document.querySelector('.toc-dropdown');
const dropdownWidth = dropdown.offsetWidth || 67;

const firstHalfNestedHiddenItems = [];

// Add a 'hidden' class for the items that should be in the dropdown
firstHalfNested.forEach((item) => {
const itemOffsetLeft = item.offsetLeft;
const itemOffsetWidth = item.offsetWidth;
if (itemOffsetLeft + itemOffsetWidth > containerWidth - dropdownWidth) {
item.classList.add('hidden');
firstHalfNestedHiddenItems.push(item);
} else {
item.classList.remove('hidden');
}
});

secondHalfNested.forEach((item) => item.classList.add('hidden-dropdown'));

const diff = firstHalfNested.length - firstHalfNestedHiddenItems.length;
const secondHalfNestedShownItems = secondHalfNested.slice(diff);
secondHalfNestedShownItems.forEach((item) =>
item.classList.remove('hidden-dropdown'),
);

// If there are elements that should be displayed in the dropdown, show the dropdown button
if (secondHalfNestedShownItems.length > 0)
dropdown.classList.remove('hidden-dropdown');
else {
dropdown.classList.add('hidden-dropdown');
}
};

const handleDropdownKeyDown = (event) => {
const dropdownMenu = document.querySelector('.menu.transition');
if (event.key === 'ArrowDown' && isDropdownOpen) {
event.preventDefault();
const menuItems = dropdownMenu.querySelectorAll(
'.item:not(.hidden-dropdown)',
);
const focusedItem = dropdownMenu.querySelector('.item.focused');
const focusedIndex = Array.from(menuItems).indexOf(focusedItem);

if (focusedIndex === -1) {
// No item is currently focused, so focus the first item
menuItems[0].classList.add('focused');
} else if (focusedIndex === menuItems.length - 1) {
// Remove focus from the currently focused item and close the dropdown
focusedItem.classList.remove('focused');
setIsDropdownOpen(false);

// Focus the next element on the page
const nextElement = dropdownMenu.nextElementSibling;
if (nextElement) {
nextElement.focus();
}
} else {
// Remove focus from the currently focused item
focusedItem.classList.remove('focused');

// Focus the next item or wrap around to the first item
const nextIndex = (focusedIndex + 1) % menuItems.length;
menuItems[nextIndex].classList.add('focused');
}
} else if (event.key === 'Enter' && isDropdownOpen) {
const focusedItem = dropdownMenu.querySelector('.item.focused');
if (focusedItem) {
focusedItem.querySelector('a').click();
focusedItem.classList.remove('focused');
}
} else if (event.key === 'Tab') {
const focusedItem = dropdownMenu.querySelector('.item.focused');
if (focusedItem) {
focusedItem.classList.remove('focused');
}
}
};

useEffect(() => {
if (data.sticky) {
const toc = document.querySelector('.horizontalMenu');
const tocPos = toc ? toc.offsetTop : 0;

const handleScroll = () => {
let scrollPos = window.scrollY;
if (scrollPos > tocPos && toc) {
toc.classList.add('sticky-toc');
} else if (scrollPos <= tocPos && toc) {
toc.classList.remove('sticky-toc');
}
};

window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);
};
}
}, [data.sticky]);

useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
});

return (
<>
{data.title && !data.hide_title ? (
Expand All @@ -50,8 +172,22 @@ const View = ({ data, tocEntries }) => {
) : (
''
)}
<Menu>
<Menu className="responsive-menu">
<RenderMenuItems items={tocEntries} />
<Dropdown
item
text="More"
className="hidden-dropdown toc-dropdown"
open={isDropdownOpen}
onOpen={() => setIsDropdownOpen(true)}
onClose={() => setIsDropdownOpen(false)}
tabIndex={0}
onKeyDown={handleDropdownKeyDown}
>
<Dropdown.Menu>
<RenderMenuItems items={tocEntries} />
</Dropdown.Menu>
</Dropdown>
</Menu>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.table-of-contents.horizontalMenu.sticky-toc {
position: fixed !important;
z-index: 1 !important;
top: 0 !important;
width: inherit;
}

.table-of-contents.horizontalMenu .hidden {
z-index: -1;
pointer-events: none;
visibility: hidden;
}

.table-of-contents.horizontalMenu .hidden-dropdown {
display: none !important;
}

.table-of-contents.horizontalMenu .item.toc-dropdown {
position: absolute !important;
right: 0;
height: 100%;
}

.table-of-contents.horizontalMenu > .ui.menu {
position: relative;
}

.table-of-contents.horizontalMenu .item.toc-dropdown .focused > a {
border: 2px solid black !important;
border-radius: 4px;
}

.ui.menu .dropdown.item .menu {
right: 0;
left: auto;
}

0 comments on commit db39fec

Please sign in to comment.