diff --git a/README.md b/README.md index be218a92..84921bd8 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ function MyComponent() { Open +

Content

@@ -52,13 +53,13 @@ Additional props: `closeThreshold`: Number between 0 and 1 that determines when the drawer should be closed. Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more. -`scrollLockTimeout`: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms +`scrollLockTimeout`: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms. `snapPoints`: Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Example `[0.2, 0.5, 0.8]`. You can also use px values, which doesn't take screen height into account. `fadeFromIndex`: Index of a `snapPoint` from which the overlay fade should be applied. Defaults to the last snap point. -`modal`: When `false`it allows to interact with elements outside of the drawer without closing it. Defaults to`true`. +`modal`: When `false` it allows to interact with elements outside of the drawer without closing it. Defaults to `true`. ### Trigger @@ -84,6 +85,10 @@ An optional accessible description to be announced when the dialog is opened. [P The button that closes the dialog. [Props](https://www.radix-ui.com/docs/primitives/components/dialog#close). +### Handle + +A drag hint (also known as grabber). Shows people that they can drag the drawer to resize it; they can also tap it to cycle through the snap points, and double tap quickly to close the drawer. Set `preventCycle={false}` to stop this default behavior. + ### Portal Portals your drawer into the body. diff --git a/src/context.ts b/src/context.ts index 4b9113bb..7f5c3075 100644 --- a/src/context.ts +++ b/src/context.ts @@ -12,6 +12,7 @@ interface DrawerContextValue { onNestedRelease: (event: React.PointerEvent, open: boolean) => void; dismissible: boolean; isOpen: boolean; + isDragging: false; keyboardIsOpen: React.MutableRefObject; snapPointsOffset: number[] | null; snapPoints?: (number | string)[] | null; @@ -39,6 +40,7 @@ export const DrawerContext = React.createContext({ openProp: undefined, dismissible: false, isOpen: false, + isDragging: false, keyboardIsOpen: { current: false }, snapPointsOffset: null, snapPoints: null, diff --git a/src/index.tsx b/src/index.tsx index 16df95c0..dba9e9f0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -655,6 +655,7 @@ function Root({ onDrag, dismissible, isOpen, + isDragging, shouldFade, closeDrawer, onNestedDrag, @@ -672,6 +673,42 @@ function Root({ ); } +type HandleProps = React.HTMLAttributes & { + preventCycle?: boolean; +}; + +const Handle = React.forwardRef(({ preventCycle = false, ...rest }, ref) => { + const { visible, closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible } = + useDrawerContext(); + + function handleCycleSnapPoints() { + // Prevent accidental taps while resizing drawer + if (isDragging || preventCycle) return; + + const isLastSnapPoint = activeSnapPoint === snapPoints?.[snapPoints?.length - 1] ?? null; + if (!snapPoints || (isLastSnapPoint && dismissible)) { + closeDrawer(); + return; + } + + const nextSnapPoint = snapPoints[snapPoints.findIndex((point) => point === activeSnapPoint) + 1]; + setActiveSnapPoint(nextSnapPoint); + } + + return ( +