Skip to content

Commit 8331800

Browse files
authored
feat: V2 (stipsan#17)
BREAKING CHANGE: this is the real initial release, `v1` is fake software.
1 parent e1f3436 commit 8331800

23 files changed

+525
-63
lines changed

GET_STARTED.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Get started
2+
3+
## Installation
4+
5+
```bash
6+
npm i react-spring-bottom-sheet
7+
```
8+
9+
## Basic usage
10+
11+
```jsx
12+
import { useState } from 'react'
13+
import { BottomSheet } from 'react-spring-bottom-sheet'
14+
15+
// if setting up the CSS is tricky, you can add this to your page somewhere:
16+
// <link rel="stylesheet" href="https://unpkg.com/react-spring-bottom-sheet/dist/style.css" crossorigin="anonymous">
17+
import 'react-spring-bottom-sheet/dist/style.css'
18+
19+
export default function Example() {
20+
const [open, setOpen] = useState(false)
21+
return (
22+
<>
23+
<button onClick={() => setOpen(true)}>Open</button>
24+
<BottomSheet open={open}>My awesome content here</BottomSheet>
25+
</>
26+
)
27+
}
28+
```
29+
30+
## TypeScript
31+
32+
TS support is baked in, and if you're using the `snapTo` API use `BottomSheetRef`:
33+
34+
```tsx
35+
import { useRef } from 'react'
36+
import { BottomSheet, BottomSheetRef } from 'react-spring-bottom-sheet'
37+
38+
export default function Example() {
39+
const sheetRef = useRef<BottomSheetRef>()
40+
return (
41+
<BottomSheet open ref={sheetRef}>
42+
<button
43+
onClick={() => {
44+
// Full typing for the arguments available in snapTo, yay!!
45+
sheetRef.current.snapTo(({ maxHeight }) => maxHeight)
46+
}}
47+
>
48+
Expand to full height
49+
</button>
50+
</BottomSheet>
51+
)
52+
}
53+
```
54+
55+
## Customizing the CSS
56+
57+
### Using CSS Custom Properties
58+
59+
These are all the variables available to customize the look and feel when using the [provided](/src/style.css) CSS.
60+
61+
```css
62+
:root {
63+
--rsbs-antigap-scale-y: 0;
64+
--rsbs-backdrop-bg: rgba(0, 0, 0, 0.6);
65+
--rsbs-backdrop-opacity: 1;
66+
--rsbs-bg: #fff;
67+
--rsbs-content-opacity: 1;
68+
--rsbs-handle-bg: hsla(0, 0%, 0%, 0.14);
69+
--rsbs-max-w: auto;
70+
--rsbs-ml: env(safe-area-inset-left);
71+
--rsbs-mr: env(safe-area-inset-right);
72+
--rsbs-overlay-rounded: 16px;
73+
--rsbs-overlay-translate-y: 0px;
74+
--rsbs-overlay-h: 0px;
75+
}
76+
```
77+
78+
### Custom CSS
79+
80+
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`):
81+
82+
```js
83+
module.exports = {
84+
plugins: {
85+
// Ensures the default variables are available
86+
'postcss-custom-properties-fallback': {
87+
importFrom: require.resolve('react-spring-bottom-sheet/defaults.json'),
88+
},
89+
},
90+
}
91+
```

README.md

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,138 @@
1-
# react-spring-bottom-sheet
1+
[![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)
2+
[![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)
3+
[![gzip size][gzip-badge]][unpkg-dist]
4+
[![size][size-badge]][unpkg-dist]
5+
[![module formats: cjs, es, and modern][module-formats-badge]][unpkg-dist]
6+
[![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)
27

3-
![Logo with the text Accessible, Delightful and Performant](https://user-images.githubusercontent.com/81981/101104249-0006a200-35cb-11eb-80bc-f8ec5fd453e0.png)
8+
![Logo with the text Accessible, Delightful and Performant](https://react-spring-bottom-sheet.cocody.dev/readme.svg)
49

10+
**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.
511

6-
# Work in progress!
12+
# Install
713

8-
Hold off using this until `v2` is out, or you're gonna have a _bad time_!
14+
```bash
15+
npm i react-spring-bottom-sheet
16+
```
17+
18+
# [Demos](https://react-spring-bottom-sheet.cocody.dev/)
19+
20+
## [Basic](https://react-spring-bottom-sheet.cocody.dev/fixtures/simple)
21+
22+
> [View demo code](/pages/fixtures/simple.tsx#L43-L47)
23+
24+
MVP example, showing what you get by implementing `open`, `onDismiss` and a single **snap point** always set to `minHeight`.
25+
26+
## [Snap points & overflow](https://react-spring-bottom-sheet.cocody.dev/fixtures/scrollable)
27+
28+
> [View demo code](/pages/fixtures/scrollable.tsx#L85-L95)
29+
30+
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.
31+
32+
## [Sticky header & footer](https://react-spring-bottom-sheet.cocody.dev/fixtures/sticky)
33+
34+
> [View demo code](/pages/fixtures/sticky.tsx#L40-L60)
35+
36+
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.
37+
38+
## [Non-blocking overlay mode](https://react-spring-bottom-sheet.cocody.dev/fixtures/aside)
39+
40+
> [View demo code](/pages/fixtures/aside.tsx#L41-L46)
41+
42+
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.
43+
44+
# [Get started](/GET_STARTED.md)
45+
46+
# API
47+
48+
## props
49+
50+
All props you provide, like `className`, `style` props or whatever else are spread onto the underlying `<animated.div>` instance, that you can style in your custom CSS using this selector: `[data-rsbs-root]`.
51+
Just note that the component is mounted in a `@reach/portal` at the bottom of `<body>`, and not in the DOM hierarchy you render it in.
52+
53+
### open
54+
55+
Type: `boolean`
56+
57+
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.
58+
59+
### onDismiss
60+
61+
Type: `() => void`
62+
63+
Called when the user do something that signal they want to dismiss the sheet:
64+
65+
- hit the `esc` key.
66+
- tap on the backdrop.
67+
- swipes the sheet to the bottom of the viewport.
68+
69+
### snapPoints
70+
71+
Type: `(state) => number | number[]`
72+
73+
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:
74+
75+
- `headerHeight` – the current measured height of the `header`.
76+
- `footerHeight` – if a `footer` prop is provided then this is its height.
77+
- `height` – the current height of the sheet.
78+
- `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`.
79+
- `maxHeight` – the maximum available height on the page, usually matches `window.innerHeight/100vh`.
80+
81+
### defaultSnap
82+
83+
Type: `number | (state) => number`
84+
85+
Provide either a number, or a callback returning a number for the default position of the sheet when it opens.
86+
`state` use the same arguments as `snapPoints`, plus two more values: `snapPoints` and `lastSnap`.
87+
88+
### header
89+
90+
Type: `ReactNode`
91+
92+
Supports the same value type as the `children` prop.
93+
94+
### footer
95+
96+
Type: `ReactNode`
97+
98+
Supports the same value type as the `children` prop.
99+
100+
### initialFocusRef
101+
102+
Type: `React.Ref`
103+
104+
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.
105+
106+
### blocking
107+
108+
Type: `boolean`
109+
110+
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.
111+
112+
## ref
113+
114+
Methods available when setting a `ref` on the sheet:
115+
116+
```jsx
117+
export default function Example() {
118+
const sheetRef = React.useRef()
119+
return <BottomSheet open ref={sheetRef} />
120+
}
121+
```
122+
123+
### snapTo
124+
125+
Type: `(numberOrCallback: number | (state => number)) => void`
126+
127+
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))`.
9128

10129
# Credits
11130

12131
- Play icon used on frame overlays: https://fontawesome.com/icons/play-circle?style=regular
13132
- Phone frame used in logo: https://www.figma.com/community/file/896042888090872154/Mono-Devices-1.0
14133
- iPhone frame used to wrap examples: https://www.figma.com/community/file/858143367356468985/(Variants)-iOS-%26-iPadOS-14-UI-Kit-for-Figma
134+
135+
[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
136+
[size-badge]: http://img.badgesize.io/https://unpkg.com/react-spring-bottom-sheet/dist/index.es.js?label=size&style=flat-square
137+
[unpkg-dist]: https://unpkg.com/react-spring-bottom-sheet/dist/
138+
[module-formats-badge]: https://img.shields.io/badge/module%20formats-cjs%2C%20es%2C%20modern-green.svg?style=flat-square

docs/HeadTitle.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.

docs/MetaTags.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import Head from 'next/head'
2+
3+
export default function MetaTags({
4+
homepage,
5+
description,
6+
name,
7+
title,
8+
...props
9+
}: {
10+
homepage?: string
11+
description?: string
12+
name: string
13+
title?: string
14+
['twitter:title']?: string | false
15+
['og:title']?: string | false
16+
['og:site_name']?: string | false
17+
['twitter:image:src']?: string | false
18+
['og:image']?: string | false
19+
}) {
20+
const fallbackTitle = `$ npm i ${name}`
21+
const twitterTitle =
22+
props['twitter:title'] ?? (props['og:title'] || fallbackTitle)
23+
const ogTitle = props['og:title'] ?? (props['twitter:title'] || fallbackTitle)
24+
const twitterImage = props['twitter:image:src'] ?? props['og:image']
25+
const ogImage = props['og:image'] ?? props['twitter:image:src']
26+
const ogSiteName = props['og:site_name'] ?? name
27+
const twitterSite = props['twitter:site']
28+
const twitterDescription = props['twitter:description'] ?? description
29+
30+
return (
31+
<Head>
32+
<title key="title">
33+
{title ? `${title} | ` : null}
34+
{props['og:site_name'] ?? name}
35+
</title>
36+
{description && (
37+
<meta key="description" name="description" content={description} />
38+
)}
39+
{twitterSite && (
40+
<>
41+
{twitterImage && (
42+
<meta
43+
key="twitter:image:src"
44+
name="twitter:image:src"
45+
content={twitterImage}
46+
/>
47+
)}
48+
{twitterSite && (
49+
<meta
50+
key="twitter:site"
51+
name="twitter:site"
52+
content={twitterSite}
53+
/>
54+
)}
55+
<meta
56+
key="twitter:card"
57+
name="twitter:card"
58+
content="summary_large_image"
59+
/>
60+
{twitterTitle && (
61+
<meta
62+
key="twitter:title"
63+
name="twitter:title"
64+
content={twitterTitle}
65+
/>
66+
)}
67+
{twitterDescription && (
68+
<meta
69+
key="twitter:description"
70+
name="twitter:description"
71+
content={twitterDescription}
72+
/>
73+
)}
74+
</>
75+
)}
76+
{homepage && (
77+
<>
78+
{ogImage && (
79+
<meta key="og:image" property="og:image" content={ogImage} />
80+
)}
81+
{ogSiteName && (
82+
<meta
83+
key="og:site_name"
84+
property="og:site_name"
85+
content={ogSiteName}
86+
/>
87+
)}
88+
<meta key="og:type" property="og:type" content="object" />
89+
{ogTitle && (
90+
<meta key="og:title" property="og:title" content={ogTitle} />
91+
)}
92+
<meta key="og:url" property="og:url" content={homepage} />
93+
<meta
94+
key="og:description"
95+
property="og:description"
96+
content={description}
97+
/>
98+
<link key="canonical" rel="canonical" href={homepage} />
99+
</>
100+
)}
101+
</Head>
102+
)
103+
}

docs/fixtures/Container.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cx from 'classnames/dedupe'
22
import { useEffect } from 'react'
33
import { useDetectEnv } from './hooks'
4+
import Link from 'next/link'
45

56
export default function Container({
67
children,
@@ -28,6 +29,11 @@ export default function Container({
2829
className
2930
)}
3031
>
32+
<Link href="/">
33+
<a className="absolute left-0 top-0 only-window my-4 mx-8 py-2 px-4 transition-colors focus:duration-0 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 text-gray-600 hover:text-gray-700 active:text-gray-800 text-xs rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:ring-gray-400">
34+
Close example
35+
</a>
36+
</Link>
3137
{children}
3238
</main>
3339
)

docs/style.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@
3030
--rsbs-ml: 0px;
3131
--rsbs-mr: 0px;
3232
}
33+
.is-iframe .only-window {
34+
display: none;
35+
}
3336
}

docs/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// @stipsan/react-spring => React Spring
2+
export function capitalize(str) {
3+
return str
4+
.split('/')
5+
.pop()
6+
.split('-')
7+
.map((_) => _.charAt(0).toUpperCase() + _.slice(1))
8+
.join(' ')
9+
}

0 commit comments

Comments
 (0)