Skip to content

Instantly share code, notes, and snippets.

@nckcol
Created April 3, 2023 14:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nckcol/10ce6a464bdc691ab7d7b6277b8ee0c8 to your computer and use it in GitHub Desktop.
Save nckcol/10ce6a464bdc691ab7d7b6277b8ee0c8 to your computer and use it in GitHub Desktop.
hook implementing priority+ navigation pattern
/**
* hook implementing Priority+ Navigation pattern
*
* Reference implementation: https://github.com/gijsroge/priority-navigation
* Ported from: https://github.com/gijsroge/vue-responsive-menu
*
* Read more about pattern Priority+ Navigation:
* https://css-tricks.com/the-priority-navigation-pattern/
* https://bradfrost.com/blog/post/revisiting-the-priority-pattern/
*/
import { useRef, useState, useEffect, useCallback } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
const getWidthIncludingMargin = (el) =>
el.offsetWidth +
parseFloat(getComputedStyle(el).marginLeft) +
parseFloat(getComputedStyle(el).marginRight);
const getTotalWidthOfChildren = (element) =>
Array.from(element.children).reduce(
(total, child) => total + getWidthIncludingMargin(child),
0,
);
const MIN_VISIBLE_ITEMS = 1;
const usePriorityPlus = (rootElementRef, items) => {
const previousMenuWidth = useRef(-1);
const done = useRef(false);
const full = useRef(true);
const [priorityCursor, setPriorityCursor] = useState(items.length);
const itemsRef = useRef(items);
useEffect(() => {
itemsRef.current = items;
done.current = false;
setPriorityCursor(items.length);
}, [items]);
const moveItem = useCallback(() => {
if (!rootElementRef.current) {
return;
}
done.current = false;
const currentMenuWidth = rootElementRef.current.getBoundingClientRect()
.width;
const totalWidthOfChildren =
itemsRef.current.length > 0
? getTotalWidthOfChildren(rootElementRef.current)
: 0;
if (currentMenuWidth < totalWidthOfChildren /* + offset */) {
previousMenuWidth.current = currentMenuWidth;
full.current = true;
if (priorityCursor > MIN_VISIBLE_ITEMS) {
setPriorityCursor(priorityCursor - 1);
return;
}
}
if (currentMenuWidth > previousMenuWidth.current || !full.current) {
previousMenuWidth.current = currentMenuWidth;
if (priorityCursor < itemsRef.current.length) {
full.current = false;
setPriorityCursor(priorityCursor + 1);
return;
}
}
done.current = true;
}, [rootElementRef, priorityCursor]);
useEffect(() => {
if (!rootElementRef.current) {
return () => {};
}
// TODO: replace ResizeObserver polyfill with fallback on window.resize
const observer = new ResizeObserver((entries) => {
entries.forEach(() => {
moveItem();
});
});
const root = rootElementRef.current;
observer.observe(root);
return () => observer.disconnect();
}, [rootElementRef, moveItem]);
useLayoutEffect(() => {
if (!done.current) {
moveItem();
}
}, [moveItem]);
const priorityItems = items.slice(0, priorityCursor);
const otherItems = items.slice(priorityCursor);
return { priorityItems, otherItems };
};
export default usePriorityPlus;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment