Skip to content

Commit

Permalink
fix: updated readme with more API docs and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan authored Dec 5, 2020
1 parent 19211ec commit 6a48fab
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 31 deletions.
105 changes: 102 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Just note that the component is mounted in a `@reach/portal` at the bottom of `<

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.
The only required prop, beyond `children`. And it's controlled, so if you don't set this to `false` then it's not possible to close the bottom sheet. It's worth knowing that the bottom sheet won't render anything but a `@reach/dialog` placeholder while `open` is `false`. Thus ensure your components behave as expected with being unmounted when the sheet closed. We can't really allow it to render and mount while in a closed/hidden position as there's no stable way of preventing keyboard users or screen readers from accidentally interacting with the closed bottom sheet as long as it's in the dom. This is especially problematic given it implements ARIA to optimize for a11y.

### onDismiss

Expand All @@ -75,7 +75,14 @@ This function should be pure as it's called often. You can choose to provide a s
- `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`.
- `maxHeight` – the maximum available height on the page, equivalent to `window.innerHeight` and `100vh`.

```jsx
<BottomSheet
// Allow the user to select between minimun height to avoid a scrollbar, and fullscren
snapPoints={({ minHeight, maxHeight }) => [minHeight, maxHeight]}
/>
```

### defaultSnap

Expand All @@ -84,6 +91,17 @@ 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`.

```jsx
<BottomSheet
// the first snap points height depends on the content, while the second one is equivalent to 60vh
snapPoints={({ minHeight, maxHeight }) => [minHeight, maxHeight / 0.6]}
// Opens the largest snap point by default, unless the user selected one previously
defaultSnap={({ lastSnap, snapPoints }) =>
lastSnap ?? Math.max(...snapPoints)
}
/>
```
### header
Type: `ReactNode`
Expand All @@ -96,6 +114,12 @@ Type: `ReactNode`
Supports the same value type as the `children` prop.
### sibling
Type: `ReactNode`
Supports the same value type as the `sibling` prop. Renders the node as a child of `[data-rsbs-root]`, but as a sibling to `[data-rsbs-backdrop]` and `[data-rsbs-overlay]`. This allows you to access the animation state and render elements on top of the bottom sheet, while being outside the overlay itself.
### initialFocusRef
Type: `React.Ref`
Expand All @@ -108,6 +132,72 @@ 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.
### scrollLocking
Type: `boolean`
iOS Safari, and some other mobile culprits, can be tricky if you're on a page that has scrolling overflow on `document.body`. Mobile browsers often prefer scrolling the page in these cases instead of letting you handle the touch interaction for UI such as the bottom sheet. Thus it's enabled by default. However it can be a bit agressive and can affect cases where you're putting a drag and drop element inside the bottom sheet. Such as `<input type="range" />` and more. For these cases you can wrap them in a container and give them this data attribute `[data-body-scroll-lock-ignore]` to prevent intervention. Really handy if you're doing crazy stuff like putting mapbox-gl widgets inside bottom sheets.
## Events
All events receive `SprinngEvent` as their argument. It has a single property, `type`, which can be `'OPEN' | 'RESIZE' | 'CLOSE'` depending on the scenario.
### onSpringStart
Type: `(event: SpringEvent) => void`
Fired on: `OPEN | RESIZE | 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.
```jsx
function Example() {
const [data, setData] = useState([])
return (
<BottomSheet
onSnapStart={async (event) => {
if (event.type === 'OPEN') {
// the bottom sheet gently waits
const data = await fetch(/* . . . */)
setData(data)
// and now we can proceed
}
}}
>
{data.map(/* . . . */)}
</BottomSheet>
)
}
```
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`
Fired on: `OPEN | CLOSE`.
#### OPEN
In order to be as fluid and delightful as possible, the open state can be interrupted and redirected by the user without waiting for the open transition to complete. Maybe they changed their mind and decided to close the sheet because they tapped a button by mistake. This interruption can happen in a number of ways:
- 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.
#### 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.
### 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.
## ref
Methods available when setting a `ref` on the sheet:
Expand All @@ -123,7 +213,16 @@ export default function Example() {
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))`.
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
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
Math.max(...snapPoints)
)
```
# Credits
Expand Down
8 changes: 8 additions & 0 deletions docs/Footer.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.skewed {
transform: skewY(-9deg);
box-shadow: 0 15rem 0 0 theme('backgroundColor.gray.900');

& > * {
transform: skewY(9deg);
}
}
10 changes: 9 additions & 1 deletion docs/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import cx from 'classnames'
import styles from './Footer.module.css'

