UNPKG

@carbon/ibm-products

Version:
122 lines (120 loc) 5.42 kB
/** * Copyright IBM Corp. 2020, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ //#region src/components/PageHeader/next/overflowHandler.ts /** * Copyright IBM Corp. 2025 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ /** * This file has test coverage in @carbon/utilities. This is a copy of the utility * that adds margin calculations to an element's size. This should ideally be * merged back into the utility. */ /* istanbul ignore file */ /** * Calculates the size (width or height) of a given HTML element. * * This function performs an expensive calculation by temporarily changing the * display style of the element if it is not currently visible. It then uses * `getBoundingClientRect` to retrieve the size of the element. * * @param el - The HTML element whose size is to be calculated. * @param dimension - The dimension to measure ('width' or 'height'). * @returns The size of the element in pixels. Returns 0 if the element is not provided. */ function getSize(el, dimension) { if (!el) return 0; const originalDisplay = el.style.display; if (!el.offsetParent && getComputedStyle(el).display === "none") el.style.display = "inline-block"; let size = el.getBoundingClientRect()[dimension]; el.style.display = originalDisplay; const computedStyles = getComputedStyle(el); size = dimension === "width" ? size + parseInt(computedStyles.marginLeft) + parseInt(computedStyles.marginRight) : size + parseInt(computedStyles.marginTop) + parseInt(computedStyles.marginBottom); return size; } /** * Updates the overflow handler by determining which items should be visible and which should be hidden. * * @param options - Configuration options for updating the overflow handler. * @param options.container - Container for overflowing * @param options.items - Child elements within container * @param options.offset - Children with data-offset attribute * @param options.sizes - Sizes of child elements * @param options.fixedSizes - Fixed sizes of child elements with data-fixed attribute * @param options.offsetSize - Total offset size * @param options.maxVisibleItems - Max visible items * @param options.dimension - width | height used to measure overflow * @param options.onChange - onChange callback * @param options.previousHiddenItems - Array of previously hidden items * @returns An array of hidden items after the update. */ function updateOverflowHandler({ container, items, offset, sizes, fixedSizes, offsetSize, maxVisibleItems, dimension, onChange, previousHiddenItems = [] }) { const containerSize = dimension === "width" ? container.clientWidth : container.clientHeight; let visibleItems = []; let hiddenItems = []; const totalSize = sizes.reduce((sum, size) => sum + size, 0); const totalFixedSize = fixedSizes.reduce((sum, size) => sum + size, 0); if (totalSize + totalFixedSize <= containerSize) { visibleItems = maxVisibleItems ? items.slice(0, maxVisibleItems) : [...items]; hiddenItems = maxVisibleItems ? items.slice(maxVisibleItems) : []; } else { const available = containerSize - offsetSize; let accumulated = 0; for (let i = 0; i < items.length; i++) { const size = sizes[i]; if (accumulated + size + totalFixedSize <= available && (!maxVisibleItems || visibleItems.length < maxVisibleItems)) { visibleItems.push(items[i]); accumulated += size; } else hiddenItems.push(items[i]); } } if (previousHiddenItems.length === hiddenItems.length && previousHiddenItems.every((item, index) => item === hiddenItems[index])) return previousHiddenItems; visibleItems.forEach((item) => item.removeAttribute("data-hidden")); hiddenItems.forEach((item) => item.setAttribute("data-hidden", "")); if (offset) offset.toggleAttribute("data-hidden", hiddenItems.length === 0); onChange(visibleItems, hiddenItems); return hiddenItems; } function createOverflowHandler({ container, maxVisibleItems, onChange, dimension = "width" }) { if (!(container instanceof HTMLElement)) throw new Error("container must be an HTMLElement"); if (typeof onChange !== "function") throw new Error("onChange must be a function"); if (maxVisibleItems !== void 0 && (!Number.isInteger(maxVisibleItems) || maxVisibleItems <= 0)) throw new Error("maxVisibleItems must be a positive integer"); const children = Array.from(container.children); const offset = children.find((item) => item.hasAttribute("data-offset")); const fixedItems = children.filter((item) => item.hasAttribute("data-fixed")); const items = children.filter((item) => item !== offset && !fixedItems.includes(item)); const fixedSizes = fixedItems.map((item) => getSize(item, dimension)); const sizes = items.map((item) => getSize(item, dimension)); const offsetSize = getSize(offset, dimension); let previousHiddenItems = []; function update() { previousHiddenItems = updateOverflowHandler({ container, items, offset, sizes, fixedSizes, offsetSize, maxVisibleItems, dimension, onChange, previousHiddenItems }); } const resizeObserver = new ResizeObserver(() => { requestAnimationFrame(update); }); resizeObserver.observe(container); requestAnimationFrame(update); return { disconnect() { resizeObserver.disconnect(); } }; } //#endregion export { createOverflowHandler };