From 0514e939d320ebe5adb02fdbe9ec8bcd61973bc3 Mon Sep 17 00:00:00 2001 From: Nicholas Petrasek Date: Thu, 1 Oct 2026 05:59:46 +0200 Subject: [PATCH] docs: explain use cases and provide complete examples --- README.md | 247 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 148 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 3f4e0e5..cbb9c51 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,171 @@ # React Pull to Refresh -Pull to refresh without owning your feed. +Add a downward refresh gesture to a React feed while keeping your existing data +fetching and cache. Useful for inboxes, activity lists, and other scrolling views +where people expect to pull at the top to check for updates. -A small, accessible pull-to-refresh primitive with scroll arbitration, -resistance, and an async refresh lifecycle. +The package coordinates scroll ownership, pull resistance, release thresholds, +and the pending refresh animation. You supply the refresh function, content, +indicator, and error handling. An ordinary refresh button remains available to +everyone, including people who cannot use the gesture. -Mobile pull-to-refresh depends on browser gesture ownership. See -[Browser Behavior](docs/BROWSER_BEHAVIOR.md). +[Demos and documentation](https://react-pull-to-refresh.nipesolutions.com) · +[npm](https://www.npmjs.com/package/@nipe-solutions/react-pull-to-refresh) · +[API reference](docs/API.md) + +## Install ```bash npm install @nipe-solutions/react-pull-to-refresh ``` +Supports React 18.3 and React 19 with the matching `react-dom` peer. There are no +additional runtime dependencies. + +## Refresh a scrolling inbox + +This component accepts the current messages and your async reload function. Both +the button and the gesture use the same handler, which prevents overlapping +requests and reports success or failure. Your parent component updates `messages` +when `refreshMessages` loads new data. + ```tsx +import { useRef, useState } from 'react' import { PullToRefresh } from '@nipe-solutions/react-pull-to-refresh' import '@nipe-solutions/react-pull-to-refresh/core.css' -export function Inbox() { +interface InboxProps { + messages: readonly { id: string; subject: string }[] + refreshMessages: () => Promise +} + +export function Inbox({ messages, refreshMessages }: InboxProps) { + const scrollRef = useRef(null) + const inFlight = useRef(false) + const [refreshing, setRefreshing] = useState(false) + const [status, setStatus] = useState('') + async function refresh() { - await refetch() + if (inFlight.current) return + inFlight.current = true + setRefreshing(true) + setStatus('Refreshing inbox…') + + try { + await refreshMessages() + setStatus('Inbox updated.') + } catch { + setStatus('Could not refresh. Please try again.') + } finally { + inFlight.current = false + setRefreshing(false) + } } return ( - <> - - - - - - - - - - +

{status}

+
+ + + ↻ + + +
    + {messages.map((message) => ( +
  • {message.subject}
  • + ))} +
