Skip to content

Commit b9b32c6

Browse files
committed
Refactored side panel swiper component.
1 parent 41cb157 commit b9b32c6

File tree

7 files changed

+292
-317
lines changed

7 files changed

+292
-317
lines changed

src/components.d.ts

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -216,18 +216,6 @@ export namespace Components {
216216
interface MvxPreloader {
217217
"class"?: string;
218218
}
219-
interface MvxSidePanelSwiper {
220-
"close": () => Promise<void>;
221-
/**
222-
* @default false
223-
*/
224-
"open": boolean;
225-
"openToSnapPoint": (snapIndex?: number) => Promise<void>;
226-
/**
227-
* @default ''
228-
*/
229-
"sidePanelIdentifier": string;
230-
}
231219
interface MvxSignTransactionsPanel {
232220
"closeWithAnimation": () => Promise<unknown>;
233221
"getEventBus": () => Promise<IEventBus>;
@@ -396,10 +384,6 @@ export interface MvxPaginationEllipsisFormCustomEvent<T> extends CustomEvent<T>
396384
detail: T;
397385
target: HTMLMvxPaginationEllipsisFormElement;
398386
}
399-
export interface MvxSidePanelSwiperCustomEvent<T> extends CustomEvent<T> {
400-
detail: T;
401-
target: HTMLMvxSidePanelSwiperElement;
402-
}
403387
export interface MvxSimpleToastCustomEvent<T> extends CustomEvent<T> {
404388
detail: T;
405389
target: HTMLMvxSimpleToastElement;
@@ -698,24 +682,6 @@ declare global {
698682
prototype: HTMLMvxPreloaderElement;
699683
new (): HTMLMvxPreloaderElement;
700684
};
701-
interface HTMLMvxSidePanelSwiperElementEventMap {
702-
"sheetDismiss": void;
703-
"sheetSnapChange": { index: number; snapPoint: string };
704-
}
705-
interface HTMLMvxSidePanelSwiperElement extends Components.MvxSidePanelSwiper, HTMLStencilElement {
706-
addEventListener<K extends keyof HTMLMvxSidePanelSwiperElementEventMap>(type: K, listener: (this: HTMLMvxSidePanelSwiperElement, ev: MvxSidePanelSwiperCustomEvent<HTMLMvxSidePanelSwiperElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
707-
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
708-
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
709-
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
710-
removeEventListener<K extends keyof HTMLMvxSidePanelSwiperElementEventMap>(type: K, listener: (this: HTMLMvxSidePanelSwiperElement, ev: MvxSidePanelSwiperCustomEvent<HTMLMvxSidePanelSwiperElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
711-
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
712-
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
713-
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
714-
}
715-
var HTMLMvxSidePanelSwiperElement: {
716-
prototype: HTMLMvxSidePanelSwiperElement;
717-
new (): HTMLMvxSidePanelSwiperElement;
718-
};
719685
interface HTMLMvxSignTransactionsPanelElement extends Components.MvxSignTransactionsPanel, HTMLStencilElement {
720686
}
721687
var HTMLMvxSignTransactionsPanelElement: {
@@ -943,7 +909,6 @@ declare global {
943909
"mvx-passkey-provider-icon": HTMLMvxPasskeyProviderIconElement;
944910
"mvx-pending-transactions-panel": HTMLMvxPendingTransactionsPanelElement;
945911
"mvx-preloader": HTMLMvxPreloaderElement;
946-
"mvx-side-panel-swiper": HTMLMvxSidePanelSwiperElement;
947912
"mvx-sign-transactions-panel": HTMLMvxSignTransactionsPanelElement;
948913
"mvx-simple-toast": HTMLMvxSimpleToastElement;
949914
"mvx-spinner-icon": HTMLMvxSpinnerIconElement;
@@ -1160,18 +1125,6 @@ declare namespace LocalJSX {
11601125
interface MvxPreloader {
11611126
"class"?: string;
11621127
}
1163-
interface MvxSidePanelSwiper {
1164-
"onSheetDismiss"?: (event: MvxSidePanelSwiperCustomEvent<void>) => void;
1165-
"onSheetSnapChange"?: (event: MvxSidePanelSwiperCustomEvent<{ index: number; snapPoint: string }>) => void;
1166-
/**
1167-
* @default false
1168-
*/
1169-
"open"?: boolean;
1170-
/**
1171-
* @default ''
1172-
*/
1173-
"sidePanelIdentifier"?: string;
1174-
}
11751128
interface MvxSignTransactionsPanel {
11761129
}
11771130
interface MvxSimpleToast {
@@ -1344,7 +1297,6 @@ declare namespace LocalJSX {
13441297
"mvx-passkey-provider-icon": MvxPasskeyProviderIcon;
13451298
"mvx-pending-transactions-panel": MvxPendingTransactionsPanel;
13461299
"mvx-preloader": MvxPreloader;
1347-
"mvx-side-panel-swiper": MvxSidePanelSwiper;
13481300
"mvx-sign-transactions-panel": MvxSignTransactionsPanel;
13491301
"mvx-simple-toast": MvxSimpleToast;
13501302
"mvx-spinner-icon": MvxSpinnerIcon;
@@ -1407,7 +1359,6 @@ declare module "@stencil/core" {
14071359
"mvx-passkey-provider-icon": LocalJSX.MvxPasskeyProviderIcon & JSXBase.HTMLAttributes<HTMLMvxPasskeyProviderIconElement>;
14081360
"mvx-pending-transactions-panel": LocalJSX.MvxPendingTransactionsPanel & JSXBase.HTMLAttributes<HTMLMvxPendingTransactionsPanelElement>;
14091361
"mvx-preloader": LocalJSX.MvxPreloader & JSXBase.HTMLAttributes<HTMLMvxPreloaderElement>;
1410-
"mvx-side-panel-swiper": LocalJSX.MvxSidePanelSwiper & JSXBase.HTMLAttributes<HTMLMvxSidePanelSwiperElement>;
14111362
"mvx-sign-transactions-panel": LocalJSX.MvxSignTransactionsPanel & JSXBase.HTMLAttributes<HTMLMvxSignTransactionsPanelElement>;
14121363
"mvx-simple-toast": LocalJSX.MvxSimpleToast & JSXBase.HTMLAttributes<HTMLMvxSimpleToastElement>;
14131364
"mvx-spinner-icon": LocalJSX.MvxSpinnerIcon & JSXBase.HTMLAttributes<HTMLMvxSpinnerIconElement>;

src/components/visual/SidePanel/SidePanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { h } from '@stencil/core';
22
import classNames from 'classnames';
33
import { SidePanelHeader } from './components/SidePanelHeader/SidePanelHeader';
4+
import { SidePanelSwiper } from './components/SidePanelSwiper/SidePanelSwiper';
45

56
// prettier-ignore
67
// const styles = {
@@ -58,10 +59,9 @@ export function SidePanel({
5859
visible: shouldAnimate,
5960
})}
6061
>
61-
<mvx-side-panel-swiper
62+
<SidePanelSwiper
6263
open={shouldAnimate}
6364
onSheetDismiss={onClose}
64-
sidePanelIdentifier={sidePanelIdentifier}
6565
>
6666
<div
6767
id={sidePanelIdentifier}
@@ -85,7 +85,7 @@ export function SidePanel({
8585
{children}
8686
</div>
8787
</div>
88-
</mvx-side-panel-swiper>
88+
</SidePanelSwiper>
8989
</div>
9090
);
9191
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { h } from '@stencil/core';
2+
import state from './sidePanelSwiperStore';
3+
import styles from './sidePanelSwiper.styles';
4+
5+
interface SidePanelSwiperPropsType {
6+
open: boolean;
7+
onSheetDismiss?: () => void;
8+
onSheetSnapChange?: (index: number, snapPoint: string) => void;
9+
}
10+
11+
let hasInitialized = false;
12+
let previousOpen: boolean | null = null;
13+
14+
const snapPointsArray: string[] = ['100%'];
15+
let sheetElement: HTMLElement | null = null;
16+
17+
let dragState = {
18+
startY: 0,
19+
currentY: 0,
20+
startTransform: 100,
21+
isAnimating: false,
22+
};
23+
24+
let isDragging = false;
25+
26+
export function SidePanelSwiper({ open = false, onSheetDismiss, onSheetSnapChange }: SidePanelSwiperPropsType, children: JSX.Element) {
27+
const handleSheetDismiss = () => {
28+
onSheetDismiss?.();
29+
}
30+
31+
const animateToPosition = (snapIndex: number, emitEvent: boolean = true) => {
32+
if (!sheetElement || dragState.isAnimating) {
33+
return;
34+
}
35+
36+
const snapPercent = parseFloat(snapPointsArray[snapIndex] || '50');
37+
const targetY = 100 - snapPercent;
38+
39+
dragState.isAnimating = true;
40+
dragState.startTransform = targetY;
41+
42+
sheetElement.style.transition = 'transform 350ms cubic-bezier(0.25, 0.46, 0.45, 0.94)';
43+
sheetElement.style.transform = `translateY(${targetY}%)`;
44+
45+
setTimeout(() => {
46+
dragState.isAnimating = false;
47+
if (emitEvent && state.isVisible) {
48+
onSheetSnapChange?.(snapIndex, snapPointsArray[snapIndex]);
49+
}
50+
if (sheetElement) {
51+
sheetElement.style.transition = '';
52+
}
53+
}, 350);
54+
}
55+
56+
const openToSnapPoint = (snapIndex: number = 1) => {
57+
if (dragState.isAnimating) {
58+
return;
59+
}
60+
61+
state.currentSnapIndex = Math.max(0, Math.min(snapIndex, snapPointsArray.length - 1));
62+
state.isVisible = true;
63+
64+
setTimeout(() => {
65+
if (sheetElement && state.isVisible) {
66+
animateToPosition(state.currentSnapIndex, false);
67+
}
68+
}, 50);
69+
}
70+
71+
const animateToClose = () => {
72+
if (!sheetElement || dragState.isAnimating) {
73+
return;
74+
}
75+
76+
dragState.isAnimating = true;
77+
sheetElement.style.transition = 'transform 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94)';
78+
sheetElement.style.transform = 'translateY(100%)';
79+
80+
setTimeout(() => {
81+
dragState.isAnimating = false;
82+
state.isVisible = false;
83+
handleSheetDismiss();
84+
if (sheetElement) {
85+
sheetElement.style.transition = '';
86+
}
87+
}, 300);
88+
}
89+
90+
const closeSwiper = () => {
91+
if (dragState.isAnimating || !state.isVisible) {
92+
return;
93+
}
94+
95+
animateToClose();
96+
}
97+
98+
if (previousOpen !== null && previousOpen !== open) {
99+
if (open && !state.isVisible) {
100+
openToSnapPoint(state.currentSnapIndex);
101+
} else if (!open && state.isVisible) {
102+
closeSwiper();
103+
}
104+
}
105+
106+
previousOpen = open;
107+
108+
const setSheetRef = (el: HTMLElement | null) => {
109+
sheetElement = el;
110+
111+
if (el && !hasInitialized) {
112+
hasInitialized = true;
113+
state.isVisible = open;
114+
115+
if (window.innerWidth <= 480) {
116+
el.style.transform = 'translateY(100%)';
117+
}
118+
119+
if (open) {
120+
openToSnapPoint(state.currentSnapIndex);
121+
}
122+
}
123+
}
124+
125+
126+
const handleDragStart = (e: MouseEvent | TouchEvent) => {
127+
if (dragState.isAnimating) {
128+
return;
129+
}
130+
131+
e.preventDefault();
132+
e.stopPropagation();
133+
134+
isDragging = true;
135+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
136+
dragState.startY = clientY;
137+
dragState.currentY = clientY;
138+
139+
// Get current transform
140+
const transform = getCurrentTransform();
141+
dragState.startTransform = transform;
142+
143+
// Add global event listeners
144+
document.addEventListener('mousemove', handleDragMove, {
145+
passive: false,
146+
});
147+
document.addEventListener('touchmove', handleDragMove, {
148+
passive: false,
149+
});
150+
document.addEventListener('mouseup', handleDragEnd);
151+
document.addEventListener('touchend', handleDragEnd);
152+
};
153+
154+
const handleDragMove = (e: MouseEvent | TouchEvent) => {
155+
if (!isDragging || !sheetElement || dragState.isAnimating) {
156+
return;
157+
}
158+
159+
e.preventDefault();
160+
e.stopPropagation();
161+
162+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
163+
dragState.currentY = clientY;
164+
165+
const deltaY = dragState.currentY - dragState.startY;
166+
const viewportHeight = window.innerHeight;
167+
const deltaPercent = (deltaY / viewportHeight) * 100;
168+
169+
const newTransform = Math.min(100, Math.max(0, dragState.startTransform + deltaPercent));
170+
171+
sheetElement.style.transform = `translateY(${newTransform}%)`;
172+
};
173+
174+
const handleDragEnd = () => {
175+
if (!isDragging || dragState.isAnimating) {
176+
return;
177+
}
178+
179+
isDragging = false;
180+
181+
// Remove global event listeners
182+
document.removeEventListener('mousemove', handleDragMove);
183+
document.removeEventListener('touchmove', handleDragMove);
184+
document.removeEventListener('mouseup', handleDragEnd);
185+
document.removeEventListener('touchend', handleDragEnd);
186+
187+
const currentTransform = getCurrentTransform();
188+
const velocity = dragState.currentY - dragState.startY;
189+
190+
// Close if dragged down significantly or fast downward velocity
191+
if (currentTransform > 70 || velocity > 150) {
192+
closeSwiper();
193+
return;
194+
}
195+
196+
// Find closest snap point
197+
const snapPercentages = snapPointsArray.map(point => parseFloat(point));
198+
let closestIndex = 0;
199+
let closestDistance = Math.abs(100 - currentTransform - snapPercentages[0]);
200+
201+
for (let i = 1; i < snapPercentages.length; i++) {
202+
const distance = Math.abs(100 - currentTransform - snapPercentages[i]);
203+
if (distance < closestDistance) {
204+
closestDistance = distance;
205+
closestIndex = i;
206+
}
207+
}
208+
209+
state.currentSnapIndex = closestIndex;
210+
animateToPosition(closestIndex, true);
211+
};
212+
213+
const getCurrentTransform = (): number => {
214+
if (!sheetElement) {
215+
return 100;
216+
}
217+
218+
const transform = sheetElement.style.transform;
219+
if (transform && transform.includes('translateY')) {
220+
const match = transform.match(/translateY\(([^)]+)%?\)/);
221+
if (match) {
222+
return parseFloat(match[1].replace('%', ''));
223+
}
224+
}
225+
return 100;
226+
}
227+
return (
228+
<div class={styles.sidePanelSwiperContainer}>
229+
<div class={{ [styles.sidePanelSwiperWrapper]: true, [styles.sidePanelSwiperWrapperVisible]: state.isVisible, [styles.sidePanelSwiperWrapperHidden]: !state.isVisible }}>
230+
<div
231+
class={styles.sidePanelSwiper}
232+
ref={setSheetRef}
233+
onClick={(event: MouseEvent) => event.stopPropagation()}
234+
>
235+
<div class={styles.sidePanelSwiperHandleWrapper}>
236+
<div
237+
class={styles.sidePanelSwiperHandleContainer}
238+
onMouseDown={handleDragStart}
239+
onTouchStart={handleDragStart}
240+
>
241+
<div class={styles.sidePanelSwiperHandle} />
242+
</div>
243+
</div>
244+
245+
<div class={styles.sidePanelSwiperContent}>
246+
{children}
247+
</div>
248+
</div>
249+
</div>
250+
</div>
251+
);
252+
}
253+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// prettier-ignore
2+
export default {
3+
sidePanelSwiperContainer: 'side-panel-swipper-container mvx:flex mvx:xs:flex-col mvx:xs:h-full',
4+
sidePanelSwiperWrapper: 'side-panel-swipper-wrapper mvx:fixed mvx:left-0 mvx:top-0 mvx:bottom-0 mvx:right-0 mvx:z-50 mvx:xs:static mvx:xs:h-full mvx:before:opacity-90 mvx:before:left-0 mvx:before:top-0 mvx:before:right-0 mvx:before:bottom-0 mvx:before:transition-all mvx:before:duration-200 mvx:before:pointer-events-none mvx:before:absolute mvx:before:ease-in-out mvx:before:bg-neutral-900 mvx:before:content-[""] mvx:before:supports-[backdrop-filter]:opacity-50 mvx:before:supports-[backdrop-filter]:backdrop-blur-sm mvx:before:supports-[backdrop-filter]:bg-neutral-900 mvx:xs:before:content-none',
5+
sidePanelSwiperWrapperVisible: 'side-panel-swiper-visible mvx:!flex',
6+
sidePanelSwiperWrapperHidden: 'side-panel-swiper-wrapper-hidden mvx:hidden mvx:xs:block',
7+
sidePanelSwiperHidden: 'side-panel-swiper-hidden mvx:translate-y-full',
8+
sidePanelSwiper: 'side-panel-swiper mvx:bottom-0 mvx:absolute mvx:left-0 mvx:right-0 mvx:flex mvx:flex-col mvx:justify-end mvx:touch-pan-y mvx:h-auto mvx:min-h-dvh mvx:rounded-t-3xl mvx:transition-none mvx:backface-hidden mvx:will-change-transform mvx:xs:h-full mvx:xs:static mvx:xs:rounded-none mvx:xs:transform-none mvx:xs:[justify-content:unset] mvx:xs:min-h-auto',
9+
sidePanelSwiperHandleWrapper: 'side-panel-swiper-handle-wrapper mvx:top-8 mvx:relative mvx:h-8 mvx:w-full mvx:z-12 mvx:xs:hidden',
10+
sidePanelSwiperHandleContainer: 'side-panel-swiper-handle-container mvx:flex mvx:top-0 mvx:bottom-0 mvx:absolute mvx:right-0 mvx:left-0 mvx:justify-center mvx:touch-none mvx:select-none mvx:cursor-grab mvx:active:cursor-grabbing',
11+
sidePanelSwiperHandle: 'side-panel-swiper-handle mvx:w-32 mvx:mt-3 mvx:h-1 mvx:rounded mvx:bg-primary',
12+
sidePanelSwiperContent: 'side-panel-swiper-content mvx:overflow-y-auto mvx:max-h-[calc(100dvh-4rem)] mvx:xs:max-h-none mvx:xs:h-full'
13+
} satisfies Record<string, string>;

0 commit comments

Comments
 (0)