Skip to content

Commit

Permalink
feat: refactor to xstate (stipsan#46)
Browse files Browse the repository at this point in the history
By refactoring out the useEffect mess and replacing it with a proper state machine the following improvements were made:
- new event type `SNAP`, fired whenever transitioning to a snap point. Usually when dragging ends, or when `ref.snapTo` got called.
- new data attribute added: `data-rsbs-state`, it can be set to any of `closed | opening | open | dragging | snapping | resizing | closing`. 
- resize events can no longer happen while closing, fixing a bug in Android when closing a bottom sheet with a soft keyboard active.
  • Loading branch information
stipsan committed Dec 28, 2020
1 parent cd490e7 commit 6b2f92a
Show file tree
Hide file tree
Showing 11 changed files with 750 additions and 272 deletions.
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ iOS Safari, and some other mobile culprits, can be tricky if you're on a page th
## Events
All events receive `SprinngEvent` as their argument. It has a single property, `type`, which can be `'OPEN' | 'RESIZE' | 'CLOSE'` depending on the scenario.
All events receive `SprinngEvent` as their argument. It has a single property, `type`, which can be `'OPEN' | 'RESIZE' | 'SNAP' | 'CLOSE'` depending on the scenario.
### onSpringStart
Type: `(event: SpringEvent) => void`
Fired on: `OPEN | RESIZE | CLOSE`.
Fired on: `OPEN | RESIZE | SNAP | CLOSE`.
If you need to delay the open animation until you're ready, perhaps you're loading some data and showing an inline spinner meanwhile. You can return a Promise or use an async function to make the bottom sheet wait for your work to finish before it starts the open transition.
Expand All @@ -170,8 +170,6 @@ function Example() {
}
```
The `CLOSE` event also supports async/await and promises, if you need to delay the close transition. The `RESIZE` event does not await on anything, but nothing bad will happen if you give it an async function.
### onSpringCancel
Type: `(event: SpringEvent) => void`
Expand All @@ -185,18 +183,27 @@ In order to be as fluid and delightful as possible, the open state can be interr
- the user swipes the sheet below the fold, triggering an `onDismiss` event.
- the user hits the `esc` key, triggering an `onDismiss` event.
- the parent component sets `open` to `false` before finishing the animation.
- a `RESIZE` event happens, like when an Android device shows its soft keyboard when an text editable input receives focus, as it changes the viewport height.
#### CLOSE
If the user reopens the sheet before it's done animating it'll trigger this event. Most importantly though it can fire if the bottom sheet is unmounted without enough time to clean animate itself out of the view before it rolls back things like `body-scroll-lock`, `focus-trap` and more. It'll still clean itself up even if React decides to be rude about it. But this also means that the event can fire after the component is unmounted, so you should avoid calling setState or similar without checking for the mounted status of your own wrapper component.
#### RESIZE
Fires whenever there's been a window resize event, or if the header, footer or content have changed its height in such a way that the valid snap points have changed. In the future (#53) you'll be able to differentiate between what triggered the resize.
#### SNAP
Fired after dragging ends, or when calling `ref.snapTo`, and a transition to a valid snap point is happening.
### onSpringEnd
Type: `(event: SpringEvent) => void`
Fired on: `CLOSE`.
The `yin` to `onSpringStart`'s `yang`. It has the same characteristics. `RESIZE` don't mind if you give it an async function, but it also won't wait for it to finish before carrying on with the resizing. `OPEN` is siding with `RESIZE` on this one too while `CLOSE` still supports awaiting on async work. For `CLOSE` it gives you a hook into the step right after it has cleaned up everything after itself, and right before it unmounts itself. This can be useful if you have some logic that needs to perform some work before it's safe to unmount.
The `yin` to `onSpringStart`'s `yang`. It has the same characteristics. Including `async/await` and Promise support for delaying a transition. For `CLOSE` it gives you a hook into the step right after it has cleaned up everything after itself, and right before it unmounts itself. This can be useful if you have some logic that needs to perform some work before it's safe to unmount.
## ref
Expand All @@ -216,7 +223,7 @@ 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:
```js
ef.current.snapTo(({ // Showing all the available props
ref.current.snapTo(({ // Showing all the available props
headerHeight, footerHeight, height, minHeight, maxHeight, snapPoints, lastSnap }) =>
// Selecting the largest snap point, if you give it a number that doesn't match a snap point then it'll
// select whichever snap point is nearest the value you gave
Expand Down
41 changes: 40 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
"types": "dist/index.d.ts",
"dependencies": {
"@reach/portal": "^0.12.1",
"@xstate/react": "^1.2.0",
"body-scroll-lock": "^3.1.5",
"focus-trap": "^6.2.2",
"react-spring": "^8.0.27",
"react-use-gesture": "^8.0.1",
"resize-observer-polyfill": "^1.5.1"
"resize-observer-polyfill": "^1.5.1",
"xstate": "^4.15.1"
},
"peerDependencies": {
"react": "^16.14.0 || 17"
Expand All @@ -53,6 +55,7 @@
"@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0",
"@use-it/interval": "^1.0.0",
"@xstate/inspect": "^0.3.0",
"autoprefixer": "^10.0.4",
"babel-eslint": "^10.1.0",
"babel-plugin-transform-remove-console": "^6.9.4",
Expand Down
13 changes: 13 additions & 0 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { inspect } from '@xstate/inspect'
import type { InferGetStaticPropsType } from 'next'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import { capitalize } from '../docs/utils'
import { debugging } from '../src/utils'

import '../docs/style.css'
import '../src/style.css'

// Setup xstate debugging, but only when in dev mode
if (debugging) {
inspect({
url: 'https://statecharts.io/inspect',
iframe: false,
})
console.log(
'@xstate/inspect setup and running! Open https://statecharts.io/inspect in another tab to see the nitty gritty details. It also works with the Redux DevTools, but it lacks chart visualization.'
)
}

export async function getStaticProps() {
const [
{ version, description, homepage, name, meta = {} },
Expand Down

0 comments on commit 6b2f92a

Please sign in to comment.