Skip to content

Instantly share code, notes, and snippets.

@tomaszgil
Created August 18, 2021 16:25
Show Gist options
  • Save tomaszgil/3a471eb7de8589c269c55440a1762b20 to your computer and use it in GitHub Desktop.
Save tomaszgil/3a471eb7de8589c269c55440a1762b20 to your computer and use it in GitHub Desktop.
import React, { useRef, useState, useLayoutEffect, useCallback } from "react";
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
function useValueEffect(defaultValue) {
const [value, setValue] = useState(defaultValue);
const effect = useRef(null);
// Store the function in a ref so we can always access the current version
// which has the proper `value` in scope.
const nextRef = useRef(null);
nextRef.current = () => {
// Run the generator to the next yield.
const newValue = effect.current.next();
// If the generator is done, reset the effect.
if (newValue.done) {
effect.current = null;
return;
}
// If the value is the same as the current value,
// then continue to the next yield. Otherwise,
// set the value in state and wait for the next layout effect.
if (value === newValue.value) {
nextRef.current();
} else {
setValue(newValue.value);
}
};
useLayoutEffect(() => {
// If there is an effect currently running, continue to the next yield.
if (effect.current) {
nextRef.current();
}
});
const queue = useCallback(
(fn) => {
effect.current = fn();
nextRef.current();
},
[effect, nextRef]
);
return [value, queue];
}
export function Breadcrumbs({ children }) {
const listRef = useRef(null);
const childrenCount = React.Children.count(children);
const [visibleItemsCount, setVisibleItemsCount] = useValueEffect(
childrenCount
);
const updateOverflow = useCallback(() => {
function computeVisibleItems(currentVisibleItemsCount) {
const listItems = Array.from(listRef.current.children);
const containerWidth = listRef.current.offsetWidth;
const isShowingStack = childrenCount > currentVisibleItemsCount;
let calculatedWidth = 0;
let newItemsNumber = 0;
if (isShowingStack) {
calculatedWidth += listItems.shift().offsetWidth;
}
listItems.reverse();
for (const listItem of listItems) {
if (calculatedWidth + listItem.offsetWidth < containerWidth) {
calculatedWidth += listItem.offsetWidth;
newItemsNumber += 1;
} else {
break;
}
}
return newItemsNumber;
}
setVisibleItemsCount(function* () {
yield childrenCount;
const newVisibleItems = computeVisibleItems(childrenCount);
yield newVisibleItems;
if (newVisibleItems < childrenCount) {
yield computeVisibleItems(newVisibleItems);
}
});
}, [setVisibleItemsCount, childrenCount]);
useLayoutEffect(updateOverflow, [children, updateOverflow]);
const shouldRenderStack = childrenCount > visibleItemsCount;
const visibleChildren = shouldRenderStack
? children.slice(-visibleItemsCount)
: children;
return (
<nav>
<ol ref={listRef} className="list">
{shouldRenderStack && <li className="crumb">...</li>}
{React.Children.map(visibleChildren, (item) => (
<li key={item.key} className="crumb">
{item}
</li>
))}
</ol>
</nav>
);
}
export function BreadcrumbItem({ children, ...props }) {
return <a {...props}>{children}</a>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment