UNPKG

@spark-ui/components

Version:

Spark (Leboncoin design system) components.

407 lines (395 loc) 13.1 kB
import { IconButton } from "../chunk-QLOIAU3C.mjs"; import { Button } from "../chunk-USSL4UZ5.mjs"; import "../chunk-MUNDKRAE.mjs"; import { Icon } from "../chunk-AESXFMCC.mjs"; import "../chunk-NBZKMCHF.mjs"; import { Slot } from "../chunk-4F5DOL57.mjs"; // src/scrolling-list/ScrollingList.tsx import { createContext, useRef } from "react"; import { useSnapCarousel } from "react-snap-carousel"; // src/scrolling-list/useScrollOverflow.ts import { useEffect, useState } from "react"; function useScrollOverflow(scrollRef) { const [overflow, setOverflow] = useState({ left: 0, right: 0 }); useEffect(() => { const checkScrollContent = () => { const scrollElement2 = scrollRef.current; if (scrollElement2) { const { scrollLeft, scrollWidth, clientWidth } = scrollElement2; setOverflow({ left: scrollLeft, right: scrollWidth - (scrollLeft + clientWidth) }); } }; checkScrollContent(); const scrollElement = scrollRef.current; if (scrollElement) { scrollElement.addEventListener("scroll", checkScrollContent); window.addEventListener("resize", checkScrollContent); } return () => { if (scrollElement) { scrollElement.removeEventListener("scroll", checkScrollContent); window.addEventListener("resize", checkScrollContent); } }; }, [scrollRef]); return overflow; } // src/scrolling-list/ScrollingList.tsx import { jsx, jsxs } from "react/jsx-runtime"; var ScrollingListContext = createContext( null ); var ScrollingList = ({ snapType = "none", snapStop = "normal", scrollBehavior = "smooth", loop = false, gap = 16, widthFade = false, // TODO: ask for default value + why it has been removed from specs scrollPadding = 0, children }) => { const scrollAreaRef = useRef(null); const skipAnchorRef = useRef(null); const snapCarouselAPI = useSnapCarousel(); const overflow = useScrollOverflow(scrollAreaRef); const { activePageIndex, pages } = snapCarouselAPI; const visibleItems = pages[activePageIndex]; const visibleItemsRange = visibleItems ? [visibleItems[0] + 1, visibleItems[visibleItems.length - 1] + 1] : [0, 0]; const skipKeyboardNavigation = () => { skipAnchorRef.current?.focus(); }; const ctxValue = { ...snapCarouselAPI, snapType, snapStop, skipKeyboardNavigation, scrollBehavior, visibleItemsRange, loop, gap, widthFade, scrollPadding, scrollAreaRef, overflow }; return /* @__PURE__ */ jsxs(ScrollingListContext.Provider, { value: ctxValue, children: [ /* @__PURE__ */ jsx("div", { className: "gap-lg group/scrolling-list relative flex w-full flex-col", children }), /* @__PURE__ */ jsx("span", { ref: skipAnchorRef, className: "size-0 overflow-hidden", tabIndex: -1 }) ] }); }; ScrollingList.displayName = "ScrollingList"; // src/scrolling-list/ScrollingListControls.tsx import { cx } from "class-variance-authority"; import { jsx as jsx2 } from "react/jsx-runtime"; var ScrollingListControls = ({ children, visibility = "always", className, ...rest }) => { return /* @__PURE__ */ jsx2( "div", { className: cx( "default:px-md pointer-events-none absolute inset-0 flex flex-row items-center justify-between overflow-hidden", className ), style: { "--scrolling-list-controls-opacity": visibility === "hover" ? "0" : "1" }, "data-orientation": "horizontal", ...rest, children } ); }; ScrollingListControls.displayName = "ScrollingList.Controls"; // src/scrolling-list/ScrollingListItem.tsx import { cx as cx2 } from "class-variance-authority"; import { useContext, useRef as useRef2 } from "react"; // src/scrolling-list/useFocusWithinScroll.tsx import { useEffect as useEffect2, useState as useState2 } from "react"; function useFocusWithinScroll(ref, scrollRef) { const [isFocusWithin, setIsFocusWithin] = useState2(false); useEffect2(() => { const handleFocusIn = (event) => { setIsFocusWithin(true); const focusedElement = event.target; const scrollContainer = scrollRef.current; if (focusedElement && scrollContainer) { const focusRect = focusedElement.getBoundingClientRect(); const scrollRect = scrollContainer.getBoundingClientRect(); const isFullyVisible = focusRect.left >= scrollRect.left && focusRect.right <= scrollRect.right && focusRect.top >= scrollRect.top && focusRect.bottom <= scrollRect.bottom; if (!isFullyVisible) { focusedElement.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" }); } } }; const handleFocusOut = (event) => { if (ref.current && !ref.current.contains(event.relatedTarget)) { setIsFocusWithin(false); } }; const node = ref.current; if (node) { node.addEventListener("focusin", handleFocusIn); node.addEventListener("focusout", handleFocusOut); } return () => { if (node) { node.removeEventListener("focusin", handleFocusIn); node.removeEventListener("focusout", handleFocusOut); } }; }, [ref, scrollRef]); return isFocusWithin; } // src/scrolling-list/ScrollingListItem.tsx import { jsx as jsx3 } from "react/jsx-runtime"; var ScrollingListItem = ({ asChild = false, children, index = 0, className = "", ...rest }) => { const ctx = useContext(ScrollingListContext); const itemRef = useRef2(null); const isSnapPoint = ctx.snapPointIndexes.has(index); useFocusWithinScroll(itemRef, ctx.scrollAreaRef); const Component = asChild ? Slot : "div"; return /* @__PURE__ */ jsx3( Component, { role: "listitem", ref: itemRef, className: cx2( "default:w-auto default:shrink-0", { "snap-start": isSnapPoint, "snap-normal": isSnapPoint && ctx.snapStop === "normal", "snap-always": isSnapPoint && ctx.snapStop === "always" }, className ), ...rest, children } ); }; ScrollingListItem.displayName = "ScrollingList.Item"; // src/scrolling-list/ScrollingListItems.tsx import { cx as cx3 } from "class-variance-authority"; import { Children, cloneElement, isValidElement, useContext as useContext2 } from "react"; import { jsx as jsx4 } from "react/jsx-runtime"; function mergeRefs(...refs) { return (value) => { refs.forEach((ref) => { if (typeof ref === "function") { ref(value); } else if (ref && typeof ref === "object" && "current" in ref) { ; ref.current = value; } }); }; } var ScrollingListItems = ({ children, className = "", ...rest }) => { const ctx = useContext2(ScrollingListContext); const snapConfig = { mandatory: "x mandatory", proximity: "x proximity", none: "none" }; const handleLeftArrow = (event) => { if (!ctx.loop && !ctx.hasPrevPage) return; event.preventDefault(); ctx.goTo(ctx.hasPrevPage ? ctx.activePageIndex - 1 : ctx.pages.length - 1, { behavior: ctx.scrollBehavior }); }; const handleRightArrow = (event) => { if (!ctx.loop && !ctx.hasNextPage) return; event.preventDefault(); ctx.goTo(ctx.hasNextPage ? ctx.activePageIndex + 1 : 0, { behavior: ctx.scrollBehavior }); }; const handleKeyDown = (event) => { if (event.key === "ArrowLeft") { handleLeftArrow(event); } if (event.key === "ArrowRight") { handleRightArrow(event); } }; const inlineStyles = { scrollSnapType: snapConfig[ctx.snapType], scrollPaddingInline: "var(--scrolling-list-px)", "--scrolling-list-px": `${ctx.scrollPadding}px`, "--scrolling-list-gap": `${ctx.gap}px`, ...ctx.widthFade && { maskImage: "linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 44px, rgba(0, 0, 0, 1) calc(100% - 44px), rgba(0, 0, 0, 0))", maskSize: `calc(100% + ${ctx.overflow.left ? "0px" : "44px"} + ${ctx.overflow.right ? "0px" : "44px"}) 100%`, maskPosition: `${ctx.overflow.left ? "0px" : "-44px"} 0` } }; return /* @__PURE__ */ jsx4( "div", { id: "scrolling-list-items", role: "list", className: cx3( "relative transition-all duration-300", "u-no-scrollbar overflow-x-auto scroll-smooth default:overscroll-contain", "w-full gap-(--scrolling-list-gap) default:flex default:flex-row", "focus-visible:u-outline", className ), ref: mergeRefs(ctx.scrollAreaRef, ctx.scrollRef), style: inlineStyles, onKeyDown: handleKeyDown, ...rest, children: Children.map( children, (child, index) => isValidElement(child) ? cloneElement(child, { index }) : child ) } ); }; ScrollingListItems.displayName = "ScrollingList.Items"; // src/scrolling-list/ScrollingListNextButton.tsx import { ArrowVerticalRight } from "@spark-ui/icons/ArrowVerticalRight"; import { cx as cx4 } from "class-variance-authority"; import { useContext as useContext3 } from "react"; import { jsx as jsx5 } from "react/jsx-runtime"; var ScrollingListNextButton = ({ "aria-label": ariaLabel, ...rest }) => { const ctx = useContext3(ScrollingListContext); const handleNextPage = () => { if (ctx.hasNextPage) { ctx.next({ behavior: ctx.scrollBehavior }); } else { ctx.goTo(0, { behavior: ctx.scrollBehavior }); } }; const listHasOverflow = ctx.overflow.left || ctx.overflow.right; const isDisabled = !listHasOverflow || !ctx.loop && !ctx.overflow.right; return /* @__PURE__ */ jsx5( IconButton, { size: "sm", intent: "surface", design: "filled", className: cx4( "pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible", "group-hover/scrolling-list:opacity-none focus-visible:opacity-none" ), onClick: handleNextPage, disabled: isDisabled, "aria-label": ariaLabel, "aria-controls": "scrolling-list-items", ...rest, children: /* @__PURE__ */ jsx5(Icon, { children: /* @__PURE__ */ jsx5(ArrowVerticalRight, {}) }) } ); }; ScrollingListNextButton.displayName = "ScrollingList.NextButton"; // src/scrolling-list/ScrollingListPrevButton.tsx import { ArrowVerticalLeft } from "@spark-ui/icons/ArrowVerticalLeft"; import { cx as cx5 } from "class-variance-authority"; import { useContext as useContext4 } from "react"; import { jsx as jsx6 } from "react/jsx-runtime"; var ScrollingListPrevButton = ({ "aria-label": ariaLabel, ...rest }) => { const ctx = useContext4(ScrollingListContext); const handlePrevPage = () => { const shouldSnapFirstPage = ctx.activePageIndex === 0 && (ctx.scrollAreaRef.current?.scrollLeft || 0) > 0; if (shouldSnapFirstPage) { ctx.goTo(0, { behavior: ctx.scrollBehavior }); } else if (ctx.hasPrevPage) { ctx.prev({ behavior: ctx.scrollBehavior }); } else { ctx.goTo(ctx.pages.length - 1, { behavior: ctx.scrollBehavior }); } }; const listHasOverflow = ctx.overflow.left || ctx.overflow.right; const isDisabled = !listHasOverflow || !ctx.loop && !ctx.overflow.left; return /* @__PURE__ */ jsx6( IconButton, { size: "sm", intent: "surface", design: "filled", className: cx5( "pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible", "group-hover/scrolling-list:opacity-none focus-visible:opacity-none" ), onClick: handlePrevPage, disabled: isDisabled, "aria-label": ariaLabel, "aria-controls": "scrolling-list-items", ...rest, children: /* @__PURE__ */ jsx6(Icon, { children: /* @__PURE__ */ jsx6(ArrowVerticalLeft, {}) }) } ); }; ScrollingListPrevButton.displayName = "ScrollingList.PrevButton"; // src/scrolling-list/ScrollingListSkipButton.tsx import { cx as cx6 } from "class-variance-authority"; import { useContext as useContext5 } from "react"; import { jsx as jsx7 } from "react/jsx-runtime"; var ScrollingListSkipButton = ({ children, ...rest }) => { const ctx = useContext5(ScrollingListContext); return /* @__PURE__ */ jsx7( Button, { type: "button", design: "tinted", intent: "surface", tabIndex: 0, className: cx6( "z-raised absolute top-1/2 left-0 -translate-y-1/2", "not-focus-visible:pointer-events-none not-focus-visible:size-0 not-focus-visible:opacity-0" ), onClick: ctx.skipKeyboardNavigation, ...rest, children } ); }; ScrollingListSkipButton.displayName = "ScrollingList.SkipButton"; // src/scrolling-list/index.ts var ScrollingList2 = Object.assign(ScrollingList, { Controls: ScrollingListControls, NextButton: ScrollingListNextButton, PrevButton: ScrollingListPrevButton, Item: ScrollingListItem, Items: ScrollingListItems, SkipButton: ScrollingListSkipButton }); ScrollingList2.displayName = "ScrollingList"; export { ScrollingList2 as ScrollingList }; //# sourceMappingURL=index.mjs.map