@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,845 @@
|
|
|
1
|
+
# bf-sheet
|
|
2
|
+
|
|
3
|
+
Bottom sheet with snap points, drag to dismiss and a backdrop
|
|
4
|
+
|
|
5
|
+
`BfSheetComponent` from `@bitforgehq/ui/mobile` · Overlays · since 0.2.0
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, inject, signal } from '@angular/core';
|
|
11
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
12
|
+
import {
|
|
13
|
+
BfContentComponent,
|
|
14
|
+
BfHeaderComponent,
|
|
15
|
+
BfPageComponent,
|
|
16
|
+
BfSheetService,
|
|
17
|
+
} from '@bitforgehq/ui/mobile';
|
|
18
|
+
import { ShowcaseSheetComponent } from './showcase-sheet.component';
|
|
19
|
+
|
|
20
|
+
/** Snap points of the showcase sheet: half the screen, then almost all of it. */
|
|
21
|
+
export const SHOWCASE_SHEET_SNAP_POINTS = [0.5, 0.92];
|
|
22
|
+
|
|
23
|
+
/** `bf-sheet` showcase: a button opens a form in a bottom sheet with two snap points. */
|
|
24
|
+
@Component({
|
|
25
|
+
selector: 'app-sheet-showcase',
|
|
26
|
+
imports: [BfPageComponent, BfHeaderComponent, BfContentComponent, BfButtonComponent],
|
|
27
|
+
template: `
|
|
28
|
+
<bf-page data-showcase="sheet">
|
|
29
|
+
<bf-header title="Sheet" [largeTitle]="false" />
|
|
30
|
+
<bf-content>
|
|
31
|
+
<div class="flex flex-col gap-4 p-4">
|
|
32
|
+
<p class="text-caption text-text-muted">
|
|
33
|
+
Drag the grabber between the two snap points; drag it down or flick to close, or tap the
|
|
34
|
+
backdrop.
|
|
35
|
+
</p>
|
|
36
|
+
<bf-button (clicked)="open()">Open sheet</bf-button>
|
|
37
|
+
<p data-showcase-result class="text-body">{{ result() }}</p>
|
|
38
|
+
</div>
|
|
39
|
+
</bf-content>
|
|
40
|
+
</bf-page>
|
|
41
|
+
`,
|
|
42
|
+
})
|
|
43
|
+
export class SheetShowcasePage {
|
|
44
|
+
private readonly sheets = inject(BfSheetService);
|
|
45
|
+
|
|
46
|
+
protected readonly result = signal('No list yet.');
|
|
47
|
+
|
|
48
|
+
async open(): Promise<void> {
|
|
49
|
+
const ref = this.sheets.open<ShowcaseSheetComponent, string>(ShowcaseSheetComponent, {
|
|
50
|
+
snapPoints: SHOWCASE_SHEET_SNAP_POINTS,
|
|
51
|
+
ariaLabel: 'New list',
|
|
52
|
+
});
|
|
53
|
+
const name = await ref.afterClosed;
|
|
54
|
+
this.result.set(name ? `Created “${name}”.` : 'Dismissed.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Overview
|
|
60
|
+
|
|
61
|
+
The bottom-sheet container that `BfSheetService.open()` renders on the overlay core: a
|
|
62
|
+
bottom-anchored `bg-surface` card with a grabber, resting at snap points (fractions of the
|
|
63
|
+
viewport height, applied as `--bf-sheet-height`) and rendering the content component or
|
|
64
|
+
template with `BF_OVERLAY_DATA` and `BfSheetRef` injectable. Dragging the handle row, any
|
|
65
|
+
element marked `data-bf-sheet-drag` (a header) or the content (unless it can scroll up) moves
|
|
66
|
+
the sheet with Pointer Events under pointer capture: rubber-banded above the top point, released
|
|
67
|
+
onto the nearest point (a fling faster than 0.3 px/ms picks the direction), and dismissed when
|
|
68
|
+
released more than 25 % below the lowest point or flung down from it — if the sheet is
|
|
69
|
+
dismissible. The content scrolls only at the top snap point, and at every point while the soft
|
|
70
|
+
keyboard is up. While the keyboard overlays the sheet's bottom edge (iOS, a mobile browser) the
|
|
71
|
+
sheet is lifted by the covered height (`translate`, its bottom safe-area padding dropped); while
|
|
72
|
+
the keyboard is up its height is capped at the keyboard's top edge minus the safe-area top minus
|
|
73
|
+
8 px (a drag starts from that capped height), and the content region's `bfKeyboardAssist`
|
|
74
|
+
scrolls the focused control 24 px clear of the keyboard. The lift follows the keyboard (250 ms,
|
|
75
|
+
instant under `prefers-reduced-motion`). Hiding the keyboard restores the snap height at once
|
|
76
|
+
(the drop is instant, so the top edge stays on screen) and scrolls content left scrolled below
|
|
77
|
+
the top point back to its top. With two or more snap points the grabber is a button: a press,
|
|
78
|
+
`Enter` or `Space` moves one point up (from the top point back to the lowest), the arrow keys
|
|
79
|
+
step, `Home` and `End` jump; it is named `expand` or `collapse` (`BF_LABELS`) after the next
|
|
80
|
+
press, then `sheetPosition`, and every settle on another point announces `sheetPosition`
|
|
81
|
+
politely. A dismissible sheet starts with a visually hidden Close (`BF_LABELS.close`) that screen
|
|
82
|
+
readers reach and a keyboard user sees when focused. Not used directly in templates.
|
|
83
|
+
|
|
84
|
+
A bottom sheet. `BfSheetService.open(content, options)` renders a component or template in a
|
|
85
|
+
`bf-sheet` container — a bottom-anchored card with a grabber, over a backdrop — that rests at snap
|
|
86
|
+
points given as fractions of the viewport height. Drag the grabber (or a header marked
|
|
87
|
+
`data-bf-sheet-drag`) to move between them; drag it far enough down, or flick it, and it closes.
|
|
88
|
+
The content reads its data from `BF_OVERLAY_DATA` and closes itself with a result through the
|
|
89
|
+
injected `BfSheetRef`. You never place `<bf-sheet>` in a template yourself.
|
|
90
|
+
|
|
91
|
+
## When to use
|
|
92
|
+
|
|
93
|
+
Use `BfSheetService` in the Capacitor shell for secondary content over the current page that the user drags between heights or away: a filter panel, a detail preview. Use [BfModalService](https://bitforge-ui.web.app/api/bf-modal-service) instead for a full task that closes itself, [BfActionSheetService](https://bitforge-ui.web.app/api/bf-action-sheet-service) for a list of actions, and [BfDialogService](https://bitforge-ui.web.app/api/bf-dialog-service) on desktop.
|
|
94
|
+
|
|
95
|
+
## Basic usage
|
|
96
|
+
|
|
97
|
+
The content is an ordinary standalone component. It injects the data passed as `data` and the
|
|
98
|
+
`BfSheetRef`, and closes with a result; the caller awaits `afterClosed`.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { Component, inject, signal } from '@angular/core';
|
|
102
|
+
import { BfButtonComponent, BfInputComponent } from '@bitforgehq/ui';
|
|
103
|
+
import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
|
|
104
|
+
import { BF_OVERLAY_DATA } from '@bitforgehq/ui/overlay';
|
|
105
|
+
|
|
106
|
+
/** A task as the sheet creates it. */
|
|
107
|
+
export interface Task {
|
|
108
|
+
title: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The sheet: a draggable header, a field and Save. */
|
|
112
|
+
@Component({
|
|
113
|
+
selector: 'app-new-task',
|
|
114
|
+
imports: [BfButtonComponent, BfInputComponent],
|
|
115
|
+
template: `
|
|
116
|
+
<header data-bf-sheet-drag class="text-heading px-4 pb-2">
|
|
117
|
+
New task in {{ data.listName }}
|
|
118
|
+
</header>
|
|
119
|
+
<div class="flex flex-col gap-3 px-4">
|
|
120
|
+
<bf-input label="Title" [(value)]="title" />
|
|
121
|
+
<bf-button (clicked)="save()">Save</bf-button>
|
|
122
|
+
</div>
|
|
123
|
+
`,
|
|
124
|
+
})
|
|
125
|
+
export class NewTaskComponent {
|
|
126
|
+
protected readonly data = inject<{ listName: string }>(BF_OVERLAY_DATA);
|
|
127
|
+
private readonly sheet = inject<BfSheetRef<Task>>(BfSheetRef);
|
|
128
|
+
protected readonly title = signal('');
|
|
129
|
+
|
|
130
|
+
save(): void {
|
|
131
|
+
this.sheet.close({ title: this.title() || 'Buy milk' });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A sheet that creates a task.
|
|
137
|
+
*
|
|
138
|
+
* `open()` passes the list name as data and awaits the new task, or `undefined` when dismissed.
|
|
139
|
+
*
|
|
140
|
+
* @preview none
|
|
141
|
+
*/
|
|
142
|
+
@Component({
|
|
143
|
+
selector: 'app-example-sheet-basic',
|
|
144
|
+
imports: [BfButtonComponent],
|
|
145
|
+
template: `
|
|
146
|
+
<div class="flex flex-col items-start gap-3">
|
|
147
|
+
<bf-button (clicked)="add()">New task</bf-button>
|
|
148
|
+
<p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
|
|
149
|
+
</div>
|
|
150
|
+
`,
|
|
151
|
+
})
|
|
152
|
+
export class SheetBasicExampleComponent {
|
|
153
|
+
private readonly sheets = inject(BfSheetService);
|
|
154
|
+
protected readonly result = signal('');
|
|
155
|
+
|
|
156
|
+
protected async add(): Promise<void> {
|
|
157
|
+
const ref = this.sheets.open<NewTaskComponent, Task>(NewTaskComponent, {
|
|
158
|
+
data: { listName: 'Groceries' },
|
|
159
|
+
});
|
|
160
|
+
const task = await ref.afterClosed; // undefined when dismissed
|
|
161
|
+
this.result.set(task ? `Added "${task.title}".` : 'Dismissed.');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A template works too; its context is `{ $implicit: ref, data }`:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { Component, type TemplateRef, inject, signal, viewChild } from '@angular/core';
|
|
170
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
171
|
+
import { BfSheetService } from '@bitforgehq/ui/mobile';
|
|
172
|
+
|
|
173
|
+
/** The filters a list shows. */
|
|
174
|
+
interface Filters {
|
|
175
|
+
open: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A sheet from a template.
|
|
180
|
+
*
|
|
181
|
+
* The `ng-template` receives `{ $implicit: ref, data }`; Apply closes the sheet with the data.
|
|
182
|
+
*
|
|
183
|
+
* @preview none
|
|
184
|
+
*/
|
|
185
|
+
@Component({
|
|
186
|
+
selector: 'app-example-sheet-template',
|
|
187
|
+
imports: [BfButtonComponent],
|
|
188
|
+
template: `
|
|
189
|
+
<div class="flex flex-col items-start gap-3">
|
|
190
|
+
<bf-button variant="outline" (clicked)="open()">Filters</bf-button>
|
|
191
|
+
<p class="text-caption text-text-muted">{{ result() }}</p>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<ng-template #filters let-ref let-data="data">
|
|
195
|
+
<div class="flex flex-col gap-3 px-4 pb-4">
|
|
196
|
+
<p class="text-heading">Filters</p>
|
|
197
|
+
<bf-button (clicked)="ref.close(data)">Apply</bf-button>
|
|
198
|
+
</div>
|
|
199
|
+
</ng-template>
|
|
200
|
+
`,
|
|
201
|
+
})
|
|
202
|
+
export class SheetTemplateExampleComponent {
|
|
203
|
+
private readonly sheets = inject(BfSheetService);
|
|
204
|
+
private readonly filters = viewChild.required<TemplateRef<unknown>>('filters');
|
|
205
|
+
protected readonly result = signal('');
|
|
206
|
+
|
|
207
|
+
protected async open(): Promise<void> {
|
|
208
|
+
const ref = this.sheets.open<unknown, Filters>(this.filters(), { data: { open: true } });
|
|
209
|
+
const applied = await ref.afterClosed;
|
|
210
|
+
this.result.set(applied ? 'Filters applied.' : 'Dismissed.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Snap points
|
|
216
|
+
|
|
217
|
+
`snapPoints` are fractions of the viewport height (the keyboard-free height from
|
|
218
|
+
`BfKeyboardService.restHeight()`, else `innerHeight`), default `[0.5, 0.92]`; `initial` is the index
|
|
219
|
+
the sheet opens at (default 0). The height is applied as `--bf-sheet-height` on the `bf-sheet`
|
|
220
|
+
host. Move programmatically with `snapTo(index)` and read the current point from `snapIndex()`:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { Component, computed, inject } from '@angular/core';
|
|
224
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
225
|
+
import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
|
|
226
|
+
|
|
227
|
+
/** Project details that expand to the top snap point. */
|
|
228
|
+
@Component({
|
|
229
|
+
selector: 'app-details',
|
|
230
|
+
imports: [BfButtonComponent],
|
|
231
|
+
template: `
|
|
232
|
+
<div class="flex flex-col gap-3 px-4 pb-4">
|
|
233
|
+
<p data-bf-sheet-drag class="text-heading">Atlas rebrand</p>
|
|
234
|
+
@if (!expanded()) {
|
|
235
|
+
<bf-button variant="tonal" (clicked)="ref.snapTo(2)">Show everything</bf-button>
|
|
236
|
+
} @else {
|
|
237
|
+
<p class="text-body">Owner, budget, milestones and files.</p>
|
|
238
|
+
}
|
|
239
|
+
</div>
|
|
240
|
+
`,
|
|
241
|
+
})
|
|
242
|
+
export class DetailsComponent {
|
|
243
|
+
protected readonly ref = inject(BfSheetRef);
|
|
244
|
+
protected readonly expanded = computed(() => this.ref.snapIndex() === 2);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A sheet with three snap points.
|
|
249
|
+
*
|
|
250
|
+
* It opens at the middle point (`initial: 1`); `snapTo(2)` moves it up and `snapIndex()` tracks
|
|
251
|
+
* the current point, also after a drag.
|
|
252
|
+
*
|
|
253
|
+
* @preview none
|
|
254
|
+
*/
|
|
255
|
+
@Component({
|
|
256
|
+
selector: 'app-example-sheet-snap-points',
|
|
257
|
+
imports: [BfButtonComponent],
|
|
258
|
+
template: `<bf-button (clicked)="open()">Project details</bf-button>`,
|
|
259
|
+
})
|
|
260
|
+
export class SheetSnapPointsExampleComponent {
|
|
261
|
+
private readonly sheets = inject(BfSheetService);
|
|
262
|
+
|
|
263
|
+
protected open(): void {
|
|
264
|
+
this.sheets.open(DetailsComponent, { snapPoints: [0.3, 0.6, 0.92], initial: 1 });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
On release, a fling faster than 0.3 px/ms goes to the next point in its direction; otherwise the
|
|
270
|
+
sheet settles on the nearest point. Above the top point the drag is rubber-banded (30 % of the
|
|
271
|
+
pointer's travel).
|
|
272
|
+
|
|
273
|
+
With two or more snap points the grabber is also a button, so the sheet can be resized without
|
|
274
|
+
dragging: a tap, a click, <kbd>Enter</kbd> or <kbd>Space</kbd> moves it one point up, and from the
|
|
275
|
+
top point back to the lowest; the arrow keys, <kbd>Home</kbd> and <kbd>End</kbd> step and jump
|
|
276
|
+
between the points (see [Keyboard](#keyboard)). Its name says what the next press does and where
|
|
277
|
+
the sheet is: `provideBfLabels()`'s `expand` ("Expand") below the top point, `collapse`
|
|
278
|
+
("Collapse") at it, then `sheetPosition(position, count)` ("Sheet at 1 of 2"). Each time the sheet
|
|
279
|
+
settles on another snap point, however it got there (the button, a key, a drag or `snapTo()`),
|
|
280
|
+
`sheetPosition` is announced politely through CDK `LiveAnnouncer`. With a single snap point the
|
|
281
|
+
grabber is decorative, there is no button and nothing is announced.
|
|
282
|
+
|
|
283
|
+
## Focus on open
|
|
284
|
+
|
|
285
|
+
A sheet opens with focus on itself (`autoFocus: 'panel'`, the default): the panel is a named
|
|
286
|
+
`role="dialog"` with `tabindex="-1"`, so a screen reader starts reading inside the sheet and no soft
|
|
287
|
+
keyboard comes up to cover it (on Android, focusing a text field on open raises the keyboard at
|
|
288
|
+
once). Tab moves on to the sheet's controls. Pass `autoFocus: 'first-field'` when the sheet exists
|
|
289
|
+
to be typed into (a quick-add field): focus goes to the element marked `autofocus` (or
|
|
290
|
+
`cdkFocusInitial`), else to the first focusable element of the content, never to the sheet's own
|
|
291
|
+
Close and grabber, and the keyboard comes up with the sheet.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import { Component, inject, signal } from '@angular/core';
|
|
295
|
+
import { BfButtonComponent, BfInputComponent } from '@bitforgehq/ui';
|
|
296
|
+
import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
|
|
297
|
+
|
|
298
|
+
/** A quick-add sheet whose only job is typing a title. */
|
|
299
|
+
@Component({
|
|
300
|
+
selector: 'app-quick-add',
|
|
301
|
+
imports: [BfButtonComponent, BfInputComponent],
|
|
302
|
+
template: `
|
|
303
|
+
<div class="flex flex-col gap-3 px-4">
|
|
304
|
+
<h2 class="text-heading">Quick add</h2>
|
|
305
|
+
<bf-input label="Title" [(value)]="title" />
|
|
306
|
+
<bf-button (clicked)="save()">Add</bf-button>
|
|
307
|
+
</div>
|
|
308
|
+
`,
|
|
309
|
+
})
|
|
310
|
+
export class QuickAddComponent {
|
|
311
|
+
private readonly sheet = inject<BfSheetRef<string>>(BfSheetRef);
|
|
312
|
+
protected readonly title = signal('');
|
|
313
|
+
|
|
314
|
+
save(): void {
|
|
315
|
+
this.sheet.close(this.title());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* A sheet that opens with the keyboard up.
|
|
321
|
+
*
|
|
322
|
+
* By default a sheet focuses itself, so no soft keyboard hides it; `autoFocus: 'first-field'`
|
|
323
|
+
* focuses the Title field instead (or the element marked `autofocus`).
|
|
324
|
+
*
|
|
325
|
+
* @preview none
|
|
326
|
+
*/
|
|
327
|
+
@Component({
|
|
328
|
+
selector: 'app-example-sheet-autofocus',
|
|
329
|
+
imports: [BfButtonComponent],
|
|
330
|
+
template: `
|
|
331
|
+
<div class="flex flex-col items-start gap-3">
|
|
332
|
+
<bf-button (clicked)="quickAdd()">Quick add</bf-button>
|
|
333
|
+
<p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
|
|
334
|
+
</div>
|
|
335
|
+
`,
|
|
336
|
+
})
|
|
337
|
+
export class SheetAutofocusExampleComponent {
|
|
338
|
+
private readonly sheets = inject(BfSheetService);
|
|
339
|
+
protected readonly result = signal('');
|
|
340
|
+
|
|
341
|
+
protected async quickAdd(): Promise<void> {
|
|
342
|
+
const ref = this.sheets.open<QuickAddComponent, string>(QuickAddComponent, {
|
|
343
|
+
snapPoints: [0.92],
|
|
344
|
+
autoFocus: 'first-field',
|
|
345
|
+
});
|
|
346
|
+
const title = await ref.afterClosed;
|
|
347
|
+
this.result.set(title ? `Added "${title}".` : 'Dismissed.');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The sheet provides its content a fresh `BfPageContext` (with `mainLandmark` false), so a
|
|
353
|
+
`bf-content` inside it is no second `main` and never takes over the page's scroller, also when
|
|
354
|
+
the sheet is opened with the page's injector.
|
|
355
|
+
|
|
356
|
+
Action sheets and modals are unchanged: `BfActionSheetService` and `BfModalService` keep the
|
|
357
|
+
focus they had: an action sheet starts on its Cancel button, and a modal on the first focusable
|
|
358
|
+
element of its content.
|
|
359
|
+
|
|
360
|
+
## Dragging and scrolling
|
|
361
|
+
|
|
362
|
+
The 24 px handle row always drags the sheet; mark any other element (typically the header of your
|
|
363
|
+
content) with `data-bf-sheet-drag` to make it a drag handle too — give it `cursor-grab
|
|
364
|
+
active:cursor-grabbing` as well, so its pointer affordance matches the handle row's. A press only
|
|
365
|
+
becomes a drag after 6 px of vertical travel, so buttons inside a header still get their clicks.
|
|
366
|
+
The same holds on the grabber button: a press released within 6 px is a click (it cycles the snap
|
|
367
|
+
points), a longer one drags the sheet and does not.
|
|
368
|
+
|
|
369
|
+
The content scrolls only at the top snap point (`overflow-y-auto` there, `overflow-hidden` below
|
|
370
|
+
it, always `overscroll-contain`) — and at every point while the soft keyboard is up. Below the top
|
|
371
|
+
point, dragging the content moves the sheet; at the top point, a downward drag on the content
|
|
372
|
+
moves the sheet only once the content is scrolled to the top — the same hand-off as iOS and Ionic
|
|
373
|
+
sheets.
|
|
374
|
+
|
|
375
|
+
## Soft keyboard
|
|
376
|
+
|
|
377
|
+
A sheet keeps its focused field and its actions above the soft keyboard:
|
|
378
|
+
|
|
379
|
+
- **Lift.** Where the keyboard overlays the page (iOS with `Keyboard.resize: 'none'`, a mobile
|
|
380
|
+
browser), the sheet moves up by the covered height while the keyboard is up, and drops its
|
|
381
|
+
bottom safe-area padding (the keyboard covers the home indicator). It slides up with the
|
|
382
|
+
keyboard (250 ms, instant under `prefers-reduced-motion`); when it arrives, the content's
|
|
383
|
+
keyboard assist measures again. On Android the WebView itself shrinks above the keyboard, so
|
|
384
|
+
there is nothing to lift.
|
|
385
|
+
- **Cap.** While the keyboard is up, the sheet is at most as tall as the space between the
|
|
386
|
+
keyboard's top edge and the safe-area top, minus 8 px — in phone landscape that can be less than
|
|
387
|
+
its snap height. A drag starts from that capped height.
|
|
388
|
+
- **Scroll and reveal.** While the keyboard is up the content scrolls at every snap point, and its
|
|
389
|
+
keyboard assist (`bfKeyboardAssist`) scrolls the focused field 24 px above the keyboard.
|
|
390
|
+
- **Restore.** Hiding the keyboard removes the lift and the cap together, at once: the sheet
|
|
391
|
+
rests at its snap height again with its top edge on screen throughout, and content left
|
|
392
|
+
scrolled below the top point starts at its top again.
|
|
393
|
+
|
|
394
|
+
The snap heights themselves never change. Keep the primary action close below
|
|
395
|
+
the fields, so the lifted sheet shows it without scrolling in portrait.
|
|
396
|
+
|
|
397
|
+
## Dismissible
|
|
398
|
+
|
|
399
|
+
By default a backdrop tap, `Escape`, the hardware back button and a release more than 25 % of the
|
|
400
|
+
lowest point's height below it (or a downward fling from it) close the sheet, resolving
|
|
401
|
+
`afterClosed` with `undefined`. With `dismissible: false` only `close()` does: the backdrop and
|
|
402
|
+
`Escape` are ignored, a downward drag rubber-bands and settles back, and the hardware back button
|
|
403
|
+
is swallowed while the sheet is open (it does not fall through to the page behind).
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
import { Component, inject } from '@angular/core';
|
|
407
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
408
|
+
import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
|
|
409
|
+
|
|
410
|
+
/** Terms that must be accepted. */
|
|
411
|
+
@Component({
|
|
412
|
+
selector: 'app-terms-sheet',
|
|
413
|
+
imports: [BfButtonComponent],
|
|
414
|
+
template: `
|
|
415
|
+
<div class="flex flex-col gap-3 p-4">
|
|
416
|
+
<p class="text-body">Accept the updated terms to continue.</p>
|
|
417
|
+
<bf-button (clicked)="ref.close()">Done</bf-button>
|
|
418
|
+
</div>
|
|
419
|
+
`,
|
|
420
|
+
})
|
|
421
|
+
export class TermsComponent {
|
|
422
|
+
protected readonly ref = inject(BfSheetRef);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* A sheet that must be answered.
|
|
427
|
+
*
|
|
428
|
+
* `dismissible: false` turns off the drag down, the backdrop and the back button.
|
|
429
|
+
*
|
|
430
|
+
* @preview none
|
|
431
|
+
*/
|
|
432
|
+
@Component({
|
|
433
|
+
selector: 'app-example-sheet-dismissible',
|
|
434
|
+
imports: [BfButtonComponent],
|
|
435
|
+
template: `<bf-button (clicked)="open()">Show the terms</bf-button>`,
|
|
436
|
+
})
|
|
437
|
+
export class SheetDismissibleExampleComponent {
|
|
438
|
+
private readonly sheets = inject(BfSheetService);
|
|
439
|
+
|
|
440
|
+
protected open(): void {
|
|
441
|
+
this.sheets.open(TermsComponent, { dismissible: false });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Include a visible close or cancel control in the sheet's content, as `bf-modal` requires; the
|
|
447
|
+
built-in hidden Close is the safety net for screen readers, not a replacement. Every dismissible
|
|
448
|
+
sheet renders that hidden Close (`provideBfLabels()`'s `close`, "Close") as its first element: a
|
|
449
|
+
screen reader reaches it by swiping, and a keyboard user sees it as a pill at the top-end corner
|
|
450
|
+
when <kbd>Tab</kbd> lands on it. A `dismissible: false` sheet has none.
|
|
451
|
+
|
|
452
|
+
## Navigation
|
|
453
|
+
|
|
454
|
+
A navigation to another URL path closes the sheet, resolving `afterClosed` with `undefined`, also
|
|
455
|
+
a `dismissible: false` one: the page it belongs to is gone. A query- or fragment-only change
|
|
456
|
+
leaves it open; a matrix-parameter change (`/list;sort=name`) is a path change and closes it. In
|
|
457
|
+
the `/mobile` shell every push, every pop (the header back button, swipe-back,
|
|
458
|
+
`BfNavController.back()`) and every tab switch changes the path, so a sheet opened on a page
|
|
459
|
+
closes when that page is left. The Android hardware back button with a sheet open closes the
|
|
460
|
+
sheet only (a `dismissible: false` sheet swallows the press); its handler runs at priority 100,
|
|
461
|
+
above the page pop, so one press never closes it and pops the page as well. The same holds for
|
|
462
|
+
Android back in a browser (Chrome, a PWA, no Capacitor): where `CloseWatcher` exists, the back
|
|
463
|
+
press closes the sheet and leaves the URL alone, and the next press goes back in history. Pass
|
|
464
|
+
`closeOnNavigation: false` for a sheet that should stay over the next page.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import { Component, inject } from '@angular/core';
|
|
468
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
469
|
+
import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
|
|
470
|
+
|
|
471
|
+
/** A player that outlives its page. */
|
|
472
|
+
@Component({
|
|
473
|
+
selector: 'app-player',
|
|
474
|
+
imports: [BfButtonComponent],
|
|
475
|
+
template: `
|
|
476
|
+
<div class="flex flex-col gap-3 p-4">
|
|
477
|
+
<p class="text-body">Now playing: Team call recording</p>
|
|
478
|
+
<bf-button (clicked)="ref.close()">Done</bf-button>
|
|
479
|
+
</div>
|
|
480
|
+
`,
|
|
481
|
+
})
|
|
482
|
+
export class PlayerComponent {
|
|
483
|
+
protected readonly ref = inject(BfSheetRef);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* A sheet that stays open across navigations.
|
|
488
|
+
*
|
|
489
|
+
* `closeOnNavigation: false` keeps a player open while the page behind it changes.
|
|
490
|
+
*
|
|
491
|
+
* @preview none
|
|
492
|
+
*/
|
|
493
|
+
@Component({
|
|
494
|
+
selector: 'app-example-sheet-navigation',
|
|
495
|
+
imports: [BfButtonComponent],
|
|
496
|
+
template: `<bf-button (clicked)="open()">Play</bf-button>`,
|
|
497
|
+
})
|
|
498
|
+
export class SheetNavigationExampleComponent {
|
|
499
|
+
private readonly sheets = inject(BfSheetService);
|
|
500
|
+
|
|
501
|
+
protected open(): void {
|
|
502
|
+
this.sheets.open(PlayerComponent, { closeOnNavigation: false });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
A sheet opened before the first navigation ends (a "What's new" sheet from the root component or
|
|
508
|
+
an `APP_INITIALIZER`) closes when the start URL redirects (`''` to `'today'`): open it after the
|
|
509
|
+
first `NavigationEnd`, or pass `closeOnNavigation: false`. A `history.pushState()` made outside
|
|
510
|
+
Angular's `Location` is not seen.
|
|
511
|
+
|
|
512
|
+
A route `canDeactivate` guard protects a form sheet's unsaved input only against in-app
|
|
513
|
+
navigations: the sheet stays open while the guard's confirm asks. On the web the browser's back
|
|
514
|
+
button changes the URL before the guard runs, so the sheet has already closed when "Stay" is
|
|
515
|
+
answered (the Android hardware back button never gets that far: the sheet takes it first). A
|
|
516
|
+
sheet that must keep its draft passes `closeOnNavigation: false` or saves the draft as it
|
|
517
|
+
changes.
|
|
518
|
+
|
|
519
|
+
## Labels and the injector
|
|
520
|
+
|
|
521
|
+
Default strings come from `provideBfLabels()` (`sheet`, "Sheet"; `close`, "Close"; `expand`,
|
|
522
|
+
"Expand"; `collapse`, "Collapse"; `sheetPosition`, "Sheet at 1 of 2"). The service is a root singleton, so it reads
|
|
523
|
+
the app-level labels; to use a feature's own `provideBfLabels()` (in a component's `providers`),
|
|
524
|
+
pass that component's injector. It then also becomes the parent of the sheet's content injector,
|
|
525
|
+
so the content resolves the feature's providers.
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import { Component, Injector, inject } from '@angular/core';
|
|
529
|
+
import { BfButtonComponent, provideBfLabels } from '@bitforgehq/ui';
|
|
530
|
+
import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
|
|
531
|
+
|
|
532
|
+
/** A note in the feature's language. */
|
|
533
|
+
@Component({
|
|
534
|
+
selector: 'app-new-note',
|
|
535
|
+
imports: [BfButtonComponent],
|
|
536
|
+
template: `
|
|
537
|
+
<div class="flex flex-col gap-3 p-4">
|
|
538
|
+
<p class="text-body">Neue Notiz</p>
|
|
539
|
+
<bf-button (clicked)="ref.close()">Fertig</bf-button>
|
|
540
|
+
</div>
|
|
541
|
+
`,
|
|
542
|
+
})
|
|
543
|
+
export class NewNoteComponent {
|
|
544
|
+
protected readonly ref = inject(BfSheetRef);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* A sheet that reads a feature's labels.
|
|
549
|
+
*
|
|
550
|
+
* This feature provides German labels; passing the opener's injector is what names the sheet
|
|
551
|
+
* "Blatt" for screen readers (its `sheet` label) and lets its content read the same labels.
|
|
552
|
+
*
|
|
553
|
+
* @preview none
|
|
554
|
+
*/
|
|
555
|
+
@Component({
|
|
556
|
+
selector: 'app-example-sheet-injector',
|
|
557
|
+
imports: [BfButtonComponent],
|
|
558
|
+
providers: [provideBfLabels({ sheet: 'Blatt' })],
|
|
559
|
+
template: `<bf-button (clicked)="open()">Neue Notiz</bf-button>`,
|
|
560
|
+
})
|
|
561
|
+
export class SheetInjectorExampleComponent {
|
|
562
|
+
private readonly sheets = inject(BfSheetService);
|
|
563
|
+
private readonly injector = inject(Injector);
|
|
564
|
+
|
|
565
|
+
protected open(): void {
|
|
566
|
+
this.sheets.open(NewNoteComponent, { injector: this.injector });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Desktop
|
|
572
|
+
|
|
573
|
+
`BfSheetService`'s bottom sheet is phone-shaped and meant for the Capacitor shell
|
|
574
|
+
(`provideBfShell()`). On a desktop web app, use the desktop overlays from `@bitforgehq/ui/overlay`
|
|
575
|
+
instead: they need only `@angular/cdk`, no Capacitor and no router. A desktop dialog locks the
|
|
576
|
+
page's scroll, makes the page behind it `inert`, and traps and restores focus.
|
|
577
|
+
|
|
578
|
+
Open a form or a detail view in a `bf-dialog` with `BfDialogService.open()`, and a short list of
|
|
579
|
+
actions in a `bf-menu` from a `bf-menu-button`.
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
import { Component, inject, signal } from '@angular/core';
|
|
583
|
+
import { BfButtonComponent, BfCheckboxComponent } from '@bitforgehq/ui';
|
|
584
|
+
import {
|
|
585
|
+
BF_DIALOG_DATA,
|
|
586
|
+
BfDialogComponent,
|
|
587
|
+
BfDialogRef,
|
|
588
|
+
BfDialogService,
|
|
589
|
+
} from '@bitforgehq/ui/overlay';
|
|
590
|
+
|
|
591
|
+
/** The filters a list shows. */
|
|
592
|
+
export interface Filters {
|
|
593
|
+
open: boolean;
|
|
594
|
+
mine: boolean;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** A filter dialog that returns the edited filters and can highlight one of them. */
|
|
598
|
+
@Component({
|
|
599
|
+
selector: 'app-filter-dialog',
|
|
600
|
+
imports: [BfButtonComponent, BfCheckboxComponent, BfDialogComponent],
|
|
601
|
+
template: `
|
|
602
|
+
<bf-dialog title="Filters">
|
|
603
|
+
<div class="flex flex-col gap-2">
|
|
604
|
+
<bf-checkbox
|
|
605
|
+
label="Open tasks only"
|
|
606
|
+
[class]="highlighted() === 'status' ? 'bg-primary-soft rounded-field' : ''"
|
|
607
|
+
[(checked)]="open"
|
|
608
|
+
/>
|
|
609
|
+
<bf-checkbox label="Assigned to me" [(checked)]="mine" />
|
|
610
|
+
</div>
|
|
611
|
+
<div bfSlot="footer">
|
|
612
|
+
<bf-button variant="outline" (clicked)="ref.close()">Cancel</bf-button>
|
|
613
|
+
<bf-button (clicked)="ref.close({ open: open(), mine: mine() })">Apply</bf-button>
|
|
614
|
+
</div>
|
|
615
|
+
</bf-dialog>
|
|
616
|
+
`,
|
|
617
|
+
})
|
|
618
|
+
export class FilterDialogComponent {
|
|
619
|
+
protected readonly ref = inject<BfDialogRef<Filters>>(BfDialogRef);
|
|
620
|
+
private readonly current = inject<Filters>(BF_DIALOG_DATA);
|
|
621
|
+
protected readonly open = signal(this.current.open);
|
|
622
|
+
protected readonly mine = signal(this.current.mine);
|
|
623
|
+
protected readonly highlighted = signal('');
|
|
624
|
+
|
|
625
|
+
highlight(field: string): void {
|
|
626
|
+
this.highlighted.set(field);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* A typed dialog result.
|
|
632
|
+
*
|
|
633
|
+
* `BfDialogRef<Filters, FilterDialogComponent>` types both the result and `componentInstance`;
|
|
634
|
+
* a dismissed dialog keeps the current filters.
|
|
635
|
+
*/
|
|
636
|
+
@Component({
|
|
637
|
+
selector: 'app-example-dialog-results',
|
|
638
|
+
imports: [BfButtonComponent],
|
|
639
|
+
template: `
|
|
640
|
+
<div class="flex flex-col items-start gap-3">
|
|
641
|
+
<bf-button variant="outline" (clicked)="edit()">Filters…</bf-button>
|
|
642
|
+
<p class="text-caption text-text-muted">
|
|
643
|
+
{{ filters().open ? 'Open' : 'All' }} tasks · {{ filters().mine ? 'mine' : 'everyone' }}
|
|
644
|
+
</p>
|
|
645
|
+
</div>
|
|
646
|
+
`,
|
|
647
|
+
})
|
|
648
|
+
export class DialogResultsExampleComponent {
|
|
649
|
+
private readonly dialogs = inject(BfDialogService);
|
|
650
|
+
protected readonly filters = signal<Filters>({ open: true, mine: false });
|
|
651
|
+
|
|
652
|
+
protected async edit(): Promise<void> {
|
|
653
|
+
const current = this.filters();
|
|
654
|
+
const ref = this.dialogs.open<FilterDialogComponent, Filters, Filters>(FilterDialogComponent, {
|
|
655
|
+
data: current,
|
|
656
|
+
});
|
|
657
|
+
ref.componentInstance?.highlight('status');
|
|
658
|
+
this.filters.set((await ref.afterClosed) ?? current);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
## Theming
|
|
664
|
+
|
|
665
|
+
- Card: `bg-surface`, `rounded-t-card` (`--radius-card`), `shadow-floating`, `pb-safe-bottom`
|
|
666
|
+
(`--spacing-safe-bottom`).
|
|
667
|
+
- Grabber: `bg-control-off`, 36 × 5 px, `rounded-full`; `ButtonText` under forced colors. With a
|
|
668
|
+
mouse the handle row shows a `grab` cursor (`grabbing` while pressed) and the grabber darkens to
|
|
669
|
+
`text-muted` on hover.
|
|
670
|
+
- Grabber button (two or more snap points): the full-width 24 px handle row, extended to 44 px
|
|
671
|
+
under a coarse pointer, downward only (20 px into the top of the content, so a tap just above
|
|
672
|
+
the sheet still reaches the backdrop); `focus-visible:ring-4 ring-ring` (inset) for the
|
|
673
|
+
keyboard focus. Keep the content's own controls out of its top 20 px, or give them `relative`
|
|
674
|
+
positioning (library buttons have it), so they paint above the extension.
|
|
675
|
+
- Hidden Close (dismissible sheets): `sr-only` until focused, then a `rounded-full
|
|
676
|
+
bg-surface-muted text-label-sm` pill at the top-end corner (`end-3 top-3`) with the focus ring.
|
|
677
|
+
- Backdrop: `bf-overlay-backdrop` from the overlay core (a black scrim: 40 % in light mode, 60 % in dark).
|
|
678
|
+
- Height: `--bf-sheet-height` on the `bf-sheet` host, written by the component — do not override it.
|
|
679
|
+
- Keyboard: inline `translate`, `padding-bottom` and `max-height` on the host while the keyboard
|
|
680
|
+
is up — do not set your own.
|
|
681
|
+
|
|
682
|
+
## Accessibility
|
|
683
|
+
|
|
684
|
+
The overlay panel is a `role="dialog"` with `aria-modal="true"` and a name: the
|
|
685
|
+
`ariaLabel` option when given; else the first heading in the content that is not `aria-hidden`
|
|
686
|
+
(`h1`–`h6` or `role="heading"`), found once the content has rendered (or when it first appears later, e.g. behind `@if`) and
|
|
687
|
+
referenced through
|
|
688
|
+
`aria-labelledby` (it gets an id `bf-overlay-title-<n>` when it has none); else
|
|
689
|
+
`provideBfLabels()`'s `sheet` ("Sheet"); the page stack behind it is `inert`. Focus moves to the
|
|
690
|
+
panel itself (or, with `autoFocus: 'first-field'`, to the content's first field), Tab is trapped
|
|
691
|
+
inside, and focus returns to the element that opened the sheet on close. `Escape` and the hardware back button close a dismissible
|
|
692
|
+
sheet. The sheet's own controls, the hidden Close and the grabber button, come first in the Tab
|
|
693
|
+
order but never take the initial focus. VoiceOver on iOS does not deliver its two-finger scrub
|
|
694
|
+
(escape) gesture to web content, so the hidden Close is how a VoiceOver user leaves a sheet: keep
|
|
695
|
+
a visible close or cancel control in the content as well.
|
|
696
|
+
|
|
697
|
+
With two or more snap points the grabber is a button (named after the next press and the current
|
|
698
|
+
position, "Expand, Sheet at 1 of 2"), the single-pointer alternative to dragging that WCAG 2.5.7
|
|
699
|
+
(Dragging Movements) asks for. A name change on a focused button is not reliably re-read, so the
|
|
700
|
+
new position is also announced politely ("Sheet at 2 of 2") whenever the sheet settles on another
|
|
701
|
+
point; focus stays on the button. A screen reader's double tap (a click with `detail` 0) always
|
|
702
|
+
moves the sheet, and the grabber ignores key combinations with <kbd>Alt</kbd>, <kbd>Ctrl</kbd> or
|
|
703
|
+
<kbd>Meta</kbd>. With one snap point the grabber is `aria-hidden`. Under
|
|
704
|
+
`prefers-reduced-motion` the sheet cross-fades in and out and settles on snap points instantly;
|
|
705
|
+
a drag still follows the pointer.
|
|
706
|
+
|
|
707
|
+
### Keyboard
|
|
708
|
+
|
|
709
|
+
The grabber button (two or more snap points) takes these keys; none of them dismisses the sheet.
|
|
710
|
+
|
|
711
|
+
| Key | Action |
|
|
712
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------- |
|
|
713
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves between the sheet's controls (Close and grabber first), wrapping |
|
|
714
|
+
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Grabber: one snap point up; from the top point, back to the lowest |
|
|
715
|
+
| <kbd>ArrowUp</kbd> | Grabber: one snap point up (stops at the top point) |
|
|
716
|
+
| <kbd>ArrowDown</kbd> | Grabber: one snap point down (stops at the lowest point, never closes) |
|
|
717
|
+
| <kbd>Home</kbd> | Grabber: the lowest snap point |
|
|
718
|
+
| <kbd>End</kbd> | Grabber: the top snap point |
|
|
719
|
+
| <kbd>Escape</kbd> | Closes a dismissible sheet |
|
|
720
|
+
|
|
721
|
+
## Notes
|
|
722
|
+
|
|
723
|
+
### Do
|
|
724
|
+
|
|
725
|
+
- Name the sheet by its purpose ("New task", "Filters"): start the content with a heading, or pass `ariaLabel`.
|
|
726
|
+
- Mark your content's header with `data-bf-sheet-drag` so the whole top area drags.
|
|
727
|
+
- Leave `autoFocus` at `'panel'` for a sheet the user reads or picks from; use `'first-field'` only
|
|
728
|
+
for a sheet that exists to be typed into.
|
|
729
|
+
- Close with a result through `BfSheetRef` and await `afterClosed` in the caller.
|
|
730
|
+
- Keep a visible close or cancel control in the content; the hidden Close is for screen readers.
|
|
731
|
+
|
|
732
|
+
### Don't
|
|
733
|
+
|
|
734
|
+
- Don't use a non-dismissible sheet for content the user may want to leave — use a modal with an
|
|
735
|
+
explicit cancel instead.
|
|
736
|
+
- Don't make the lowest snap point so small that the content's primary action is hidden.
|
|
737
|
+
- Don't nest scrolling containers inside the content; let the sheet's content region scroll.
|
|
738
|
+
|
|
739
|
+
## Migration notes
|
|
740
|
+
|
|
741
|
+
### 0.3.0
|
|
742
|
+
|
|
743
|
+
**Behaviour:** a navigation to another URL path (a push, a pop, a tab switch) closes it with no
|
|
744
|
+
result, also a `dismissible: false` one; new `closeOnNavigation` option (default true). Migration:
|
|
745
|
+
pass `closeOnNavigation: false` to keep it open (see Migration 19).
|
|
746
|
+
|
|
747
|
+
**Behaviour:** named by the first heading: without `ariaLabel`, the panel is named by the first
|
|
748
|
+
heading in its content that is not `aria-hidden` (`h1`–`h6` or `role="heading"`, given an id
|
|
749
|
+
`bf-overlay-title-<n>` when it has none) through `aria-labelledby`, once the content has rendered;
|
|
750
|
+
without a heading, by `provideBfLabels()`'s `sheet` ("Sheet"). An explicit `ariaLabel` always wins.
|
|
751
|
+
|
|
752
|
+
New `injector` option: labels resolve from it per call (default the root injector) and it becomes
|
|
753
|
+
the parent of the content's injector, so a feature-level `provideBfLabels()` and the feature's
|
|
754
|
+
other providers reach the sheet.
|
|
755
|
+
|
|
756
|
+
A heading that renders after the overlay opened (behind `@if`, a `resource()`) still names it: the
|
|
757
|
+
panel is watched until its first heading appears.
|
|
758
|
+
|
|
759
|
+
JSDoc of `sheet-ref.ts` for the API reference: `@summary`/`@since` on the exported symbols; `@param`
|
|
760
|
+
docs. No behaviour change.
|
|
761
|
+
|
|
762
|
+
JSDoc of `sheet.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
763
|
+
`@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
764
|
+
|
|
765
|
+
**Behaviour:** the keyboard lift and cap (`translate`, `padding-bottom`, `max-height`,
|
|
766
|
+
`transition`) are written in the render phase (`afterRenderEffect({ write })`) instead of an
|
|
767
|
+
`effect`; internal timing only, no API change.
|
|
768
|
+
|
|
769
|
+
With two or more snap points the grabber is a button (`[data-bf-sheet-grabber-button]`), so the
|
|
770
|
+
sheet can be resized without dragging (WCAG 2.5.7): a tap, click, `Enter` or `Space` moves it one
|
|
771
|
+
point up, and from the top point back to the lowest; `ArrowUp`/`ArrowDown` step one point (never
|
|
772
|
+
past the ends, never dismissing), `Home`/`End` jump to the lowest/top point, all through
|
|
773
|
+
`snapTo()` (instant under reduced motion). It is named after the next press and the position
|
|
774
|
+
("Expand, Sheet at 1 of 2": `expand` below the top point, `collapse` at it, then
|
|
775
|
+
`sheetPosition`); focus stays on it, and every settle on another point (button, key, drag or
|
|
776
|
+
`snapTo()`) announces `sheetPosition` politely through CDK `LiveAnnouncer`. A drag that starts on
|
|
777
|
+
it still drags (a pointer click within the 6 px slop is the press; a keyboard or screen-reader
|
|
778
|
+
click, `detail` 0, is never swallowed). Alt, Ctrl and Meta combinations are left alone. It is
|
|
779
|
+
the full-width 24 px handle row, extended downward to 44 px under a coarse pointer, so a tap just
|
|
780
|
+
above the sheet reaches the backdrop. With one snap point the grabber stays decorative
|
|
781
|
+
(`aria-hidden`) and nothing is announced.
|
|
782
|
+
|
|
783
|
+
**Behaviour:** every dismissible sheet renders a visually hidden "Close" button
|
|
784
|
+
(`provideBfLabels()`'s `close`) as its first element, shown as a pill at the top-end corner when
|
|
785
|
+
focused; activating it closes the sheet. VoiceOver reaches it by swiping, since WKWebView does not
|
|
786
|
+
deliver the two-finger scrub to web content. A `dismissible: false` sheet has none. The Close and
|
|
787
|
+
the grabber button never take the initial focus. Keep a visible close or cancel control in the
|
|
788
|
+
content (Migration 28).
|
|
789
|
+
|
|
790
|
+
**Behaviour:** a sheet opens with focus on its panel (the `role="dialog"` pane, named and
|
|
791
|
+
`tabindex="-1"`), so a screen reader lands inside it and no soft keyboard comes up on open. New
|
|
792
|
+
option `autoFocus?: BfSheetAutoFocus` (`'panel' | 'first-field'`, default `'panel'`; new exported
|
|
793
|
+
type), mapped to the overlay core's `autoFocus` (`'first-field'` → `'first-tabbable'`, which
|
|
794
|
+
honours `[autofocus]`/`[cdkFocusInitial]` and skips the sheet's Close and grabber). Action sheets
|
|
795
|
+
and modals are unchanged. See Migration 33.
|
|
796
|
+
|
|
797
|
+
`bf-sheet` provides a fresh `BfPageContext` (`mainLandmark` false) to its content: a `bf-content`
|
|
798
|
+
in a sheet opened with a page's injector no longer resolves the page's context, so it is no second
|
|
799
|
+
`main` and does not replace the page's registered scroller.
|
|
800
|
+
|
|
801
|
+
Android back in a browser (no Capacitor) closes an open sheet first where `CloseWatcher` exists
|
|
802
|
+
(see `### overlay core`, Migration 35).
|
|
803
|
+
|
|
804
|
+
### 0.2.0
|
|
805
|
+
|
|
806
|
+
New `BfSheetService.open(componentOrTemplate, { snapPoints, initial, dismissible, data, ariaLabel })`
|
|
807
|
+
in `@bitforgehq/ui/mobile`, a bottom sheet on the overlay core (`position: 'bottom'`, back priority
|
|
808
|
+
100, not counted as a modal; slides up on open and down on close). It returns a `BfSheetRef`
|
|
809
|
+
(extends `BfOverlayRef`) with `snapTo(index)` and a `snapIndex` signal; the content injects the
|
|
810
|
+
caller's `data` as `BF_OVERLAY_DATA` and the ref as `BfSheetRef` and `BfOverlayRef` (a template
|
|
811
|
+
gets `{ $implicit: ref, data }`). The `bf-sheet` container is a `bg-surface rounded-t-card
|
|
812
|
+
shadow-floating` card with `pb-safe-bottom`, a 36 × 5 px `bg-control-off` grabber (`aria-hidden`,
|
|
813
|
+
`ButtonText` under forced colors)
|
|
814
|
+
in a 24 px `touch-none` handle row and a content region; its height is `--bf-sheet-height`, a snap
|
|
815
|
+
point (fraction of `BfKeyboardService.restHeight()`, else `innerHeight`; default `[0.5, 0.92]`)
|
|
816
|
+
re-applied on resize. Dragging the handle row, any `[data-bf-sheet-drag]` element or the content
|
|
817
|
+
(below the top point, or downward at the top point while scrolled to 0) moves the sheet with
|
|
818
|
+
Pointer Events under pointer capture and a non-passive `touchmove` guard: rubber-banded (30 %)
|
|
819
|
+
above the top point, released onto the nearest point or, faster than 0.3 px/ms, the next one in
|
|
820
|
+
the fling's direction; a release more than 25 % of the lowest point's height below it, or a
|
|
821
|
+
downward fling from it, dismisses. The content scrolls (`overflow-y-auto`) only at the top point,
|
|
822
|
+
`overflow-hidden` below it, always `overscroll-contain`. `dismissible: false` ignores the backdrop,
|
|
823
|
+
`Escape` and downward dismissal and swallows the hardware back button while open. Settling is a
|
|
824
|
+
260 ms WAAPI animation, instant under `prefers-reduced-motion` (the drag still tracks the
|
|
825
|
+
pointer). The panel is `role="dialog"` `aria-modal` with `aria-label` (default "Sheet"), focus
|
|
826
|
+
moves in and returns on close.
|
|
827
|
+
|
|
828
|
+
Fixed: the handle row shows a `grab` cursor (`grabbing` while pressed) and the grabber darkens to
|
|
829
|
+
`text-muted` on hover.
|
|
830
|
+
|
|
831
|
+
Fixed (behaviour change): the soft keyboard no longer covers a sheet's focused field and actions.
|
|
832
|
+
While the keyboard overlays the sheet's bottom edge (iOS with `Keyboard.resize: 'none'`, a mobile
|
|
833
|
+
browser), `bf-sheet` is lifted by the covered height (an inline `translate`, bottom safe-area
|
|
834
|
+
padding dropped, since the keyboard covers the home indicator); while the keyboard is up at all
|
|
835
|
+
(also Android, where the WebView resizes and nothing is lifted) its height is capped at the
|
|
836
|
+
keyboard's top edge minus the safe-area top minus 8 px (`max-height`), and the content region
|
|
837
|
+
scrolls at every snap point, with `bfKeyboardAssist` revealing the focused control 24 px above the
|
|
838
|
+
keyboard. The lift follows the keyboard with a 250 ms `translate` transition on the iOS curve
|
|
839
|
+
(instant under `prefers-reduced-motion`), and is left alone once the sheet is closing. The snap
|
|
840
|
+
heights (`--bf-sheet-height`) are unchanged, and a drag of a capped sheet starts from its rendered
|
|
841
|
+
(capped) height, so it moves from the first pixel. Hiding the keyboard removes the lift and the
|
|
842
|
+
cap together and at once (the drop is not animated, so a sheet at a snap point taller than the cap
|
|
843
|
+
never has its top edge above the screen), so the sheet rests at its snap height again, and content
|
|
844
|
+
left scrolled while the keyboard was up returns to its top when the keyboard hides below the top
|
|
845
|
+
snap point (a drag between snap points without the keyboard keeps the scroll offset, as before).
|