+
+
+
+ ) } ``` -## Why this exists - -Pull-to-refresh coordinates downward intent, the active scroll boundary, -resistance, threshold hysteresis, exactly-once refresh commitment, and settling. -It does not fetch data, own a cache, render a feed, or manage application errors. - -## API - -| Root prop | Type | Default | Purpose | -| --------------- | ---------------------------------- | -------- | ----------------------------------------------------------- | -| onRefresh | function returning void or Promise | required | Runs the application-owned refresh. | -| threshold | number | 72 | Visual pull distance required to arm. | -| disabled | boolean | false | Cancels an uncommitted gesture without disabling scrolling. | -| scrollContainer | HTMLElement, Window, or ref | auto | Overrides nearest scroll-surface detection. | - -Root exposes data-state with idle, pending, pulling, armed, refreshing, -settling, or disabled. Indicator positions consumer-owned visual content and is -aria-hidden. Content provides a transform-isolated wrapper. - -## CSS - -Import core.css for mechanics. theme.css is an optional neutral starting point; -styles.css combines both. - -```css -@import '@nipe-solutions/react-pull-to-refresh/core.css'; -``` - -Available root variables: - -- --ptr-distance: resisted visual distance in pixels -- --ptr-progress: threshold progress clamped from 0 to 1 -- --ptr-overshoot: resisted distance beyond the threshold -- --ptr-threshold: configured threshold - -## Scroll ownership - -The root resolves an explicit owner or the nearest actually scrollable ancestor, -then Window. It checks nested scrollers from the gesture origin and rechecks the -boundary before claim. An explicit null ref stays unresolved until a subsequent -gesture. Gestures starting below the top stay browser-owned for that stream. - -Core CSS does not set root overflow or overscroll policy. Apply optional -`overscroll-behavior-y: contain` to your actual scroller. Chromium page-level -containment may help suppress native refresh; it is not a universal guarantee. -Page-level custom Pull to Refresh on iOS Safari is not a guaranteed configuration. -Safari may retain native browser Pull-to-Refresh ownership. - -## Accessibility - -The gesture must not be the only refresh path. Pass the same application -function to a normal button. Add a polite status announcement only when it -benefits the product. The primitive adds no progress role, live region, -keyboard gesture, or focus movement. - -## Browser notes - -Directional-capable browsers use Pointer Events with `pan-x pan-down pinch-zoom`. -Firefox/Safari use a session-scoped Touch Events compatibility adapter. Automated -Chromium, Firefox, and WebKit tests cover mechanics, keyboard use, and axe rules. -They do not verify physical iOS or Android behavior. No physical-device or human -screen-reader session was performed for 1.0.0. Always provide an application -refresh button as the accessible alternative. - -See [browser behavior](docs/BROWSER_BEHAVIOR.md), -[integrations](docs/INTEGRATIONS.md), and -[real-device QA](docs/REAL_DEVICE_QA.md). - -## When not to use it - -Prefer a refresh button when the app is desktop-only, content does not scroll, -browser-native refresh is enough, or refresh requires explicit confirmation. - -## Development +`core.css` supplies positioning, translation, and touch policy. It does not +create a scroll container: the outer `div` above provides the scrolling layout. +The explicit ref keeps it the owner even when a short inbox does not overflow. +Its optional overscroll containment limits scroll chaining at that surface. +You can add `theme.css` after core for neutral indicator styling, or use +`styles.css` to import both together. + +## Refresh lifecycle and presentation + +`onRefresh` runs once when an armed pull is released. Return a promise to keep +the gesture in its refreshing state until the work finishes. Resolution, +rejection, and synchronous throws all settle the gesture; the library does not +display errors, retry requests, or cancel your promise. Handle those concerns in +the application, as the example does. + +| Root prop | Default | Use | +| ----------------- | --------- | ---------------------------------------------------------------------- | +| `onRefresh` | Required | Function returning `void` or `Promise`. | +| `threshold` | `72` | Positive visual pull distance in pixels required to arm. | +| `disabled` | `false` | Cancels an uncommitted gesture while preserving ordinary scrolling. | +| `scrollContainer` | Automatic | Explicit `HTMLElement`, `Window`, or element ref for scroll ownership. | + +Root exposes `data-state`: `idle`, `pending`, `pulling`, `armed`, `refreshing`, +`settling`, or `disabled`. Use it and the `--ptr-distance`, `--ptr-progress`, +`--ptr-overshoot`, and `--ptr-threshold` CSS variables to style your indicator. +Indicator is `aria-hidden`; Content supplies the moving content wrapper. The +primitive adds no live region, keyboard gesture, or focus movement. The example +provides its own status announcement outside the indicator. + +## Choose the scroll surface deliberately + +Automatic detection selects Root or the nearest ancestor with vertical overflow, +then falls back to Window. Relevant nested scrollers must also be at the top. +A gesture that begins below the top stays with scrolling for that gesture, +even if it later reaches the boundary. Pass `scrollContainer` when your layout +needs an explicit owner; a ref whose `current` is null leaves the gesture +ineligible until a later gesture resolves it. + +Page-level custom pull-to-refresh on iOS Safari is not guaranteed: Safari may +retain native browser refresh ownership. Overscroll CSS is application policy, +not a universal way to suppress native refresh. Element scrollers are a more +controllable starting point, but still need testing on the devices you support. +See [browser behavior](docs/BROWSER_BEHAVIOR.md) before choosing page-level use. + +Inputs and editable content do not initiate the gesture. Mark custom surfaces +with `data-pull-to-refresh-ignore` when they should keep their own interaction. +For swipeable rows and bottom-sheet feeds, see [integrations](docs/INTEGRATIONS.md). + +## Support and limits + +Prefer a refresh button alone for desktop-only views, non-scrolling content, +or refresh actions that need confirmation. This package does not fetch data, +manage a cache, virtualize a list, or render a feed. + +Directional-capable browsers use Pointer Events; browsers without directional +touch-action support use a session-scoped Touch Events adapter. Automated +Chromium, Firefox, and WebKit checks cover mechanics, keyboard use of fallback +controls, and axe rules. No physical iOS/Android or human screen-reader session +was performed for 1.0.0. Desktop WebKit does not establish physical iOS support. +Keep the refresh button available and consult the [real-device QA checklist](docs/REAL_DEVICE_QA.md). + +## Working on the package + +Requires Node 24 and npm 11. ```bash npm install @@ -120,13 +173,9 @@ npm run check npm run test:e2e ``` -Requires Node 24. React 18.3 and React 19 are peer-supported. Runtime -dependencies are limited to React peers. - -Current status: 1.0.0 stable. Automated checks do not replace the documented -manual device and assistive-technology matrix. - -Part of [NIPE Open Source](https://opensource.nipesolutions.com). +See [contributing](CONTRIBUTING.md), [security reporting](SECURITY.md), and +[architecture](docs/ARCHITECTURE.md). Part of +[NIPE Open Source](https://opensource.nipesolutions.com). ## License