@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
195 lines (191 loc) • 6.53 kB
JavaScript
/**
* 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.
*/
import "../../globals/settings.js";
//#region src/utilities/snapscroll/snapscroll.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.
*/
/**
* Initiates the snapScroll functionality
* @param body selector for element that contains the items
* @param child selector for the item elements
* @returns void
*/
const blockClass = `c4p--snappy`;
const elmClass = `${blockClass}__elm`;
const selectionClass = `${blockClass}__selection`;
function snapScroll(body, child) {
const bodyEl = document.querySelector(body);
if (!bodyEl) return;
const styleElement = document.createElement("style");
styleElement.textContent = `
.${blockClass} {
display: flex;
flex: none;
overflow: scroll;
scroll-snap-type: x mandatory;
width: 100%;
}
.${blockClass}::-webkit-scrollbar {
display: none;
}
.${blockClass}__elm {
scroll-snap-align: start;
flex: none;
width: 100%;
}
`;
document.head.appendChild(styleElement);
bodyEl.classList.add(blockClass);
const scrollsnapSupported = "onscrollsnapchange" in bodyEl;
const children = bodyEl.querySelectorAll(child);
if (children.length > 0) children.forEach((el) => {
el.classList.add(elmClass);
});
if (scrollsnapSupported) bodyEl.addEventListener("scrollsnapchange", (event) => {
const snapevent = event;
const currentlySnapped = document.querySelector(`.${selectionClass}`);
if (currentlySnapped) currentlySnapped.classList.remove(selectionClass);
snapevent.snapTargetInline.classList.add(selectionClass);
});
else {
const handler = (entries) => {
entries.forEach((entry) => {
entry.target.classList.toggle(selectionClass, entry.isIntersecting);
});
};
let observer;
const createObserver = () => {
if (observer) observer.disconnect();
observer = new IntersectionObserver(handler, {
root: bodyEl,
threshold: 1
});
children.forEach((child) => {
observer.observe(child);
});
};
createObserver();
}
}
/**
* Retreives the currently focused element
* @returns currently focused element
*/
function getFocusedItem() {
return document.querySelector(`.${selectionClass}`);
}
/**
* Helper function to check if an element is fully visible within its parent
* @param element The element to check
* @param parent The parent container
* @returns true if element is fully visible within parent bounds
*/
function isElementFullyInView(element, parent) {
const elementRect = element.getBoundingClientRect();
const parentRect = parent.getBoundingClientRect();
return elementRect.left >= parentRect.left && elementRect.right <= parentRect.right;
}
/**
* Finds the first sibling that is not in view by traversing in the specified direction
* @param startSibling The sibling to start checking from
* @param parent The parent container
* @param direction 'next' to check nextElementSibling, 'previous' to check previousElementSibling
* @returns The first sibling not in view, or the last sibling if all are in view
*/
function findFirstSiblingNotInView(startSibling, parent, direction) {
let currentSibling = startSibling;
let lastCheckedSibling = startSibling;
while (currentSibling) {
if (!isElementFullyInView(currentSibling, parent)) return currentSibling;
lastCheckedSibling = currentSibling;
currentSibling = direction === "next" ? currentSibling.nextElementSibling : currentSibling.previousElementSibling;
}
return lastCheckedSibling;
}
/**
* Helper function to scroll to a sibling element with proper handling for already-visible items
* @param sibling The sibling element to start checking from
* @param inline The scroll alignment ('start' for next, 'nearest' for previous)
* @param direction The direction to traverse ('next' or 'previous')
*/
function scrollToSibling(sibling, inline, direction) {
const parent = sibling.parentElement;
if (!parent) return;
const targetSibling = findFirstSiblingNotInView(sibling, parent, direction);
const isFullyInView = isElementFullyInView(targetSibling, parent);
if (isFullyInView) {
const currentlySnapped = document.querySelector(`.${selectionClass}`);
if (currentlySnapped) currentlySnapped.classList.remove(selectionClass);
targetSibling.classList.add(selectionClass);
}
targetSibling.scrollIntoView({
behavior: "smooth",
inline
});
if (isFullyInView) setTimeout(() => {
parent.dispatchEvent(new Event("scrollend", { bubbles: true }));
}, 50);
}
/**
* Scrolls to the next sibling element of the currently focused element
*/
function scrollNext() {
const sibling = getNextSibling();
if (sibling) scrollToSibling(sibling, "start", "next");
}
/**
* Scrolls to the previous sibling element of the currently focused element
*/
function scrollPrevious() {
const sibling = getPreviousSibling();
if (sibling) scrollToSibling(sibling, "nearest", "previous");
}
function getNextSibling() {
const item = getFocusedItem();
if (!item) return;
return item.nextElementSibling;
}
function getPreviousSibling() {
const item = getFocusedItem();
if (!item) return;
return item.previousElementSibling;
}
/**
* Checks if there are any next siblings that are not fully in view
* @returns true if there are more items to scroll to in the next direction
*/
function hasNextSiblingNotInView() {
const item = getFocusedItem();
if (!item || !item.parentElement) return false;
let currentSibling = item.nextElementSibling;
while (currentSibling) {
if (!isElementFullyInView(currentSibling, item.parentElement)) return true;
currentSibling = currentSibling.nextElementSibling;
}
return false;
}
/**
* Checks if there are any previous siblings that are not fully in view
* @returns true if there are more items to scroll to in the previous direction
*/
function hasPreviousSiblingNotInView() {
const item = getFocusedItem();
if (!item || !item.parentElement) return false;
let currentSibling = item.previousElementSibling;
while (currentSibling) {
if (!isElementFullyInView(currentSibling, item.parentElement)) return true;
currentSibling = currentSibling.previousElementSibling;
}
return false;
}
//#endregion
export { blockClass, getFocusedItem, getNextSibling, getPreviousSibling, hasNextSiblingNotInView, hasPreviousSiblingNotInView, scrollNext, scrollPrevious, snapScroll };
//# sourceMappingURL=snapscroll.js.map