diff --git a/src/components/bottom-sheet/BottomSheet.tsx b/src/components/bottom-sheet/BottomSheet.tsx new file mode 100644 index 0000000..cda7f99 --- /dev/null +++ b/src/components/bottom-sheet/BottomSheet.tsx @@ -0,0 +1,141 @@ +import { + Component, + createSignal, + onMount, + onCleanup, + splitProps, + mergeProps, +} from "solid-js"; +import { clsx } from "clsx"; +import type { IComponentBaseProps } from "../types"; + +export interface BottomSheetProps extends IComponentBaseProps { + isOpen: boolean; + onClose: () => void; + children?: any; + closeOnOverlayClick?: boolean; + closeOnSwipeDown?: boolean; +} + +const BottomSheet: Component = (props) => { + const merged = mergeProps( + { + closeOnOverlayClick: true, + closeOnSwipeDown: true, + }, + props + ); + + const [local, others] = splitProps(merged, [ + "isOpen", + "onClose", + "children", + "dataTheme", + "class", + "className", + "style", + "closeOnOverlayClick", + "closeOnSwipeDown", + ]); + + const [isDragging, setIsDragging] = createSignal(false); + const [startY, setStartY] = createSignal(0); + const [currentY, setCurrentY] = createSignal(0); + + let sheetRef: HTMLDivElement | undefined; + let overlayRef: HTMLDivElement | undefined; + + const handleTouchStart = (e: TouchEvent) => { + if (!local.closeOnSwipeDown) return; + const touchY = e.touches[0].clientY; + const sheetTop = sheetRef?.getBoundingClientRect().top || 0; + if (touchY - sheetTop < 50) { + setIsDragging(true); + setStartY(touchY); + setCurrentY(touchY); + } + }; + + const handleTouchMove = (e: TouchEvent) => { + if (!isDragging()) return; + const deltaY = e.touches[0].clientY - startY(); + const newY = Math.max(0, deltaY); + setCurrentY(e.touches[0].clientY); + if (sheetRef) { + sheetRef.style.transform = `translateY(${newY}px)`; + } + e.preventDefault(); + }; + + const handleTouchEnd = () => { + if (!isDragging()) return; + setIsDragging(false); + const deltaY = currentY() - startY(); + if (deltaY > 100) { + local.onClose(); + } else { + if (sheetRef) sheetRef.style.transform = "translateY(0)"; + } + }; + + const handleOverlayClick = (e: MouseEvent) => { + if (local.closeOnOverlayClick && e.target === overlayRef) { + local.onClose(); + } + }; + + onMount(() => { + if (sheetRef && local.closeOnSwipeDown) { + sheetRef.addEventListener("touchstart", handleTouchStart, { + passive: true, + }); + sheetRef.addEventListener("touchmove", handleTouchMove, { + passive: false, + }); + sheetRef.addEventListener("touchend", handleTouchEnd, { passive: true }); + } + }); + + onCleanup(() => { + if (sheetRef) { + sheetRef.removeEventListener("touchstart", handleTouchStart); + sheetRef.removeEventListener("touchmove", handleTouchMove); + sheetRef.removeEventListener("touchend", handleTouchEnd); + } + }); + + return ( + <> +
+ +
+
+
+ {local.children} +
+
+ + ); +}; + +export default BottomSheet; diff --git a/src/components/bottom-sheet/index.ts b/src/components/bottom-sheet/index.ts new file mode 100644 index 0000000..a25abd2 --- /dev/null +++ b/src/components/bottom-sheet/index.ts @@ -0,0 +1,2 @@ +export type { BottomSheetProps } from "./BottomSheet"; +export { default } from "./BottomSheet"; diff --git a/src/index.ts b/src/index.ts index ea411b2..be29ce6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,8 @@ export { default as BrowserMockup, type BrowserMockupProps, } from "./components/browsermockup"; +export { default as BottomSheet } from "./components/bottom-sheet/BottomSheet"; +export type { BottomSheetProps } from "./components/bottom-sheet/BottomSheet"; export { default as Button } from "./components/button"; export { default as Calendar, type CalendarProps } from "./components/calendar"; export { default as Card } from "./components/card"; @@ -132,4 +134,4 @@ export { // Stores export * from "./stores"; -export { default } from "./components/connectionstatus"; \ No newline at end of file +export { default } from "./components/connectionstatus";