@bitforgehq/ui 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3665 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/ai/MANIFEST.index.json +383 -0
- package/ai/MANIFEST.json +7528 -0
- package/ai/REFERENCE.index.json +1461 -0
- package/ai/REFERENCE.json +7213 -0
- package/ai/UI_GUIDE.md +1159 -0
- package/ai/components/action-bar.md +209 -0
- package/ai/components/action-sheet.md +526 -0
- package/ai/components/alert-dialog.md +509 -0
- package/ai/components/alert.md +376 -0
- package/ai/components/avatar.md +146 -0
- package/ai/components/back-button.md +286 -0
- package/ai/components/badge.md +171 -0
- package/ai/components/button.md +431 -0
- package/ai/components/card.md +303 -0
- package/ai/components/checkbox.md +418 -0
- package/ai/components/chip-group.md +205 -0
- package/ai/components/chip.md +300 -0
- package/ai/components/content.md +474 -0
- package/ai/components/desktop-toast.md +420 -0
- package/ai/components/dialog.md +962 -0
- package/ai/components/empty-state.md +146 -0
- package/ai/components/field.md +319 -0
- package/ai/components/header.md +809 -0
- package/ai/components/icon-button.md +221 -0
- package/ai/components/icon.md +176 -0
- package/ai/components/input.md +455 -0
- package/ai/components/list-item.md +326 -0
- package/ai/components/list.md +178 -0
- package/ai/components/menu-button.md +377 -0
- package/ai/components/menu.md +661 -0
- package/ai/components/modal.md +613 -0
- package/ai/components/number-input.md +423 -0
- package/ai/components/page.md +299 -0
- package/ai/components/progress.md +215 -0
- package/ai/components/radio-group.md +432 -0
- package/ai/components/refresher.md +377 -0
- package/ai/components/segmented.md +517 -0
- package/ai/components/select.md +510 -0
- package/ai/components/sheet.md +845 -0
- package/ai/components/spinner.md +171 -0
- package/ai/components/stack-outlet.md +661 -0
- package/ai/components/tab-bar.md +427 -0
- package/ai/components/tab.md +295 -0
- package/ai/components/tabs.md +408 -0
- package/ai/components/textarea.md +371 -0
- package/ai/components/toast.md +522 -0
- package/ai/components/toggle.md +387 -0
- package/ai/reference/bf-action-directive.md +21 -0
- package/ai/reference/bf-action-sheet-action-role.md +27 -0
- package/ai/reference/bf-action-sheet-action.md +28 -0
- package/ai/reference/bf-action-sheet-options.md +28 -0
- package/ai/reference/bf-action-sheet-service.md +33 -0
- package/ai/reference/bf-alert-dialog-button-role.md +27 -0
- package/ai/reference/bf-alert-dialog-button.md +27 -0
- package/ai/reference/bf-alert-dialog-options.md +29 -0
- package/ai/reference/bf-alert-dialog-service.md +34 -0
- package/ai/reference/bf-alert-variant.md +28 -0
- package/ai/reference/bf-autocapitalize.md +30 -0
- package/ai/reference/bf-avatar-size.md +27 -0
- package/ai/reference/bf-avatar-variant.md +28 -0
- package/ai/reference/bf-back-at-root.md +28 -0
- package/ai/reference/bf-back-button-service.md +40 -0
- package/ai/reference/bf-badge-variant.md +29 -0
- package/ai/reference/bf-button-size.md +29 -0
- package/ai/reference/bf-button-type.md +28 -0
- package/ai/reference/bf-button-variant.md +29 -0
- package/ai/reference/bf-card-padding.md +26 -0
- package/ai/reference/bf-card-variant.md +28 -0
- package/ai/reference/bf-chip-group-mode.md +27 -0
- package/ai/reference/bf-chip-variant.md +26 -0
- package/ai/reference/bf-compare-with.md +24 -0
- package/ai/reference/bf-component-overlay-ref.md +33 -0
- package/ai/reference/bf-confirm-button-role.md +26 -0
- package/ai/reference/bf-confirm-button.md +27 -0
- package/ai/reference/bf-confirm-options.md +31 -0
- package/ai/reference/bf-confirm-size.md +26 -0
- package/ai/reference/bf-content-scroll-event.md +21 -0
- package/ai/reference/bf-defaults-options.md +32 -0
- package/ai/reference/bf-defaults.md +28 -0
- package/ai/reference/bf-desktop-toast-config.md +28 -0
- package/ai/reference/bf-desktop-toast-harness.md +33 -0
- package/ai/reference/bf-desktop-toast-options.md +28 -0
- package/ai/reference/bf-desktop-toast-position.md +28 -0
- package/ai/reference/bf-desktop-toast-service.md +42 -0
- package/ai/reference/bf-dialog-config.md +44 -0
- package/ai/reference/bf-dialog-harness.md +52 -0
- package/ai/reference/bf-dialog-ref.md +44 -0
- package/ai/reference/bf-dialog-role.md +26 -0
- package/ai/reference/bf-dialog-service.md +55 -0
- package/ai/reference/bf-dialog-size.md +31 -0
- package/ai/reference/bf-dialog-template-context.md +32 -0
- package/ai/reference/bf-empty-state-heading-level.md +26 -0
- package/ai/reference/bf-enter-key-hint.md +31 -0
- package/ai/reference/bf-field-control-class.md +32 -0
- package/ai/reference/bf-field-control-state.md +28 -0
- package/ai/reference/bf-field-size.md +29 -0
- package/ai/reference/bf-haptics-impact-style.md +27 -0
- package/ai/reference/bf-haptics-notification-type.md +27 -0
- package/ai/reference/bf-haptics-service.md +41 -0
- package/ai/reference/bf-harness-filters.md +29 -0
- package/ai/reference/bf-horizontal-scroll-directive.md +129 -0
- package/ai/reference/bf-icon-button-variant.md +28 -0
- package/ai/reference/bf-icon-size.md +27 -0
- package/ai/reference/bf-input-autocapitalize.md +36 -0
- package/ai/reference/bf-input-enter-key-hint.md +38 -0
- package/ai/reference/bf-input-input-mode.md +38 -0
- package/ai/reference/bf-input-mode.md +32 -0
- package/ai/reference/bf-input-type.md +35 -0
- package/ai/reference/bf-jsdom-shim-options.md +22 -0
- package/ai/reference/bf-keyboard-assist-behavior.md +26 -0
- package/ai/reference/bf-keyboard-assist-directive.md +185 -0
- package/ai/reference/bf-keyboard-phase.md +30 -0
- package/ai/reference/bf-keyboard-service.md +50 -0
- package/ai/reference/bf-labels-overrides.md +26 -0
- package/ai/reference/bf-labels.md +44 -0
- package/ai/reference/bf-menu-button-harness.md +40 -0
- package/ai/reference/bf-menu-button-variant.md +27 -0
- package/ai/reference/bf-menu-divider-directive.md +13 -0
- package/ai/reference/bf-menu-group-directive.md +14 -0
- package/ai/reference/bf-menu-harness.md +55 -0
- package/ai/reference/bf-menu-item-checkbox-directive.md +31 -0
- package/ai/reference/bf-menu-item-directive.md +31 -0
- package/ai/reference/bf-menu-item-harness.md +52 -0
- package/ai/reference/bf-menu-item-radio-directive.md +29 -0
- package/ai/reference/bf-menu-placement.md +29 -0
- package/ai/reference/bf-menu-trigger-directive.md +46 -0
- package/ai/reference/bf-modal-options.md +30 -0
- package/ai/reference/bf-modal-presentation.md +26 -0
- package/ai/reference/bf-modal-service.md +38 -0
- package/ai/reference/bf-nav-controller.md +59 -0
- package/ai/reference/bf-nav-direction.md +23 -0
- package/ai/reference/bf-nav-target.md +21 -0
- package/ai/reference/bf-number-input-mode.md +28 -0
- package/ai/reference/bf-number-input-step.md +25 -0
- package/ai/reference/bf-overlay-auto-focus.md +29 -0
- package/ai/reference/bf-overlay-controller.md +22 -0
- package/ai/reference/bf-overlay-host.md +36 -0
- package/ai/reference/bf-overlay-options.md +53 -0
- package/ai/reference/bf-overlay-position.md +30 -0
- package/ai/reference/bf-overlay-ref.md +53 -0
- package/ai/reference/bf-overlay-role.md +27 -0
- package/ai/reference/bf-overlay-service.md +53 -0
- package/ai/reference/bf-overlay-stack-options.md +24 -0
- package/ai/reference/bf-overlay-stack.md +81 -0
- package/ai/reference/bf-overlay-template-context.md +24 -0
- package/ai/reference/bf-overscroll-stretch-directive.md +163 -0
- package/ai/reference/bf-page-context.md +24 -0
- package/ai/reference/bf-platform-service.md +53 -0
- package/ai/reference/bf-platform.md +28 -0
- package/ai/reference/bf-progress-size.md +26 -0
- package/ai/reference/bf-progress-tone.md +28 -0
- package/ai/reference/bf-radio-group-harness.md +49 -0
- package/ai/reference/bf-radio-group-orientation.md +26 -0
- package/ai/reference/bf-radio-option.md +30 -0
- package/ai/reference/bf-refresher-refresh-event.md +22 -0
- package/ai/reference/bf-segment-option.md +29 -0
- package/ai/reference/bf-segmented-harness.md +48 -0
- package/ai/reference/bf-segmented-size.md +29 -0
- package/ai/reference/bf-select-harness.md +70 -0
- package/ai/reference/bf-select-option.md +29 -0
- package/ai/reference/bf-sheet-auto-focus.md +29 -0
- package/ai/reference/bf-sheet-options.md +32 -0
- package/ai/reference/bf-sheet-ref.md +45 -0
- package/ai/reference/bf-sheet-service.md +34 -0
- package/ai/reference/bf-shell-config-mode.md +28 -0
- package/ai/reference/bf-shell-config.md +38 -0
- package/ai/reference/bf-shell-dynamic-type-limit.md +21 -0
- package/ai/reference/bf-shell-dynamic-type.md +25 -0
- package/ai/reference/bf-shell-mode.md +28 -0
- package/ai/reference/bf-shell-testing-options.md +26 -0
- package/ai/reference/bf-size.md +45 -0
- package/ai/reference/bf-spinner-size.md +27 -0
- package/ai/reference/bf-stack-change.md +22 -0
- package/ai/reference/bf-stack-reuse-strategy.md +32 -0
- package/ai/reference/bf-stack-transition-event.md +26 -0
- package/ai/reference/bf-stack-transition.md +26 -0
- package/ai/reference/bf-stacked-overlay.md +23 -0
- package/ai/reference/bf-system-bars-style.md +38 -0
- package/ai/reference/bf-tab-bar-controller.md +51 -0
- package/ai/reference/bf-tab-bar-host.md +52 -0
- package/ai/reference/bf-tab-context.md +32 -0
- package/ai/reference/bf-tab-links.md +37 -0
- package/ai/reference/bf-testing-options.md +27 -0
- package/ai/reference/bf-text-scale-service.md +35 -0
- package/ai/reference/bf-theme.md +27 -0
- package/ai/reference/bf-toast-action.md +25 -0
- package/ai/reference/bf-toast-controller.md +17 -0
- package/ai/reference/bf-toast-dismiss-reason.md +29 -0
- package/ai/reference/bf-toast-options.md +29 -0
- package/ai/reference/bf-toast-position.md +26 -0
- package/ai/reference/bf-toast-ref.md +41 -0
- package/ai/reference/bf-toast-service.md +39 -0
- package/ai/reference/bf-toggle-label-position.md +26 -0
- package/ai/reference/bf-transition-kind.md +28 -0
- package/ai/reference/bf-validation-message.md +31 -0
- package/ai/reference/bf-validation-messages.md +33 -0
- package/ai/reference/bf-view-event.md +28 -0
- package/ai/reference/bf-view-lifecycle.md +41 -0
- package/ai/reference/bf-view-state.md +28 -0
- package/ai/reference/bf_default_labels.md +21 -0
- package/ai/reference/bf_default_values.md +21 -0
- package/ai/reference/bf_defaults.md +21 -0
- package/ai/reference/bf_desktop_toast_defaults.md +21 -0
- package/ai/reference/bf_dialog_data.md +17 -0
- package/ai/reference/bf_labels.md +21 -0
- package/ai/reference/bf_overlay_data.md +19 -0
- package/ai/reference/bf_overlay_host.md +21 -0
- package/ai/reference/bf_sheet_default_snap_points.md +17 -0
- package/ai/reference/bf_shell_config.md +21 -0
- package/ai/reference/bf_shell_defaults.md +21 -0
- package/ai/reference/bf_tab_bar.md +21 -0
- package/ai/reference/bf_tab_bar_controller.md +21 -0
- package/ai/reference/bf_tab_context.md +21 -0
- package/ai/reference/bf_tab_links.md +21 -0
- package/ai/reference/bf_theme_storage.md +19 -0
- package/ai/reference/create-transition.md +36 -0
- package/ai/reference/duration_fast.md +17 -0
- package/ai/reference/duration_md.md +17 -0
- package/ai/reference/duration_overlay.md +17 -0
- package/ai/reference/duration_push.md +17 -0
- package/ai/reference/fade-down.md +26 -0
- package/ai/reference/fade-in.md +25 -0
- package/ai/reference/fade-out.md +25 -0
- package/ai/reference/fade-up.md +26 -0
- package/ai/reference/inject-shell-mode.md +23 -0
- package/ai/reference/install-bf-jsdom-shims.md +31 -0
- package/ai/reference/ios_duration.md +17 -0
- package/ai/reference/ios_easing.md +17 -0
- package/ai/reference/is-text-entry.md +25 -0
- package/ai/reference/max_complete_ms.md +17 -0
- package/ai/reference/md-transition.md +31 -0
- package/ai/reference/md_duration.md +17 -0
- package/ai/reference/md_easing.md +17 -0
- package/ai/reference/overlay_duration.md +21 -0
- package/ai/reference/overlay_easing.md +19 -0
- package/ai/reference/overlay_reduced_duration.md +17 -0
- package/ai/reference/pop-frames-at.md +25 -0
- package/ai/reference/prefers-reduced-motion.md +25 -0
- package/ai/reference/provide-bf-defaults.md +33 -0
- package/ai/reference/provide-bf-desktop-toast.md +28 -0
- package/ai/reference/provide-bf-labels.md +38 -0
- package/ai/reference/provide-bf-shell-testing.md +34 -0
- package/ai/reference/provide-bf-shell.md +41 -0
- package/ai/reference/provide-bf-testing.md +33 -0
- package/ai/reference/reduced-transition.md +28 -0
- package/ai/reference/reduced_duration.md +17 -0
- package/ai/reference/resolve-shell-mode.md +33 -0
- package/ai/reference/scale-in.md +25 -0
- package/ai/reference/settle-duration.md +25 -0
- package/ai/reference/settle-pop.md +33 -0
- package/ai/reference/settle_easing.md +17 -0
- package/ai/reference/should-complete-pop.md +26 -0
- package/ai/reference/slide-down.md +26 -0
- package/ai/reference/slide-up.md +26 -0
- package/ai/reference/with-bf-shell-routing.md +19 -0
- package/bin/bf-ui.js +81 -0
- package/fesm2022/bitforgehq-ui-icons.mjs +230 -0
- package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-mobile.mjs +5530 -0
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-number-input.mjs +680 -0
- package/fesm2022/bitforgehq-ui-number-input.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-overlay.mjs +2675 -0
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-testing.mjs +402 -0
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui.mjs +7765 -0
- package/fesm2022/bitforgehq-ui.mjs.map +1 -0
- package/llms-full.txt +25394 -0
- package/llms.txt +329 -0
- package/package.json +94 -0
- package/theme.css +253 -0
- package/types/bitforgehq-ui-icons.d.ts +50 -0
- package/types/bitforgehq-ui-mobile.d.ts +2914 -0
- package/types/bitforgehq-ui-number-input.d.ts +282 -0
- package/types/bitforgehq-ui-overlay.d.ts +1514 -0
- package/types/bitforgehq-ui-testing.d.ts +495 -0
- package/types/bitforgehq-ui.d.ts +3499 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# bf-refresher
|
|
2
|
+
|
|
3
|
+
Pull-to-refresh indicator with haptic feedback, native iOS bounce and an Android-style pull
|
|
4
|
+
|
|
5
|
+
`BfRefresherComponent` from `@bitforgehq/ui/mobile` · Navigation · since 0.2.0
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, DestroyRef, inject, signal } from '@angular/core';
|
|
11
|
+
import { BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
|
|
12
|
+
import {
|
|
13
|
+
BfContentComponent,
|
|
14
|
+
BfHeaderComponent,
|
|
15
|
+
BfPageComponent,
|
|
16
|
+
BfRefresherComponent,
|
|
17
|
+
} from '@bitforgehq/ui/mobile';
|
|
18
|
+
|
|
19
|
+
/** How long the showcase's fake reload takes, so the spinner is visible. */
|
|
20
|
+
export const SHOWCASE_REFRESH_MS = 800;
|
|
21
|
+
|
|
22
|
+
/** `bf-refresher` showcase: pull the list down to "reload", which adds a row at the top. */
|
|
23
|
+
@Component({
|
|
24
|
+
selector: 'app-refresher-showcase',
|
|
25
|
+
imports: [
|
|
26
|
+
BfPageComponent,
|
|
27
|
+
BfHeaderComponent,
|
|
28
|
+
BfContentComponent,
|
|
29
|
+
BfRefresherComponent,
|
|
30
|
+
BfListComponent,
|
|
31
|
+
BfListItemComponent,
|
|
32
|
+
],
|
|
33
|
+
template: `
|
|
34
|
+
<bf-page data-showcase="refresher">
|
|
35
|
+
<bf-header title="Refresher" [largeTitle]="false" />
|
|
36
|
+
<bf-content>
|
|
37
|
+
<bf-refresher (refreshRequested)="onRefresh($event.complete)" />
|
|
38
|
+
<div class="flex flex-col gap-4 p-4">
|
|
39
|
+
<p class="text-caption text-text-muted">
|
|
40
|
+
Pull the list down past the disc's threshold and let go: it spins, and a new row arrives
|
|
41
|
+
at the top.
|
|
42
|
+
</p>
|
|
43
|
+
<bf-list [inset]="false">
|
|
44
|
+
@for (row of rows(); track row) {
|
|
45
|
+
<bf-list-item [title]="row" />
|
|
46
|
+
}
|
|
47
|
+
</bf-list>
|
|
48
|
+
</div>
|
|
49
|
+
</bf-content>
|
|
50
|
+
</bf-page>
|
|
51
|
+
`,
|
|
52
|
+
})
|
|
53
|
+
export class RefresherShowcasePage {
|
|
54
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
55
|
+
|
|
56
|
+
protected readonly rows = signal<readonly string[]>(['Message 3', 'Message 2', 'Message 1']);
|
|
57
|
+
|
|
58
|
+
/** The pending fake-reload timer, so it can be cancelled if the page is destroyed mid-refresh. */
|
|
59
|
+
private reloadTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
this.destroyRef.onDestroy(() => {
|
|
63
|
+
if (this.reloadTimeoutId !== undefined) {
|
|
64
|
+
clearTimeout(this.reloadTimeoutId);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async onRefresh(complete: () => void): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
await new Promise<void>((resolve) => {
|
|
72
|
+
this.reloadTimeoutId = setTimeout(resolve, SHOWCASE_REFRESH_MS);
|
|
73
|
+
});
|
|
74
|
+
this.rows.update((rows) => [`Message ${rows.length + 1}`, ...rows]);
|
|
75
|
+
} finally {
|
|
76
|
+
this.reloadTimeoutId = undefined;
|
|
77
|
+
complete();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Overview
|
|
84
|
+
|
|
85
|
+
Pull-to-refresh, placed as the first child of `bf-content`. It tracks Pointer Events on the
|
|
86
|
+
page's scroller (read from `BfPageContext`, or its nearest scrolling ancestor when used without one)
|
|
87
|
+
only while the scroller is at `scrollTop === 0`: the disc resists the pull (half the pointer's
|
|
88
|
+
travel, capped) from a resting position above the clipped content to a 64 px threshold, ticks
|
|
89
|
+
`BfHapticsService.impact('light')` once on crossing it, and on release past the threshold
|
|
90
|
+
parks at the top with a spinner and emits `refreshRequested` — call `complete()` on the event to animate
|
|
91
|
+
the disc away and reset. A press by itself changes nothing: only once a pointermove clears an
|
|
92
|
+
8 px downward slop (out-vertical'ing any horizontal travel) does it claim the gesture and
|
|
93
|
+
capture the pointer on the scroller, so a plain click on the content underneath (a row, a
|
|
94
|
+
button) still reaches it. For touch, a non-passive `touchmove` guard (attached only while a
|
|
95
|
+
press is tracked) keeps the browser from taking a downward drag at the top as a pan, which
|
|
96
|
+
would cancel the pointer stream; upward drags still scroll the list. The disc itself is
|
|
97
|
+
`aria-hidden`; a permanent, visually hidden `role="status"` region announces
|
|
98
|
+
`provideBfLabels()`'s `refreshing` ("Refreshing"). `prefers-reduced-motion` still follows the
|
|
99
|
+
pointer 1:1 during the pull (direct manipulation is not motion); only the settle, park and `complete()` animations
|
|
100
|
+
become instant.
|
|
101
|
+
|
|
102
|
+
That pull is the Material (and mouse/pen) mode. A touch in iOS mode (`BfPlatformService.mode()`)
|
|
103
|
+
is the bounce mode instead: the refresher claims nothing (no capture, no `preventDefault`, no
|
|
104
|
+
guard), the scroller rubber-bands natively, and while the finger is down a negative `scrollTop`
|
|
105
|
+
shows the disc inside the gap the bounce opens, at its resting −48 px, with opacity and scale
|
|
106
|
+
(0.6 → 1) following `−scrollTop / 64` once it passes 8 px; one light haptic at 64 px. Lifting the
|
|
107
|
+
finger at 64 px or more parks the disc and emits `refreshRequested`, like `UIRefreshControl`; a bounce
|
|
108
|
+
with no finger down (a fling's overshoot) never shows it. The host carries `data-bf-refresher`
|
|
109
|
+
while enabled, so `bfOverscrollStretch` leaves the top edge to it.
|
|
110
|
+
|
|
111
|
+
Pull-to-refresh. `bf-refresher` goes inside `bf-content`, as its first child: it tracks Pointer
|
|
112
|
+
Events on the page's scroller and, only while the scroller is at the very top, resists a downward
|
|
113
|
+
pull into a 40 px disc above the content. A plain press changes nothing — only once a move clears
|
|
114
|
+
an 8 px downward slop does it claim the gesture, and only then does it capture the pointer on the
|
|
115
|
+
scroller (so the gesture keeps tracking even if the pointer strays outside the scroller mid-drag)
|
|
116
|
+
and the disc becomes visible. Pull past the 64 px threshold and release, and it parks the disc at
|
|
117
|
+
the top with a spinner and emits `refreshRequested` — call `complete()` on the event once the reload is
|
|
118
|
+
done to animate the disc away and reset.
|
|
119
|
+
|
|
120
|
+
## When to use
|
|
121
|
+
|
|
122
|
+
Use `bf-refresher` as the first child of a [bf-content](https://bitforge-ui.web.app/components/content) whose content the user reloads on a phone: an inbox, a feed. Use a Refresh [bf-button](https://bitforge-ui.web.app/components/button) instead on desktop, and add one next to the refresher for keyboard and switch-control users.
|
|
123
|
+
|
|
124
|
+
## Basic usage
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { Component, signal } from '@angular/core';
|
|
128
|
+
import { BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
|
|
129
|
+
import {
|
|
130
|
+
BfContentComponent,
|
|
131
|
+
BfHeaderComponent,
|
|
132
|
+
BfPageComponent,
|
|
133
|
+
BfRefresherComponent,
|
|
134
|
+
} from '@bitforgehq/ui/mobile';
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pull to refresh.
|
|
138
|
+
*
|
|
139
|
+
* `refreshRequested` hands over `complete`; call it once the reload has finished.
|
|
140
|
+
*
|
|
141
|
+
* @preview none
|
|
142
|
+
*/
|
|
143
|
+
@Component({
|
|
144
|
+
selector: 'app-example-refresher-basic',
|
|
145
|
+
imports: [
|
|
146
|
+
BfContentComponent,
|
|
147
|
+
BfHeaderComponent,
|
|
148
|
+
BfListComponent,
|
|
149
|
+
BfListItemComponent,
|
|
150
|
+
BfPageComponent,
|
|
151
|
+
BfRefresherComponent,
|
|
152
|
+
],
|
|
153
|
+
template: `
|
|
154
|
+
<bf-page>
|
|
155
|
+
<bf-header title="Inbox" />
|
|
156
|
+
<bf-content>
|
|
157
|
+
<bf-refresher (refreshRequested)="onRefresh($event.complete)" />
|
|
158
|
+
<bf-list>
|
|
159
|
+
@for (message of messages(); track message) {
|
|
160
|
+
<bf-list-item [title]="message" />
|
|
161
|
+
}
|
|
162
|
+
</bf-list>
|
|
163
|
+
</bf-content>
|
|
164
|
+
</bf-page>
|
|
165
|
+
`,
|
|
166
|
+
})
|
|
167
|
+
export class RefresherBasicExampleComponent {
|
|
168
|
+
protected readonly messages = signal(['Welcome']);
|
|
169
|
+
|
|
170
|
+
async onRefresh(complete: () => void): Promise<void> {
|
|
171
|
+
await this.reload();
|
|
172
|
+
complete();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Stands in for the inbox state's reload. */
|
|
176
|
+
private async reload(): Promise<void> {
|
|
177
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
178
|
+
this.messages.update((messages) => [`New message ${messages.length}`, ...messages]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## iOS: riding the native bounce
|
|
184
|
+
|
|
185
|
+
In iOS mode (`BfPlatformService.mode() === 'ios'`) a touch works like `UIRefreshControl`. The
|
|
186
|
+
refresher claims nothing — no pointer capture, no `preventDefault`, no `touchmove` guard — so the
|
|
187
|
+
content rubber-bands natively (`bf-content` keeps even a short page scrollable by 1 px, so it always
|
|
188
|
+
bounces). While a finger is down it watches the scroller's `scroll` events: once the bounce opens
|
|
189
|
+
more than 8 px above the content, the disc shows inside that gap, at its resting position just
|
|
190
|
+
above the content and moving with it, with opacity and scale (0.6 → 1) following the pull up to the
|
|
191
|
+
64 px threshold, where one light haptic ticks. Lift the finger at 64 px or more and the disc
|
|
192
|
+
settles onto the content top while the content snaps back, and `refreshRequested` emits; lift it earlier and
|
|
193
|
+
the disc hides. A bounce with no finger down — the overshoot of a fling — never shows the disc.
|
|
194
|
+
|
|
195
|
+
This follows the engine (`BfPlatformService.mode()`, iOS only on native iOS), not
|
|
196
|
+
`provideBfShell({ mode })`: the bounce mode claims nothing and relies on WebKit reporting a negative
|
|
197
|
+
`scrollTop` while it rubber-bands. Chrome never reports a negative `scrollTop`, so an Android app
|
|
198
|
+
forced into `mode: 'ios'` keeps the material pull; with the shell mode, its pull-to-refresh would
|
|
199
|
+
stop working.
|
|
200
|
+
|
|
201
|
+
## Android and mouse: the material pull
|
|
202
|
+
|
|
203
|
+
Everywhere else — Material mode, and a mouse or pen in iOS mode (the desktop docs preview) — the
|
|
204
|
+
refresher drives the pull itself, as described above: the slop, the pointer capture, the resisted
|
|
205
|
+
disc and the park.
|
|
206
|
+
|
|
207
|
+
## Clicks pass through
|
|
208
|
+
|
|
209
|
+
A tap or click that never moves past the slop is left completely alone — the row or button
|
|
210
|
+
underneath it still receives its own `click`. `bf-refresher` only starts intercepting the pointer
|
|
211
|
+
once a move proves it's a downward pull, not a press on the content beneath it.
|
|
212
|
+
|
|
213
|
+
## Touch
|
|
214
|
+
|
|
215
|
+
In Material mode a phone's scroller is `touch-pan-y`, so the browser would take a downward drag as
|
|
216
|
+
its own pan and cancel the pointer stream. While a press is tracked, `bf-refresher` adds a non-passive
|
|
217
|
+
`touchmove` listener to the scroller that prevents a downward drag at the top (and every move once
|
|
218
|
+
the pull is claimed); upward drags are never prevented, so the list scrolls as usual. The row under
|
|
219
|
+
the finger losing its implicit pointer capture when the scroller claims the pull does not end it.
|
|
220
|
+
|
|
221
|
+
## The `complete()` contract
|
|
222
|
+
|
|
223
|
+
`refreshRequested` emits once per completed pull, with a `BfRefresherRefreshEvent`
|
|
224
|
+
(`{ complete: () => void }`); the 0.2 name `refresh` still emits the same event until 0.4.0. The disc stays parked
|
|
225
|
+
with its spinner spinning — and further pulls are ignored — until `complete()` is called; call it
|
|
226
|
+
whether the reload succeeded or failed, or the disc never leaves the top. `complete()` animates the
|
|
227
|
+
disc back to its resting position above the content and only then does another pull start working
|
|
228
|
+
again.
|
|
229
|
+
|
|
230
|
+
## Disabling
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { Component, signal } from '@angular/core';
|
|
234
|
+
import { BfContentComponent, BfPageComponent, BfRefresherComponent } from '@bitforgehq/ui/mobile';
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A refresher that is off while the page is read-only.
|
|
238
|
+
*
|
|
239
|
+
* @preview none
|
|
240
|
+
*/
|
|
241
|
+
@Component({
|
|
242
|
+
selector: 'app-example-refresher-disabled',
|
|
243
|
+
imports: [BfContentComponent, BfPageComponent, BfRefresherComponent],
|
|
244
|
+
template: `
|
|
245
|
+
<bf-page>
|
|
246
|
+
<bf-content>
|
|
247
|
+
<bf-refresher [disabled]="isReadOnly()" />
|
|
248
|
+
<p class="text-body p-4">An archived project cannot be refreshed.</p>
|
|
249
|
+
</bf-content>
|
|
250
|
+
</bf-page>
|
|
251
|
+
`,
|
|
252
|
+
})
|
|
253
|
+
export class RefresherDisabledExampleComponent {
|
|
254
|
+
protected readonly isReadOnly = signal(true);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`disabled` stops pull tracking entirely; the disc never moves and `refreshRequested` never emits. It has no
|
|
259
|
+
effect on a pull already in progress — flip it before the next `pointerdown`.
|
|
260
|
+
|
|
261
|
+
## Reduced motion
|
|
262
|
+
|
|
263
|
+
The disc still follows the pointer 1:1 (resisted) during the pull under
|
|
264
|
+
`prefers-reduced-motion: reduce` — direct manipulation tied to the pointer's own position isn't
|
|
265
|
+
motion. What changes is the parts driven by the Web Animations API: parking the disc when a pull
|
|
266
|
+
completes, `complete()`'s dismissal, and settling back on a pull that didn't reach the threshold
|
|
267
|
+
all become instant jumps instead of animated transitions. The `refreshing` state and the `refreshRequested`
|
|
268
|
+
event fire exactly the same either way.
|
|
269
|
+
|
|
270
|
+
## Desktop
|
|
271
|
+
|
|
272
|
+
A mouse can drag the pull (the Material pull runs for a mouse or pen), but desktop users do not expect pull-to-refresh and a wheel or trackpad scroll never starts it. On desktop, offer a Refresh [bf-button](https://bitforge-ui.web.app/components/button) in the toolbar that calls the same reload.
|
|
273
|
+
|
|
274
|
+
## Theming
|
|
275
|
+
|
|
276
|
+
- Disc: `bg-surface`, `shadow-raised`, `rounded-full`, 40 × 40 px; `visibility: hidden` at rest so
|
|
277
|
+
its shadow never bleeds above the content, shown only once a pull is claimed (or, on iOS, once
|
|
278
|
+
the bounce opens more than 8 px).
|
|
279
|
+
- Host: carries `data-bf-refresher` while enabled, so `bf-content`'s Android edge stretch leaves
|
|
280
|
+
the top edge to the refresher.
|
|
281
|
+
- Spinner: `bf-spinner` inherits `currentColor`; set `text-primary` on an ancestor to color it,
|
|
282
|
+
the same as any other spinner usage.
|
|
283
|
+
- No background or border of its own beyond the disc — it never changes the scroller's
|
|
284
|
+
`overscroll-behavior`.
|
|
285
|
+
|
|
286
|
+
## Accessibility
|
|
287
|
+
|
|
288
|
+
A visually hidden `role="status"` region with `aria-live="polite"` is always in the accessibility
|
|
289
|
+
tree: empty at rest, it reads `provideBfLabels()`'s `refreshing` ("Refreshing") while a refresh runs, so the change is announced. The
|
|
290
|
+
disc and its spinner are `aria-hidden="true"` — purely visual, with no status of their own. The pull gesture itself has no
|
|
291
|
+
keyboard equivalent — wire a manual refresh action (e.g. a toolbar button calling the same reload
|
|
292
|
+
method) for keyboard and switch-control users.
|
|
293
|
+
|
|
294
|
+
## Inputs
|
|
295
|
+
|
|
296
|
+
| Name | Type | Default | Description |
|
|
297
|
+
| ---------- | --------- | ------- | -------------------------------------------------------------------------------- |
|
|
298
|
+
| `disabled` | `boolean` | `false` | Disables pull tracking; the disc never moves and `refreshRequested` never emits. |
|
|
299
|
+
|
|
300
|
+
## Outputs
|
|
301
|
+
|
|
302
|
+
| Name | Payload | Description |
|
|
303
|
+
| ------------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
304
|
+
| `refreshRequested` | `BfRefresherRefreshEvent` | Emitted when a pull past the threshold is released; call `complete()` to end it. |
|
|
305
|
+
| `refresh` | `BfRefresherRefreshEvent` | Emitted with the same event as `refreshRequested`, together with it. **Deprecated** (0.3.0 use refreshRequested; removed in 0.4.0) |
|
|
306
|
+
|
|
307
|
+
## Methods
|
|
308
|
+
|
|
309
|
+
### `complete(): void`
|
|
310
|
+
|
|
311
|
+
Ends a refresh: animates the disc away (skipped under reduced motion) and resets state.
|
|
312
|
+
|
|
313
|
+
## Notes
|
|
314
|
+
|
|
315
|
+
### Do
|
|
316
|
+
|
|
317
|
+
- Place exactly one `bf-refresher` as the first child of `bf-content`.
|
|
318
|
+
- Always call `complete()` from the `refreshRequested` handler, including on a failed reload.
|
|
319
|
+
- Leave `overscroll-behavior` alone; `bf-refresher` depends on `bf-content`'s `overscroll-contain`.
|
|
320
|
+
|
|
321
|
+
### Don't
|
|
322
|
+
|
|
323
|
+
- Don't put `bf-refresher` anywhere the page can scroll away from the top on load — a pull only
|
|
324
|
+
starts at `scrollTop === 0`.
|
|
325
|
+
- Don't provide a second way to trigger the same pull gesture; give keyboard users a real button
|
|
326
|
+
instead of relying on the pull.
|
|
327
|
+
|
|
328
|
+
## Migration notes
|
|
329
|
+
|
|
330
|
+
### 0.3.0
|
|
331
|
+
|
|
332
|
+
**Deprecated:** `refresh` → `refreshRequested`, typed with the new exported
|
|
333
|
+
`BfRefresherRefreshEvent` (`{ complete: () => void }`, was an inline object type). Both outputs
|
|
334
|
+
emit the same event object for each completed pull until `refresh` is removed in 0.4.0 (see
|
|
335
|
+
Migration 23).
|
|
336
|
+
|
|
337
|
+
**Behaviour:** the status region announces `provideBfLabels()`'s `refreshing` ("Refreshing").
|
|
338
|
+
|
|
339
|
+
### 0.2.0
|
|
340
|
+
|
|
341
|
+
New `bf-refresher`: pull-to-refresh, placed as the first child of `bf-content`. It tracks Pointer
|
|
342
|
+
Events on the page's scroller (read from `BfPageContext`, falling back to its own parent element)
|
|
343
|
+
only while `scrollTop === 0`. A press by itself changes nothing; only once a move clears an 8 px
|
|
344
|
+
downward slop (out-vertical'ing any horizontal travel) does it claim the gesture, capture the
|
|
345
|
+
pointer on the scroller, and start resisting the drag at `delta * 0.5` (capped at 72 px) into a
|
|
346
|
+
40 px `bg-surface shadow-raised rounded-full` disc holding a `bf-spinner`, from a resting −48 px
|
|
347
|
+
up to a 64 px threshold — so a plain click on the content underneath (a row, a button) still
|
|
348
|
+
reaches it. Crossing the threshold ticks `BfHapticsService.impact('light')` once; releasing past
|
|
349
|
+
it sets `refreshing` (a readonly signal), parks the disc at 0 and emits `refresh` with `{
|
|
350
|
+
complete: () => void }` — releasing before the threshold animates the disc back to rest instead.
|
|
351
|
+
`complete()` animates the disc away and clears `refreshing`, after which the next pull starts
|
|
352
|
+
working again; `disabled` stops tracking pulls entirely, and further pulls are ignored while
|
|
353
|
+
already `refreshing`. The park/settle/`complete()` transitions use the Web Animations API;
|
|
354
|
+
`prefers-reduced-motion` only skips those — the disc still tracks the pointer 1:1 during the pull
|
|
355
|
+
itself, since direct manipulation isn't motion. For touch, a non-passive `touchmove` guard,
|
|
356
|
+
attached only while a press is tracked, prevents a downward drag at the top (and every move once
|
|
357
|
+
claimed) so the browser does not take it as a pan and cancel the pointer stream; upward drags keep
|
|
358
|
+
scrolling. Only the scroller's own `lostpointercapture` ends a pull, not the bubbling one from the
|
|
359
|
+
row that held the touch's implicit capture. The disc (with its spinner) is `aria-hidden` and
|
|
360
|
+
`visibility: hidden` until a pull is claimed (so its shadow never bleeds above the content); a
|
|
361
|
+
separate, permanent visually hidden `role="status" aria-live="polite"` region is empty at rest and
|
|
362
|
+
reads "Refreshing" while a refresh runs. It never touches the scroller's `overscroll-behavior`.
|
|
363
|
+
|
|
364
|
+
Changed: on iOS a touch rides the native bounce. In iOS mode (`BfPlatformService.mode()`) a touch
|
|
365
|
+
press (`pointerType === 'touch'`, or a `touchstart`, tracked with passive listeners) is not
|
|
366
|
+
claimed — no pointer capture, no `preventDefault`, no `touchmove` guard — so the scroller
|
|
367
|
+
rubber-bands natively. While a finger is down, passive `scroll` events with a negative `scrollTop`
|
|
368
|
+
show the disc at its resting −48 px, inside the gap the bounce opens, once `−scrollTop > 8`, with
|
|
369
|
+
`opacity` = progress and `scale(0.6 + 0.4 × progress)` (progress = `min(1, −scrollTop / 64)`) and
|
|
370
|
+
one light haptic at 1. Lifting the last finger at `−scrollTop ≥ 64` sets `refreshing`, settles the
|
|
371
|
+
disc to 0 and emits `refresh`; otherwise the disc hides. A negative `scrollTop` with no finger down
|
|
372
|
+
(a fling's overshoot) never shows it. Material mode and a mouse or pen in iOS mode keep the pull
|
|
373
|
+
unchanged. The host carries `data-bf-refresher` while not `disabled`. A pen press in iOS mode,
|
|
374
|
+
which also fires touch events, never starts a bounce, so its trailing `touchend` cannot hide the
|
|
375
|
+
parked disc; the bounce writes the disc's styles only when it shows, changes or hides, not on
|
|
376
|
+
every scroll; and a scroller change or destroy mid-bounce resets the disc's `opacity`, `transform`
|
|
377
|
+
and `visibility`.
|