UNPKG

react-modal-sheet

Version:

Flexible bottom sheet component for your React apps

243 lines (225 loc) 6.23 kB
import type { SheetSnapPoint } from './types'; import { isAscendingOrder } from './utils'; /** * Convert negative / percentage snap points to absolute values * * Example output: * * ```json * [ * { * "snapIndex": 0, // <-- bottom snap point * "snapValue": 0, * "snapValueY": 810 * }, * { * "snapIndex": 1, * "snapValue": 170, * "snapValueY": 640 * }, * { * "snapIndex": 2, * "snapValue": 405, * "snapValueY": 405 * }, * { * "snapIndex": 3, * "snapValue": 760, * "snapValueY": 50 * }, * { * "snapIndex": 4, // <-- top snap point * "snapValue": 810, * "snapValueY": 0 * } * ] * ``` */ export function computeSnapPoints({ snapPointsProp, sheetHeight, }: { snapPointsProp: number[]; sheetHeight: number; }): SheetSnapPoint[] { if (snapPointsProp[0] !== 0) { console.error( 'First snap point should be 0 to ensure the sheet can be fully closed. ' + `Got: [${snapPointsProp.join(', ')}]` ); snapPointsProp.unshift(0); } if (snapPointsProp[snapPointsProp.length - 1] !== 1) { console.error( 'Last snap point should be 1 to ensure the sheet can be fully opened. ' + `Got: [${snapPointsProp.join(', ')}]` ); snapPointsProp.push(1); } if (sheetHeight <= 0) { console.error( `Sheet height is ${sheetHeight}, cannot compute snap points. ` + 'Make sure the sheet is mounted and has a valid height.' ); return []; } const snapPointValues = snapPointsProp.map((point) => { // Percentage values e.g. between 0.0 and 1.0 if (point > 0 && point <= 1) { return Math.round(point * sheetHeight); } return point < 0 ? sheetHeight + point : point; // negative values }); console.assert( isAscendingOrder(snapPointValues), `Snap points need to be in ascending order got: [${snapPointsProp.join(', ')}]` ); // Make sure all snap points are within the sheet height snapPointValues.forEach((snap) => { if (snap < 0 || snap > sheetHeight) { console.warn( `Snap point ${snap} is outside of the sheet height ${sheetHeight}. ` + 'This can cause unexpected behavior. Consider adjusting your snap points.' ); } }); if (!snapPointValues.includes(sheetHeight)) { console.warn( 'Snap points do not include the sheet height.' + 'Please include `1` as the last snap point or it will be included automatically.' + 'This is to ensure the sheet can be fully opened.' ); snapPointValues.push(sheetHeight); } return snapPointValues.map((snap, index) => ({ snapIndex: index, snapValue: snap, // Absolute value from the bottom of the sheet snapValueY: sheetHeight - snap, // Y value is inverted as `y = 0` means sheet is at the top })); } function findClosestSnapPoint({ snapPoints, currentY, }: { snapPoints: SheetSnapPoint[]; currentY: number; }) { return snapPoints.reduce((closest, snap) => Math.abs(snap.snapValueY - currentY) < Math.abs(closest.snapValueY - currentY) ? snap : closest ); } function findNextSnapPointInDirection({ y, snapPoints, dragDirection, }: { y: number; snapPoints: SheetSnapPoint[]; dragDirection: 'up' | 'down'; }) { // NOTE: lower Y means higher in the sheet position! if (dragDirection === 'down') { /** * Example: * * [ * { snapIndex: 0, snapValueY: 810 }, * { snapIndex: 1, snapValueY: 640 }, * { snapIndex: 2, snapValueY: 405 }, <-- next down * ------------- Y = 60 ------------ * { snapIndex: 3, snapValueY: 50 }, * { snapIndex: 4, snapValueY: 0 }, * ] */ return snapPoints .slice() .reverse() .find((s) => s.snapValueY > y); } else { /** * Example: * [ * { snapIndex: 0, snapValueY: 810 }, * { snapIndex: 1, snapValueY: 640 }, * { snapIndex: 2, snapValueY: 405 }, * ------------- Y = 60 ------------ * { snapIndex: 3, snapValueY: 50 }, <-- next up * { snapIndex: 4, snapValueY: 0 }, * ] */ return snapPoints.find((s) => s.snapValueY < y); } } export function handleHighVelocityDrag({ dragDirection, snapPoints, }: { dragDirection: 'up' | 'down'; snapPoints: SheetSnapPoint[]; }) { // Go to either the last or the first snap point depending on the direction const bottomSnapPoint = snapPoints[0]; const topSnapPoint = snapPoints[snapPoints.length - 1]; if (dragDirection === 'down') { return { yTo: bottomSnapPoint.snapValueY, snapIndex: bottomSnapPoint.snapIndex, }; } return { yTo: topSnapPoint.snapValueY, snapIndex: topSnapPoint.snapIndex, }; } export function handleLowVelocityDrag({ currentSnapPoint, currentY, dragDirection, snapPoints, velocity, }: { currentSnapPoint: SheetSnapPoint; currentY: number; dragDirection: 'up' | 'down'; snapPoints: SheetSnapPoint[]; velocity: number; }) { const closestSnapRelativeToCurrentY = findClosestSnapPoint({ snapPoints, currentY, }); /** * If velocity is very low the user has stopped the sheet to a specific * position and we should snap to the closest snap point as there is no * "momentum" that would push the sheet further to the given direction */ if (Math.abs(velocity) < 20) { return { yTo: closestSnapRelativeToCurrentY.snapValueY, snapIndex: closestSnapRelativeToCurrentY.snapIndex, }; } /** * If the dragging has a bit more velocity, we instead want to go to * the next snap point in the given direction if it exists */ const nextSnapInDirectionRelativeToCurrentY = findNextSnapPointInDirection({ y: currentY, snapPoints, dragDirection, }); if (nextSnapInDirectionRelativeToCurrentY) { return { yTo: nextSnapInDirectionRelativeToCurrentY.snapValueY, snapIndex: nextSnapInDirectionRelativeToCurrentY.snapIndex, }; } // No snap point down, stay at current return { yTo: currentSnapPoint.snapValueY, snapIndex: currentSnapPoint.snapIndex, }; }