@spark-ui/components
Version:
Spark (Leboncoin design system) components.
407 lines (395 loc) • 13.1 kB
JavaScript
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