From 8331800f0b1e95fa495a408bdddf02c3fc8c9d83 Mon Sep 17 00:00:00 2001 From: Cody Olsen Date: Fri, 4 Dec 2020 22:25:25 +0100 Subject: [PATCH] feat: V2 (#17) BREAKING CHANGE: this is the real initial release, `v1` is fake software. --- GET_STARTED.md | 91 +++++++++++++++++++++++ README.md | 132 ++++++++++++++++++++++++++++++++-- docs/HeadTitle.tsx | 14 ---- docs/MetaTags.tsx | 103 ++++++++++++++++++++++++++ docs/fixtures/Container.tsx | 6 ++ docs/style.css | 3 + docs/utils.ts | 9 +++ package-lock.json | 12 ++-- package.json | 18 ++++- pages/_app.tsx | 26 ++++--- pages/_document.tsx | 2 +- pages/docs.tsx | 3 - pages/fixtures/aside.tsx | 23 +++++- pages/fixtures/scrollable.tsx | 23 +++++- pages/fixtures/simple.tsx | 23 +++++- pages/fixtures/sticky.tsx | 23 +++++- pages/index.tsx | 27 +++++-- postcss.config.js | 7 +- public/readme.svg | 24 +++++++ public/somecard.png | Bin 0 -> 21971 bytes src/BottomSheet.tsx | 10 ++- src/index.tsx | 5 ++ src/types.ts | 4 +- 23 files changed, 525 insertions(+), 63 deletions(-) create mode 100644 GET_STARTED.md delete mode 100644 docs/HeadTitle.tsx create mode 100644 docs/MetaTags.tsx create mode 100644 docs/utils.ts delete mode 100644 pages/docs.tsx create mode 100644 public/readme.svg create mode 100644 public/somecard.png diff --git a/GET_STARTED.md b/GET_STARTED.md new file mode 100644 index 00000000..e5f6290e --- /dev/null +++ b/GET_STARTED.md @@ -0,0 +1,91 @@ +# Get started + +## Installation + +```bash +npm i react-spring-bottom-sheet +``` + +## Basic usage + +```jsx +import { useState } from 'react' +import { BottomSheet } from 'react-spring-bottom-sheet' + +// if setting up the CSS is tricky, you can add this to your page somewhere: +// +import 'react-spring-bottom-sheet/dist/style.css' + +export default function Example() { + const [open, setOpen] = useState(false) + return ( + <> + + My awesome content here + + ) +} +``` + +## TypeScript + +TS support is baked in, and if you're using the `snapTo` API use `BottomSheetRef`: + +```tsx +import { useRef } from 'react' +import { BottomSheet, BottomSheetRef } from 'react-spring-bottom-sheet' + +export default function Example() { + const sheetRef = useRef() + return ( + + + + ) +} +``` + +## Customizing the CSS + +### Using CSS Custom Properties + +These are all the variables available to customize the look and feel when using the [provided](/src/style.css) CSS. + +```css +:root { + --rsbs-antigap-scale-y: 0; + --rsbs-backdrop-bg: rgba(0, 0, 0, 0.6); + --rsbs-backdrop-opacity: 1; + --rsbs-bg: #fff; + --rsbs-content-opacity: 1; + --rsbs-handle-bg: hsla(0, 0%, 0%, 0.14); + --rsbs-max-w: auto; + --rsbs-ml: env(safe-area-inset-left); + --rsbs-mr: env(safe-area-inset-right); + --rsbs-overlay-rounded: 16px; + --rsbs-overlay-translate-y: 0px; + --rsbs-overlay-h: 0px; +} +``` + +### Custom CSS + +It's recommended that you copy from [style.css](/src/style.css) into your own project, and add this to your `postcss.config.js` setup (`npm i postcss-custom-properties-fallback`): + +```js +module.exports = { + plugins: { + // Ensures the default variables are available + 'postcss-custom-properties-fallback': { + importFrom: require.resolve('react-spring-bottom-sheet/defaults.json'), + }, + }, +} +``` diff --git a/README.md b/README.md index 19337b84..f636f957 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,138 @@ -# react-spring-bottom-sheet +[![npm stat](https://img.shields.io/npm/dm/react-spring-bottom-sheet.svg?style=flat-square)](https://npm-stat.com/charts.html?package=react-spring-bottom-sheet) +[![npm version](https://img.shields.io/npm/v/react-spring-bottom-sheet.svg?style=flat-square)](https://www.npmjs.com/package/react-spring-bottom-sheet) +[![gzip size][gzip-badge]][unpkg-dist] +[![size][size-badge]][unpkg-dist] +[![module formats: cjs, es, and modern][module-formats-badge]][unpkg-dist] +[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release) -![Logo with the text Accessible, Delightful and Performant](https://user-images.githubusercontent.com/81981/101104249-0006a200-35cb-11eb-80bc-f8ec5fd453e0.png) +![Logo with the text Accessible, Delightful and Performant](https://react-spring-bottom-sheet.cocody.dev/readme.svg) +**react-spring-bottom-sheet** is built on top of **react-spring** and **react-use-gesture**. It busts the myth that accessibility and supporting keyboard navigation and screen readers are allegedly at odds with delightful, beautiful and highly animated UIs. Every animation and transition is implemented using CSS custom properties instead of manipulating them directly, allowing complete control over the experience from CSS alone. -# Work in progress! +# Install -Hold off using this until `v2` is out, or you're gonna have a _bad time_! +```bash +npm i react-spring-bottom-sheet +``` + +# [Demos](https://react-spring-bottom-sheet.cocody.dev/) + +## [Basic](https://react-spring-bottom-sheet.cocody.dev/fixtures/simple) + +> [View demo code](/pages/fixtures/simple.tsx#L43-L47) + +MVP example, showing what you get by implementing `open`, `onDismiss` and a single **snap point** always set to `minHeight`. + +## [Snap points & overflow](https://react-spring-bottom-sheet.cocody.dev/fixtures/scrollable) + +> [View demo code](/pages/fixtures/scrollable.tsx#L85-L95) + +A more elaborate example that showcases how snap points work. It also shows how it behaves if you want it to be open by default, and not closable. Notice how it responds if you resize the window, or scroll to the bottom and starts adjusting the height of the sheet without scrolling back up first. + +## [Sticky header & footer](https://react-spring-bottom-sheet.cocody.dev/fixtures/sticky) + +> [View demo code](/pages/fixtures/sticky.tsx#L40-L60) + +If you provide either a `header` or `footer` prop you'll enable the special behavior seen in this example. And they're not just sticky positioned, both areas support touch gestures. + +## [Non-blocking overlay mode](https://react-spring-bottom-sheet.cocody.dev/fixtures/aside) + +> [View demo code](/pages/fixtures/aside.tsx#L41-L46) + +In most cases you use a bottom sheet the same way you do with a dialog: you want it to overlay the page and block out distractions. But there are times when you want a bottom sheet but without it taking all the attention and overlaying the entire page. Providing `blocking={false}` helps this use case. By doing so you disable a couple of behaviors that are there for accessibility (focus-locking and more) that prevents a screen reader or a keyboard user from accidentally leaving the bottom sheet. + +# [Get started](/GET_STARTED.md) + +# API + +## props + +All props you provide, like `className`, `style` props or whatever else are spread onto the underlying `` instance, that you can style in your custom CSS using this selector: `[data-rsbs-root]`. +Just note that the component is mounted in a `@reach/portal` at the bottom of ``, and not in the DOM hierarchy you render it in. + +### open + +Type: `boolean` + +The only required prop. And it's controlled, so if you don't set this to `false` then it's not possible to close the bottom sheet. + +### onDismiss + +Type: `() => void` + +Called when the user do something that signal they want to dismiss the sheet: + +- hit the `esc` key. +- tap on the backdrop. +- swipes the sheet to the bottom of the viewport. + +### snapPoints + +Type: `(state) => number | number[]` + +This function should be pure as it's called often. You can choose to provide a single value or an array of values to customize the behavior. The `state` contains these values: + +- `headerHeight` – the current measured height of the `header`. +- `footerHeight` – if a `footer` prop is provided then this is its height. +- `height` – the current height of the sheet. +- `minHeight` – the minimum height needed to avoid a scrollbar. If there's not enough height available to avoid it then this will be the same as `maxHeight`. +- `maxHeight` – the maximum available height on the page, usually matches `window.innerHeight/100vh`. + +### defaultSnap + +Type: `number | (state) => number` + +Provide either a number, or a callback returning a number for the default position of the sheet when it opens. +`state` use the same arguments as `snapPoints`, plus two more values: `snapPoints` and `lastSnap`. + +### header + +Type: `ReactNode` + +Supports the same value type as the `children` prop. + +### footer + +Type: `ReactNode` + +Supports the same value type as the `children` prop. + +### initialFocusRef + +Type: `React.Ref` + +A react ref to the element you want to get keyboard focus when opening. If not provided it's automatically selecting the first interactive element it finds. + +### blocking + +Type: `boolean` + +Enabled by default. Enables focus trapping of keyboard navigation, so you can't accidentally tab out of the bottom sheet and into the background. Also sets `aria-hidden` on the rest of the page to prevent Screen Readers from escaping as well. + +## ref + +Methods available when setting a `ref` on the sheet: + +```jsx +export default function Example() { + const sheetRef = React.useRef() + return +} +``` + +### snapTo + +Type: `(numberOrCallback: number | (state => number)) => void` + +Same signature as the `defaultSnap` prop, calling it will animate the sheet to the new snap point you return. You can either call it with a number, which is the height in px (it'll select the closest snap point that matches your value): `ref.current.snapTo(200)`. Or `ref.current.snapTo(({headerHeight, footerHeight, height, minHeight, maxHeight, snapPoints, lastSnap}) => Math.max(...snapPoints))`. # Credits - Play icon used on frame overlays: https://fontawesome.com/icons/play-circle?style=regular - Phone frame used in logo: https://www.figma.com/community/file/896042888090872154/Mono-Devices-1.0 - iPhone frame used to wrap examples: https://www.figma.com/community/file/858143367356468985/(Variants)-iOS-%26-iPadOS-14-UI-Kit-for-Figma + +[gzip-badge]: http://img.badgesize.io/https://unpkg.com/react-spring-bottom-sheet/dist/index.es.js?compression=gzip&label=gzip%20size&style=flat-square +[size-badge]: http://img.badgesize.io/https://unpkg.com/react-spring-bottom-sheet/dist/index.es.js?label=size&style=flat-square +[unpkg-dist]: https://unpkg.com/react-spring-bottom-sheet/dist/ +[module-formats-badge]: https://img.shields.io/badge/module%20formats-cjs%2C%20es%2C%20modern-green.svg?style=flat-square diff --git a/docs/HeadTitle.tsx b/docs/HeadTitle.tsx deleted file mode 100644 index 80ea5c54..00000000 --- a/docs/HeadTitle.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import Head from 'next/head' - -const TITLE = 'React Spring Bottom Sheet' - -export default function HeadTitle({ children }: { children?: string }) { - return ( - - - {children ? `${children} | ` : null} - {TITLE} - - - ) -} diff --git a/docs/MetaTags.tsx b/docs/MetaTags.tsx new file mode 100644 index 00000000..0f6eb878 --- /dev/null +++ b/docs/MetaTags.tsx @@ -0,0 +1,103 @@ +import Head from 'next/head' + +export default function MetaTags({ + homepage, + description, + name, + title, + ...props +}: { + homepage?: string + description?: string + name: string + title?: string + ['twitter:title']?: string | false + ['og:title']?: string | false + ['og:site_name']?: string | false + ['twitter:image:src']?: string | false + ['og:image']?: string | false +}) { + const fallbackTitle = `$ npm i ${name}` + const twitterTitle = + props['twitter:title'] ?? (props['og:title'] || fallbackTitle) + const ogTitle = props['og:title'] ?? (props['twitter:title'] || fallbackTitle) + const twitterImage = props['twitter:image:src'] ?? props['og:image'] + const ogImage = props['og:image'] ?? props['twitter:image:src'] + const ogSiteName = props['og:site_name'] ?? name + const twitterSite = props['twitter:site'] + const twitterDescription = props['twitter:description'] ?? description + + return ( + + + {title ? `${title} | ` : null} + {props['og:site_name'] ?? name} + + {description && ( + + )} + {twitterSite && ( + <> + {twitterImage && ( + + )} + {twitterSite && ( + + )} + + {twitterTitle && ( + + )} + {twitterDescription && ( + + )} + + )} + {homepage && ( + <> + {ogImage && ( + + )} + {ogSiteName && ( + + )} + + {ogTitle && ( + + )} + + + + + )} + + ) +} diff --git a/docs/fixtures/Container.tsx b/docs/fixtures/Container.tsx index 97bd938b..dfb42712 100644 --- a/docs/fixtures/Container.tsx +++ b/docs/fixtures/Container.tsx @@ -1,6 +1,7 @@ import cx from 'classnames/dedupe' import { useEffect } from 'react' import { useDetectEnv } from './hooks' +import Link from 'next/link' export default function Container({ children, @@ -28,6 +29,11 @@ export default function Container({ className )} > + + + Close example + + {children} ) diff --git a/docs/style.css b/docs/style.css index d0f18925..dcdfde9d 100644 --- a/docs/style.css +++ b/docs/style.css @@ -30,4 +30,7 @@ --rsbs-ml: 0px; --rsbs-mr: 0px; } + .is-iframe .only-window { + display: none; + } } diff --git a/docs/utils.ts b/docs/utils.ts new file mode 100644 index 00000000..f2a2421a --- /dev/null +++ b/docs/utils.ts @@ -0,0 +1,9 @@ +// @stipsan/react-spring => React Spring +export function capitalize(str) { + return str + .split('/') + .pop() + .split('-') + .map((_) => _.charAt(0).toUpperCase() + _.slice(1)) + .join(' ') +} diff --git a/package-lock.json b/package-lock.json index bcf0717c..f96a5942 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2939,6 +2939,12 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true } } }, @@ -13025,12 +13031,6 @@ "lines-and-columns": "^1.1.6" } }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, "path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", diff --git a/package.json b/package.json index 6c265ea1..f25c4df8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "react-spring-bottom-sheet", + "description": "✨ Accessible, 🪄 Delightful, and 🤯 Performant. Built on react-spring for the web, and react-use-gesture.", "license": "MIT", "author": "Cody Olsen", "homepage": "https://react-spring-bottom-sheet.cocody.dev", @@ -11,13 +12,14 @@ "main": "dist/index.js", "module": "dist/index.es.js", "files": [ + "defaults.json", "dist" ], "scripts": { "build": "next build", "prebuild:dist": "rimraf dist/**", "build:dist": "npm run build:postcss && npm run build:microbundle", - "build:microbundle": "NODE_ENV=production microbundle --define process.env.NODE_ENV=production --tsconfig tsconfig.microbundle.json -f cjs,es", + "build:microbundle": "NODE_ENV=production microbundle --define process.env.NODE_ENV=production --tsconfig tsconfig.microbundle.json -f cjs,es,modern", "build:postcss": "postcss -d dist --no-map src/style.css", "dev": "next", "lint": "eslint . --ext ts,tsx,js,jsx --max-warnings 0 && tsc", @@ -53,7 +55,7 @@ "@typescript-eslint/eslint-plugin": "^4.9.0", "@typescript-eslint/parser": "^4.9.0", "@use-it/interval": "^1.0.0", - "autoprefixer": "10.0.4", + "autoprefixer": "^10.0.4", "babel-eslint": "^10.1.0", "babel-plugin-transform-remove-console": "^6.9.4", "classnames": "^2.2.6", @@ -84,6 +86,14 @@ "tailwindcss": "^2.0.1", "typescript": "^4.1.2" }, + "browserslist": [ + "Chrome >= 49", + "Android >= 58", + "Safari >= 9.1", + "iOS >= 9.3", + "Firefox >= 31", + "Edge >= 16" + ], "husky": { "hooks": { "pre-commit": "lint-staged" @@ -95,6 +105,10 @@ "*.{js,jsx,ts,tsx,md,html,css,yml,json}": "prettier --write", "package.json": "prettier-package-json --write" }, + "meta": { + "twitter:site": "@stipsan", + "twitter:image:src": "https://react-spring-bottom-sheet.cocody.dev/somecard.png" + }, "prettier": { "semi": false, "singleQuote": true diff --git a/pages/_app.tsx b/pages/_app.tsx index 00acac92..4d556b3a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,23 +1,31 @@ +import type { InferGetStaticPropsType } from 'next' import type { AppProps } from 'next/app' import Head from 'next/head' -import HeadTitle from '../docs/HeadTitle' +import { capitalize } from '../docs/utils' import '../docs/style.css' import '../src/style.css' -function _AppPage({ Component, pageProps }: AppProps) { +export async function getStaticProps() { + const { version, description, homepage, name, meta = {} } = await import( + '../package.json' + ) + if (!meta['og:site_name']) { + meta['og:site_name'] = capitalize(name) + } + + return { props: { version, description, homepage, name, meta } } +} + +export type GetStaticProps = InferGetStaticPropsType + +export default function _AppPage({ Component, pageProps }: AppProps) { return ( <> - + - ) } - -export default _AppPage diff --git a/pages/_document.tsx b/pages/_document.tsx index c180e97d..ed6e129a 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -30,12 +30,12 @@ gtag('config', '${ga}'); href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700;900&family=Source+Sans+Pro&display=swap" rel="stylesheet" /> - +
diff --git a/pages/docs.tsx b/pages/docs.tsx deleted file mode 100644 index 718990a5..00000000 --- a/pages/docs.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function DocsPage() { - return <>@TODO -} diff --git a/pages/fixtures/aside.tsx b/pages/fixtures/aside.tsx index dda05cdd..be5a150a 100644 --- a/pages/fixtures/aside.tsx +++ b/pages/fixtures/aside.tsx @@ -1,13 +1,22 @@ +import type { NextPage } from 'next' import { useEffect, useRef, useState } from 'react' import Button from '../../docs/fixtures/Button' import Code from '../../docs/fixtures/Code' import Container from '../../docs/fixtures/Container' import SheetContent from '../../docs/fixtures/SheetContent' import { aside } from '../../docs/headings' -import HeadTitle from '../../docs/HeadTitle' +import MetaTags from '../../docs/MetaTags' import { BottomSheet } from '../../src' +import type { GetStaticProps } from '../_app' -export default function AsideFixturePage() { +export { getStaticProps } from '../_app' + +const AsideFixturePage: NextPage = ({ + description, + homepage, + meta, + name, +}) => { const [open, setOpen] = useState(true) const focusRef = useRef() @@ -18,7 +27,13 @@ export default function AsideFixturePage() { return ( <> - {aside} + ) } + +export default SimpleFixturePage diff --git a/pages/fixtures/sticky.tsx b/pages/fixtures/sticky.tsx index 4ef2f4ff..c18b13d0 100644 --- a/pages/fixtures/sticky.tsx +++ b/pages/fixtures/sticky.tsx @@ -1,12 +1,21 @@ +import type { NextPage } from 'next' import { useEffect, useState } from 'react' import Button from '../../docs/fixtures/Button' import Container from '../../docs/fixtures/Container' import SheetContent from '../../docs/fixtures/SheetContent' import { sticky } from '../../docs/headings' -import HeadTitle from '../../docs/HeadTitle' +import MetaTags from '../../docs/MetaTags' import { BottomSheet } from '../../src' +import type { GetStaticProps } from '../_app' -export default function StickyFixturePage() { +export { getStaticProps } from '../_app' + +const StickyFixturePage: NextPage = ({ + description, + homepage, + meta, + name, +}) => { const [open, setOpen] = useState(false) useEffect(() => { @@ -19,7 +28,13 @@ export default function StickyFixturePage() { return ( <> - {sticky} + ) } + +export default StickyFixturePage diff --git a/pages/index.tsx b/pages/index.tsx index 87917741..24aaf763 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,10 +1,27 @@ +import type { NextPage } from 'next' import { aside, scrollable, simple, sticky } from '../docs/headings' import Hero from '../docs/Hero' import Nugget from '../docs/Nugget' import StickyNugget from '../docs/StickyNugget' +import MetaTags from '../docs/MetaTags' +import type { GetStaticProps } from './_app' -export default function IndexPage() { - return ( +export { getStaticProps } from './_app' + +const IndexPage: NextPage = ({ + name, + version, + description, + homepage, + meta, +}) => ( + <> +
@@ -40,5 +57,7 @@ export default function IndexPage() { />
- ) -} + +) + +export default IndexPage diff --git a/postcss.config.js b/postcss.config.js index caf4794f..02d3fd81 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,13 +1,12 @@ const path = require('path') +const importFrom = path.resolve(__dirname, './defaults.json') module.exports = { plugins: { tailwindcss: {}, - 'postcss-custom-properties-fallback': { - importFrom: path.resolve(__dirname, './defaults.json'), - }, + 'postcss-custom-properties-fallback': { importFrom }, // @TODO add importFrom to preset-env when CSS snapshot testing is in place - 'postcss-preset-env': { stage: 0 }, + 'postcss-preset-env': { importFrom, stage: 0 }, 'postcss-import-svg': { paths: [path.resolve(__dirname, 'docs')], svgo: { diff --git a/public/readme.svg b/public/readme.svg new file mode 100644 index 00000000..40b4cbb0 --- /dev/null +++ b/public/readme.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/somecard.png b/public/somecard.png new file mode 100644 index 0000000000000000000000000000000000000000..03d5057e84961cf22266c1130bafc7d070bde249 GIT binary patch literal 21971 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 literal 0 HcmV?d00001 diff --git a/src/BottomSheet.tsx b/src/BottomSheet.tsx index 24755b8a..f5164fe9 100644 --- a/src/BottomSheet.tsx +++ b/src/BottomSheet.tsx @@ -162,13 +162,17 @@ export const BottomSheet = React.forwardRef< if (off) return // @TODO refactor to setState and useEffect hooks to easier track cancel events + + const snap = findSnap(numberOrCallback) + lastSnapRef.current = snap + heightRef.current = snap set({ - y: findSnap(numberOrCallback), + y: snap, immediate: prefersReducedMotion.current, }) }, }), - [findSnap, off, prefersReducedMotion, set] + [findSnap, lastSnapRef, off, prefersReducedMotion, set] ) // Handle closed to open transition @@ -347,7 +351,7 @@ export const BottomSheet = React.forwardRef< // Set to false so the async flow can detect if it got cancelled cancelled = true } - }, [on, prefersReducedMotion, ready, set]) + }, [observeBoundsRef, on, prefersReducedMotion, ready, set]) const getY = ({ down, diff --git a/src/index.tsx b/src/index.tsx index bf6f0e32..1ce97985 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,11 @@ export const BottomSheet = forwardRef(function BottomSheet( if (props.open) { clearTimeout(timerRef.current) setMounted(true) + + // Cleanup defaultOpen state on close + return () => { + openRef.current = false + } } }, [props.open]) diff --git a/src/types.ts b/src/types.ts index c8674bc2..a34f8f85 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,7 +24,9 @@ type defaultSnap = (props: defaultSnapProps) => number /* Might make sense to expose a preventDefault method here */ export type SpringEvent = { - type: 'OPEN' | 'SNAP' | 'CLOSE' + type: 'OPEN' | 'CLOSE' + //type: 'OPEN' | 'SNAP' | 'CLOSE' + // @TODO implemen SNAP events } // Rename to Props! Woohoo!