import { createEffect, createRenderEffect, createSignal, onCleanup, Show, untrack, type Component, type Signal, } from "solid-js"; import { css } from "solid-styled"; import { Refresh as RefreshIcon } from "@suid/icons-material"; import { CircularProgress } from "@suid/material"; import { createEventListener, makeEventListener, } from "@solid-primitives/event-listener"; import { createViewportObserver, createVisibilityObserver, } from "@solid-primitives/intersection-observer"; const PullDownToRefresh: Component<{ loading?: boolean; linkedElement?: HTMLElement; onRefresh?: () => void; }> = (props) => { let rootElement: HTMLDivElement; const [pullDown, setPullDown] = createSignal(0); const stopPos = () => 160; const indicatorOfsY = () => { if (props.loading) { return stopPos() * 0.875; } return pullDown(); }; const obvx = createVisibilityObserver({ threshold: 0.0001, }); const rootVisible = obvx(() => rootElement); createEffect(() => { if (!rootVisible()) setPullDown(0); }); let released = true; let v = 0; let lts = -1; let ds = 0; let holding = false; const K = 10; const updatePullDown = (ts: number) => { released = false; try { const x = untrack(pullDown); const dt = lts !== -1 ? ts - lts : 1 / 60; const vspring = holding ? 0 : K * x * dt; v = ds / dt - vspring; setPullDown(Math.max(Math.min(x + v * dt, stopPos()), 0)); if (Math.abs(x) > 1 || Math.abs(v) > 1) { requestAnimationFrame(updatePullDown); } else { v = 0; lts = -1; } if ( !holding && untrack(pullDown) >= stopPos() && !props.loading && props.onRefresh ) { setTimeout(props.onRefresh, 0); } } finally { ds = 0; released = true; } }; let wheelTimeout: ReturnType | undefined; const onWheelNotUpdated = () => { wheelTimeout = undefined; holding = false; }; const handleLinkedWheel = (event: WheelEvent) => { const scrollTop = (event.target as HTMLElement).scrollTop; if (scrollTop >= 0 && scrollTop < 1) { const d = untrack(pullDown); if (d > 1) event.preventDefault(); ds = -(event.deltaY / window.devicePixelRatio / 2); holding = d < stopPos(); if (wheelTimeout) { clearTimeout(wheelTimeout); } wheelTimeout = setTimeout(onWheelNotUpdated, 200); if (released) { released = false; requestAnimationFrame(updatePullDown); } } }; createEffect((cleanup?: () => void) => { if (!rootVisible()) { return; } cleanup?.(); const element = props.linkedElement; if (!element) return; return makeEventListener(element, "wheel", handleLinkedWheel); }); let lastTouchId: number | undefined = undefined; let lastTouchScreenY = 0; const handleTouch = (event: TouchEvent) => { if (event.targetTouches.length > 1) { lastTouchId = 0; lastTouchScreenY = 0; return; } const item = event.targetTouches.item(0)!; if (lastTouchId && item.identifier !== lastTouchId) { lastTouchId = undefined; lastTouchScreenY = 0; return; } holding = true; if (lastTouchScreenY !== 0) { ds = item.screenY - lastTouchScreenY; } if (Math.abs(ds) > 1 && untrack(pullDown) > 1) event.preventDefault(); lastTouchScreenY = item.screenY; if (released) { released = false; requestAnimationFrame(updatePullDown); } }; const handleTouchEnd = () => { lastTouchId = undefined; lastTouchScreenY = 0; holding = false; if ( untrack(indicatorOfsY) >= stopPos() && !props.loading && props.onRefresh ) { setTimeout(props.onRefresh, 0); } else { if (released) { released = false; requestAnimationFrame(updatePullDown); } } }; createEffect((cleanup?: () => void) => { if (!rootVisible()) { return; } cleanup?.(); const element = props.linkedElement; if (!element) return; const cleanup0 = makeEventListener(element, "touchmove", handleTouch); const cleanup1 = makeEventListener(element, "touchend", handleTouchEnd); return () => (cleanup0(), cleanup1()); }); css` .pull-down { width: 100%; display: flex; justify-content: center; margin-top: -2rem; height: calc(1px + 2rem); } .indicator { display: inline-flex; justify-content: center; align-items: center; box-shadow: ${props.loading ? "var(--tutu-shadow-e12)" : "var(--tutu-shadow-e1)"}; border-radius: 50%; aspect-ratio: 1/1; width: 2rem; color: var(--tutu-color-primary); transform: translateY(${`${indicatorOfsY() - 2}px`}); will-change: transform; z-index: var(--tutu-zidx-nav); background-color: var(--tutu-color-surface); > :global(.refresh-icon) { transform: rotate( ${`${((indicatorOfsY() / 160) * 180).toString()}deg`} ); will-change: transform; } > :global(.refresh-indicator) { width: 1.5rem; height: 1.5rem; aspect-ratio: 1/1; } } `; return (
} >
); }; export default PullDownToRefresh;