Skip to content

Commit 6b2f92a

Browse files
authored
feat: refactor to xstate (#46)
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.
1 parent cd490e7 commit 6b2f92a

File tree

11 files changed

+750
-272
lines changed

11 files changed

+750
-272
lines changed

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,13 @@ iOS Safari, and some other mobile culprits, can be tricky if you're on a page th
140140
141141
## Events
142142
143-
All events receive `SprinngEvent` as their argument. It has a single property, `type`, which can be `'OPEN' | 'RESIZE' | 'CLOSE'` depending on the scenario.
143+
All events receive `SprinngEvent` as their argument. It has a single property, `type`, which can be `'OPEN' | 'RESIZE' | 'SNAP' | 'CLOSE'` depending on the scenario.
144144
145145
### onSpringStart
146146
147147
Type: `(event: SpringEvent) => void`
148148
149-
Fired on: `OPEN | RESIZE | CLOSE`.
149+
Fired on: `OPEN | RESIZE | SNAP | CLOSE`.
150150
151151
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.
152152
@@ -170,8 +170,6 @@ function Example() {
170170
}
171171
```
172172
173-
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.
174-
175173
### onSpringCancel
176174
177175
Type: `(event: SpringEvent) => void`
@@ -185,18 +183,27 @@ In order to be as fluid and delightful as possible, the open state can be interr
185183
- the user swipes the sheet below the fold, triggering an `onDismiss` event.
186184
- the user hits the `esc` key, triggering an `onDismiss` event.
187185
- the parent component sets `open` to `false` before finishing the animation.
186+
- 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.
188187
189188
#### CLOSE
190189
191190
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.
192191
192+
#### RESIZE
193+
194+
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.
195+
196+
#### SNAP
197+
198+
Fired after dragging ends, or when calling `ref.snapTo`, and a transition to a valid snap point is happening.
199+
193200
### onSpringEnd
194201
195202
Type: `(event: SpringEvent) => void`
196203
197204
Fired on: `CLOSE`.
198205
199-
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.
206+
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.
200207
201208
## ref
202209
@@ -216,7 +223,7 @@ Type: `(numberOrCallback: number | (state => number)) => void`
216223
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:
217224
218225
```js
219-
ef.current.snapTo(({ // Showing all the available props
226+
ref.current.snapTo(({ // Showing all the available props
220227
headerHeight, footerHeight, height, minHeight, maxHeight, snapPoints, lastSnap }) =>
221228
// Selecting the largest snap point, if you give it a number that doesn't match a snap point then it'll
222229
// select whichever snap point is nearest the value you gave

package-lock.json

Lines changed: 40 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
"types": "dist/index.d.ts",
3535
"dependencies": {
3636
"@reach/portal": "^0.12.1",
37+
"@xstate/react": "^1.2.0",
3738
"body-scroll-lock": "^3.1.5",
3839
"focus-trap": "^6.2.2",
3940
"react-spring": "^8.0.27",
4041
"react-use-gesture": "^8.0.1",
41-
"resize-observer-polyfill": "^1.5.1"
42+
"resize-observer-polyfill": "^1.5.1",
43+
"xstate": "^4.15.1"
4244
},
4345
"peerDependencies": {
4446
"react": "^16.14.0 || 17"
@@ -53,6 +55,7 @@
5355
"@typescript-eslint/eslint-plugin": "^4.9.0",
5456
"@typescript-eslint/parser": "^4.9.0",
5557
"@use-it/interval": "^1.0.0",
58+
"@xstate/inspect": "^0.3.0",
5659
"autoprefixer": "^10.0.4",
5760
"babel-eslint": "^10.1.0",
5861
"babel-plugin-transform-remove-console": "^6.9.4",

pages/_app.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1+
import { inspect } from '@xstate/inspect'
12
import type { InferGetStaticPropsType } from 'next'
23
import type { AppProps } from 'next/app'
34
import Head from 'next/head'
45
import { capitalize } from '../docs/utils'
6+
import { debugging } from '../src/utils'
57

68
import '../docs/style.css'
79
import '../src/style.css'
810

11+
// Setup xstate debugging, but only when in dev mode
12+
if (debugging) {
13+
inspect({
14+
url: 'https://statecharts.io/inspect',
15+
iframe: false,
16+
})
17+
console.log(
18+
'@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.'
19+
)
20+
}
21+
922
export async function getStaticProps() {
1023
const [
1124
{ version, description, homepage, name, meta = {} },

0 commit comments

Comments
 (0)