function Badge({
name,
version,
Expand Down Expand Up @@ -30,7 +33,12 @@ export default function Footer({
reactUseGestureVersion: string
}) {
return (
<footer className="px-10 py-32 grid md:grid-flow-col md:place-items-center place-content-center gap-8 bg-gray-900">
<footer
className={cx(
'px-10 py-32 grid md:grid-flow-col md:place-items-center place-content-center gap-8 bg-gray-900',
styles.skewed
)}
>
<Badge name="react-spring-bottom-sheet" version={version} />
<Badge name="react-spring" version={reactSpringVersion} />
<Badge name="react-use-gesture" version={reactUseGestureVersion} />
Expand Down
6 changes: 3 additions & 3 deletions docs/Nugget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export default function Nugget({
lead: React.ReactNode
}) {
return (
<article className="md:text-center">
<h2 className="text-3xl text-hero font-display">{heading}</h2>
<p className="text-1xl px-0.5">{lead}</p>
<article className="">
<h2 className="text-2xl text-gray-900 font-display">{heading}</h2>
<p className="text-1xl px-0.5 text-left">{lead}</p>
</article>
)
}
6 changes: 6 additions & 0 deletions docs/StickyNugget.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@
box-shadow: 0 0 0 2px black;
width: 86.6%;
}

.fancybg {
left: 50%;
transform: translateX(-50%) skewY(-9deg);
width: 100vw;
}
20 changes: 16 additions & 4 deletions docs/StickyNugget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ export default function StickyNugget({
heading,
lead,
example,
text = 'text-gray-800',
bg = 'bg-gray-100',
}: {
flip?: boolean
heading: string
lead: React.ReactNode
example: string
bg?: string
text?: string
}) {
const [loading, setLoading] = useState(false)
const [loaded, setLoaded] = useState(false)
Expand All @@ -28,18 +32,26 @@ export default function StickyNugget({
}, [loaded, loading])

return (
<article className="grid grid-cols-1 sm:grid-cols-2 gap-5">
<article
className={cx('grid grid-cols-1 md:grid-cols-2 gap-5 relative py-20', {
'md:pt-40': !flip,
'md:pb-40': flip,
})}
>
<div
className={cx('absolute top-0 right-0 bottom-0', styles.fancybg, bg)}
/>
<div>
<div className="grid grid-flow-row place-items-start gap-2.5 sticky top-5 mb-5">
<h2 className="text-5xl text-hero font-display">{heading}</h2>
<h2 className={cx('text-5xl font-display', text)}>{heading}</h2>
{[].concat(lead).map((lead, i) => (
<p key={`lead-${i}`} className="text-2xl px-0.5">
{lead}
</p>
))}

<Link href={example}>
<a className="px-2 py-0 rounded-full transition-colors duration-150 focus-visible:duration-0 bg-hero-lighter text-hero hover:bg-hero hover:text-hero-lighter focus:outline-none focus-visible:bg-hero focus-visible:text-hero-lighter focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:ring-hero">
<a className="underline-none hover:underline focus:underline text-xl font-bold text-gray-700 hover:text-gray-800 active:text-gray-900 focus:outline-none">
Open example
</a>
</Link>
Expand All @@ -58,7 +70,7 @@ export default function StickyNugget({
</div>
<div
className={cx(styles.example, {
'sm:order-first': flip,
'md:order-first': flip,
})}
>
<div className={styles.phoneframe}>
Expand Down
4 changes: 2 additions & 2 deletions docs/headings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const simple = 'Easy to dismiss'
export const scrollable = 'Snap points & overflow'
export const sticky = 'Sticky Example'
export const aside = 'Aside Example'
export const sticky = 'Sticky Header & Footer'
export const aside = 'Non-blocking mode'
4 changes: 2 additions & 2 deletions docs/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@

/* the bottom sheet doesn't need display cutout paddings when in the iframe */
& [data-rsbs-has-footer='false'] [data-rsbs-content-padding] {
padding-bottom: 0px;
padding-bottom: 0px !important;
}
& [data-rsbs-footer-padding] {
padding-bottom: 16px;
padding-bottom: 16px !important;
}
}
/* Used by things like the "Close example" links that are only needed when not in the iframe */
Expand Down
10 changes: 10 additions & 0 deletions pages/fixtures/simple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ const SimpleFixturePage: NextPage<GetStaticProps> = ({
it down, tapping on the backdrop or by hitting <Kbd>esc</Kbd> on
their keyboard.
</p>
<details className=" text-xl px-7 py-3 rounded-2xl transition-colors duration-150 focus-within:duration-0 bg-gray-100 text-gray-900 focus:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-offset-white focus-within:ring-gray-300 w-full">
<summary className="focus:outline-none">
Tap on me to expand and change the height
</summary>
<div className="bg-gray-200 block rounded-md h-10 w-full my-10" />
<p>
The height adjustment is done automatically, it just works™!
</p>
<div className="bg-gray-200 block rounded-md h-10 w-full my-10" />
</details>
<Button onClick={onDismiss} className="w-full">
Dismiss
</Button>
Expand Down
2 changes: 1 addition & 1 deletion pages/fixtures/sticky.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const StickyFixturePage: NextPage<GetStaticProps> = ({
lastSnap ?? Math.max(...snapPoints)
}
snapPoints={({ maxHeight }) => [
maxHeight - maxHeight / 10,
maxHeight - maxHeight / 5,
maxHeight * 0.6,
]}
header={
Expand Down
43 changes: 35 additions & 8 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@ const IndexPage: NextPage<GetStaticProps> = ({
/>
<main>
<Hero />
<div className="max-w-5xl mx-auto py-10 px-8 grid grid-cols-1 md:grid-cols-3 gap-4">
<Nugget heading="Placeholder" lead="Lorem ipsum and so on" />
<Nugget heading="Placeholder" lead="Lorem ipsum and so on" />
<Nugget heading="Placeholder" lead="Lorem ipsum and so on" />
<div className="max-w-5xl mx-auto py-20 px-8 grid grid-cols-1 md:grid-cols-3 gap-x-5 gap-y-10">
<Nugget
heading="Modern"
lead="Built on top of react-spring and react-use-gesture, following best practices for minimal rerenders and only animating CSS properties that can be done on the GPU when possible."
/>
<Nugget
heading="Flexible"
lead="Can be used like a blocking dialog that require the user to make a choice before it can be closed, or like floating bottom panel. It automatically adapts to the dimensions of the content you put in it."
/>
<Nugget
heading="CSS Variables"
lead="By using CSS Custom Properties you're not limited by what is spring animated by default. You can change which elements animations are applied to without writing any JS."
/>
</div>
<div className="max-w-4xl mx-auto py-10 px-8 grid grid-flow-row gap-y-20">
<div className="max-w-5xl mx-auto pt-20 px-8 grid grid-flow-row">
<StickyNugget
heading={simple}
lead={[
Expand All @@ -43,19 +52,37 @@ const IndexPage: NextPage<GetStaticProps> = ({
/>
<StickyNugget
flip
bg="bg-gray-200"
heading={scrollable}
lead="Intro"
lead={[
"The snap points api lets you control exactly what positions the sheet can be in. If the user drag the sheet out of bounds you'll get a rubber band effect, and it gently slides into position on release. You can even flick it from top to bottom with some speed, if that's your jam.",
'By default the sheet will try to use enough height to avoid a scrolling overflow.',
"And finally, it shows how the sheet behaves when you don't provide the onDismiss callback, note how you can't close it.",
]}
example="/fixtures/scrollable"
/>
<StickyNugget
bg="bg-gray-300"
heading={sticky}
lead="Intro"
lead={[
"Can be really tricky to implement in a performant way. Luckily with this component you don't have to worry about that.",
'By adding a header the touch hit target is much larger, making it more pleasant to use.',
"For those big thicc phones they'll be happy to find that you can swipe on the sticky footer to adust the height, making one-handed usage a bit easier.",
'On top of all that see how it remembers the last snap position it had when closing, and restore it when reopened.',
'One more thing, the opening transition is interruptible, you can start dragging it right away.',
]}
example="/fixtures/sticky"
/>
<StickyNugget
flip
bg="bg-gray-400"
text="text-900"
heading={aside}
lead="Intro"
lead={[
"Examples so far have all been with blocking=true, which is the default state. It's comparable to a blocking modal dialog, you can't interract with the rest of the page until the dialog closes.",
"This mode can be turned off and changes the look and feel of the sheet to fit scenarios where it's used as you would a draggable sidebar.",
'Or as an search overlay over a map perhaps. ',
]}
example="/fixtures/aside"
/>
</div>
Expand Down
Loading

0 comments on commit 6a48fab

Please sign in to comment.