@ng-native/expo 0.5.0 → 0.6.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/dist/expo-ui-components.d.ts +177 -9
- package/dist/expo-ui-components.js +311 -2
- package/dist/expo-ui-components.js.map +1 -1
- package/dist/expo-ui.js +5 -0
- package/dist/expo-ui.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ui-bottom-sheet-content.d.ts +18 -0
- package/dist/ui-bottom-sheet-content.js +29 -0
- package/dist/ui-bottom-sheet-content.js.map +1 -0
- package/dist/ui-bottom-sheet-view.d.ts +25 -0
- package/dist/ui-bottom-sheet-view.js +55 -0
- package/dist/ui-bottom-sheet-view.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,5 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed components for the `@expo/ui` views an app reaches for most, named and shaped as SwiftUI's.
|
|
3
|
+
*
|
|
4
|
+
* `EXPO_UI_VIEWS` makes every view reachable by name, and a name is all the renderer needs. What a
|
|
5
|
+
* name cannot give is a type: under strict templates an unknown element is an error, and so is
|
|
6
|
+
* each prop on it. These are the same elements, claimed by a component whose inputs carry the
|
|
7
|
+
* props' types and whose host bindings pass each one straight to the node, so nothing changes at
|
|
8
|
+
* runtime. An unset input stays `undefined` and never reaches native.
|
|
9
|
+
*
|
|
10
|
+
* Events are declared as outputs so that `$event` is typed. SwiftUI's events are never emitted:
|
|
11
|
+
* Angular binds a template's `(dateChange)` to the output *and* to the element, and it is the
|
|
12
|
+
* element's own event that arrives, once. Emitting from a host listener as well would deliver it
|
|
13
|
+
* twice; a host listener of the same name as the output listens to the output itself and recurses.
|
|
14
|
+
* So subscribe in a template, not to the output in code.
|
|
15
|
+
*
|
|
16
|
+
* Where Compose names a prop or an event differently, a component sends Android its own names and
|
|
17
|
+
* emits the output from a host listener for Compose's event, which SwiftUI never sends. Only a few
|
|
18
|
+
* do: the toggle, slider, button, progress, stacks and slot, and the date picker.
|
|
19
|
+
*
|
|
20
|
+
* Only the views and props an app has needed are here; the rest stay names, as before. Add a view
|
|
21
|
+
* when a template wants it typed, from `@expo/ui`'s own props for it.
|
|
22
|
+
*
|
|
23
|
+
* Still register the names with `registerExpoUiViews`: the component types the element, the
|
|
24
|
+
* registration is what makes it commit as the SwiftUI or Compose view.
|
|
25
|
+
*/
|
|
26
|
+
import { InjectionToken, type Signal } from '@angular/core';
|
|
1
27
|
import type { NativeState } from './native-state.ts';
|
|
2
28
|
import { type NativeSyntheticEvent } from '@ng-native/fabric';
|
|
29
|
+
import { type UiBottomSheetPresentedEvent } from './ui-bottom-sheet-view.ts';
|
|
3
30
|
import * as i0 from "@angular/core";
|
|
4
31
|
/** One SwiftUI modifier, as `@expo/ui`'s modifier functions build them: `{ $type: 'frame', ... }`. */
|
|
5
32
|
export interface UiModifier {
|
|
@@ -19,8 +46,8 @@ export declare class UiHost {
|
|
|
19
46
|
readonly vertical?: boolean;
|
|
20
47
|
readonly horizontal?: boolean;
|
|
21
48
|
} | undefined>;
|
|
22
|
-
protected readonly matchVertical:
|
|
23
|
-
protected readonly matchHorizontal:
|
|
49
|
+
protected readonly matchVertical: Signal<boolean | undefined>;
|
|
50
|
+
protected readonly matchHorizontal: Signal<boolean | undefined>;
|
|
24
51
|
readonly ignoreSafeArea: import("@angular/core").InputSignal<"all" | "keyboard" | "container" | undefined>;
|
|
25
52
|
readonly useViewportSizeMeasurement: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
|
|
26
53
|
static ɵfac: i0.ɵɵFactoryDeclaration<UiHost, never>;
|
|
@@ -157,9 +184,9 @@ export declare class UiSlider {
|
|
|
157
184
|
/** A whole number of steps, which is all Compose can draw. */
|
|
158
185
|
private readonly wholeSteps;
|
|
159
186
|
/** SwiftUI takes a step size, and draws 0 to 1 unless it has both ends. */
|
|
160
|
-
protected readonly stepSize:
|
|
187
|
+
protected readonly stepSize: Signal<number | undefined>;
|
|
161
188
|
/** Compose takes the number of points between the ends. */
|
|
162
|
-
protected readonly stepsBetween:
|
|
189
|
+
protected readonly stepsBetween: Signal<number | undefined>;
|
|
163
190
|
static ɵfac: i0.ɵɵFactoryDeclaration<UiSlider, never>;
|
|
164
191
|
static ɵcmp: i0.ɵɵComponentDeclaration<UiSlider, "ui-slider", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "min": { "alias": "min"; "required": false; "isSignal": true; }; "max": { "alias": "max"; "required": false; "isSignal": true; }; "steps": { "alias": "steps"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, { "valueChanged": "valueChanged"; }, never, never, true, never>;
|
|
165
192
|
}
|
|
@@ -240,9 +267,9 @@ export declare class UiDatePicker {
|
|
|
240
267
|
readonly touch: import("@angular/core").OutputEmitterRef<void>;
|
|
241
268
|
readonly dateChange: import("@angular/core").OutputEmitterRef<UiDateChangeEvent>;
|
|
242
269
|
private readonly android;
|
|
243
|
-
protected readonly resolvedSelection:
|
|
244
|
-
protected readonly initialDate:
|
|
245
|
-
protected readonly resolvedModifiers:
|
|
270
|
+
protected readonly resolvedSelection: Signal<string | undefined>;
|
|
271
|
+
protected readonly initialDate: Signal<number | null | undefined>;
|
|
272
|
+
protected readonly resolvedModifiers: Signal<readonly UiModifier[] | undefined>;
|
|
246
273
|
/**
|
|
247
274
|
* SwiftUI's picker cannot be empty: it shows today, and reports that as a change the moment it
|
|
248
275
|
* appears. Taken as the user's choice, an empty date field would fill itself in, pass
|
|
@@ -283,8 +310,8 @@ export declare class UiPicker {
|
|
|
283
310
|
readonly selection: string | number;
|
|
284
311
|
}>>;
|
|
285
312
|
/** Each option's `tag`, one array per value, so a pass hands native the same objects. */
|
|
286
|
-
protected readonly tags:
|
|
287
|
-
protected readonly resolvedModifiers:
|
|
313
|
+
protected readonly tags: Signal<Map<string | number, readonly UiModifier[]>>;
|
|
314
|
+
protected readonly resolvedModifiers: Signal<readonly UiModifier[] | undefined>;
|
|
288
315
|
constructor();
|
|
289
316
|
static ɵfac: i0.ɵɵFactoryDeclaration<UiPicker, never>;
|
|
290
317
|
static ɵcmp: i0.ɵɵComponentDeclaration<UiPicker, "ui-picker", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "systemImage": { "alias": "systemImage"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "pickerStyle": { "alias": "pickerStyle"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "touch": "touch"; "selectionChange": "selectionChange"; }, never, ["*"], true, never>;
|
|
@@ -604,4 +631,145 @@ export declare class UiSection {
|
|
|
604
631
|
static ɵfac: i0.ɵɵFactoryDeclaration<UiSection, never>;
|
|
605
632
|
static ɵcmp: i0.ɵɵComponentDeclaration<UiSection, "ui-section", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
|
|
606
633
|
}
|
|
634
|
+
/**
|
|
635
|
+
* A SwiftUI `Group`, iOS only: its children as they are, with no layout of its own. What it is for
|
|
636
|
+
* is `modifiers` that belong to several views at once, or to content that takes none itself, as a
|
|
637
|
+
* sheet's presentation modifiers go on a group around a `ui-view-host`.
|
|
638
|
+
*/
|
|
639
|
+
export declare class UiGroup {
|
|
640
|
+
protected readonly nativeView: true;
|
|
641
|
+
readonly modifiers: import("@angular/core").InputSignal<readonly UiModifier[] | undefined>;
|
|
642
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<UiGroup, never>;
|
|
643
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<UiGroup, "ui-group", never, { "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* A height a SwiftUI sheet rests at, as `@expo/ui`'s `PresentationDetent`: about half the screen,
|
|
647
|
+
* all of it, a fraction of it from 0 to 1, or a height in points.
|
|
648
|
+
*/
|
|
649
|
+
export type UiPresentationDetent = 'medium' | 'large' | {
|
|
650
|
+
readonly fraction: number;
|
|
651
|
+
} | {
|
|
652
|
+
readonly height: number;
|
|
653
|
+
};
|
|
654
|
+
/** The functions `@expo/ui` defines on Compose's sheet, called with the view's tag as `this`. */
|
|
655
|
+
export interface UiBottomSheetViewFunctions {
|
|
656
|
+
/** Slides the sheet away, answering once it has gone. */
|
|
657
|
+
hide?(this: {
|
|
658
|
+
nativeTag: number;
|
|
659
|
+
}): Promise<void>;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* A sheet that slides up from the bottom of the screen, opened from any component: SwiftUI's on
|
|
663
|
+
* iOS, Material's on Android.
|
|
664
|
+
*
|
|
665
|
+
* ```html
|
|
666
|
+
* <ui-bottom-sheet [(open)]="sorting" fitToContents>
|
|
667
|
+
* <app-sort-options (done)="sorting.set(false)" />
|
|
668
|
+
* </ui-bottom-sheet>
|
|
669
|
+
* ```
|
|
670
|
+
*
|
|
671
|
+
* `open` presents and dismisses it, and is written back when the user dismisses it. Its content is
|
|
672
|
+
* what is written inside it, the app's own components, in the tree while the sheet is on screen.
|
|
673
|
+
*
|
|
674
|
+
* The two platforms drive a sheet differently, and this is where that difference ends. SwiftUI's
|
|
675
|
+
* stays in the tree and is presented by `isPresented`, reporting `isPresentedChange` and
|
|
676
|
+
* `dismiss`. Compose's shows for as long as it is in the tree, and reports `dismissRequest`.
|
|
677
|
+
*
|
|
678
|
+
* Unlike the other components here, the element this is mounted on is a plain view, which takes
|
|
679
|
+
* no room and needs no `ui-host` around it. The sheet is inside it, wrapped as `@expo/ui`'s own
|
|
680
|
+
* React component wraps one: a `ui-host`, the sheet, on iOS a `ui-group` that carries the
|
|
681
|
+
* presentation modifiers, and a `ui-view-host` holding the one view the content goes in.
|
|
682
|
+
*/
|
|
683
|
+
export declare class UiBottomSheet {
|
|
684
|
+
/**
|
|
685
|
+
* The functions of Compose's sheet, none of them on iOS, or null where `@expo/ui` is not in the
|
|
686
|
+
* build and there is no sheet. Overridden in a test to hide a sheet without a device.
|
|
687
|
+
*/
|
|
688
|
+
static readonly SOURCE: InjectionToken<UiBottomSheetViewFunctions | null>;
|
|
689
|
+
private readonly functions;
|
|
690
|
+
private readonly engine;
|
|
691
|
+
private readonly injector;
|
|
692
|
+
private readonly sheet;
|
|
693
|
+
/** Whether the sheet is on screen. Written back as false when the user dismisses it. */
|
|
694
|
+
readonly open: import("@angular/core").ModelSignal<boolean>;
|
|
695
|
+
/**
|
|
696
|
+
* The sheet is as tall as its content. Without it the sheet rests at half the screen's height
|
|
697
|
+
* and drags up to all of it. Read as the sheet opens.
|
|
698
|
+
*/
|
|
699
|
+
readonly fitToContents: import("@angular/core").InputSignalWithTransform<boolean, unknown>;
|
|
700
|
+
/**
|
|
701
|
+
* The heights the sheet rests at, iOS only: SwiftUI's `presentationDetents`. Compose's sheet has
|
|
702
|
+
* a half and a full height and no others, so Android takes no notice of this. On iOS a sheet
|
|
703
|
+
* given detents rests at them rather than at the height of its content, whatever `fitToContents`
|
|
704
|
+
* says, as `@expo/ui`'s own sheets do. None, or an empty list, is the sheet without them.
|
|
705
|
+
*/
|
|
706
|
+
readonly detents: import("@angular/core").InputSignal<readonly UiPresentationDetent[] | undefined>;
|
|
707
|
+
/** Whether the grabber shows at the top of the sheet. */
|
|
708
|
+
readonly showDragIndicator: import("@angular/core").InputSignalWithTransform<boolean, unknown>;
|
|
709
|
+
/**
|
|
710
|
+
* The sheet has finished closing, whether the user dismissed it or `open` was set false: the
|
|
711
|
+
* moment another sheet or a dialog can be presented.
|
|
712
|
+
*/
|
|
713
|
+
readonly dismissed: import("@angular/core").OutputEmitterRef<void>;
|
|
714
|
+
/** SwiftUI's sheet stays in the tree; anywhere else it is there while it shows. */
|
|
715
|
+
protected readonly swiftUI: boolean;
|
|
716
|
+
protected readonly width: Signal<number | undefined>;
|
|
717
|
+
/**
|
|
718
|
+
* Whether the content is in the tree: from when the sheet opens until it has gone, which is
|
|
719
|
+
* after `open` turns false, so that it slides away with its content still in it.
|
|
720
|
+
*/
|
|
721
|
+
protected readonly mounted: import("@angular/core").WritableSignal<boolean>;
|
|
722
|
+
/**
|
|
723
|
+
* Whether the sheet is sized to its content, as it opened: a view host takes its sizing once,
|
|
724
|
+
* as it mounts. SwiftUI's sheet is not where it has detents to rest at.
|
|
725
|
+
*/
|
|
726
|
+
protected readonly fits: Signal<boolean>;
|
|
727
|
+
/**
|
|
728
|
+
* SwiftUI reads how a sheet is presented from modifiers on its content, not from props. The
|
|
729
|
+
* detents of a sheet not sized to its content are followed while it is open.
|
|
730
|
+
*/
|
|
731
|
+
protected readonly presentation: Signal<readonly UiModifier[]>;
|
|
732
|
+
/**
|
|
733
|
+
* The one view a view host holds. Sized to its content it needs a width of its own, the
|
|
734
|
+
* window's; otherwise it fills the height the sheet gives it.
|
|
735
|
+
*/
|
|
736
|
+
protected readonly body: Signal<{
|
|
737
|
+
width: number | undefined;
|
|
738
|
+
flexGrow?: undefined;
|
|
739
|
+
height?: undefined;
|
|
740
|
+
} | {
|
|
741
|
+
flexGrow: number;
|
|
742
|
+
height: number;
|
|
743
|
+
width?: undefined;
|
|
744
|
+
}>;
|
|
745
|
+
/** Whether `open` has turned false on a sheet that has yet to go. */
|
|
746
|
+
private closing;
|
|
747
|
+
/** Whether Compose is sliding the sheet away. */
|
|
748
|
+
private hiding;
|
|
749
|
+
constructor();
|
|
750
|
+
protected presented(event: UiBottomSheetPresentedEvent): void;
|
|
751
|
+
protected dismissRequested(event: NativeSyntheticEvent): void;
|
|
752
|
+
/**
|
|
753
|
+
* SwiftUI has finished dismissing the sheet. After a swipe it says so before it reports that the
|
|
754
|
+
* sheet is no longer presented, so a sheet that has gone while `open`, and was not closing, is
|
|
755
|
+
* one the user dismissed.
|
|
756
|
+
*/
|
|
757
|
+
protected gone(event: NativeSyntheticEvent): void;
|
|
758
|
+
/**
|
|
759
|
+
* Whether an event is this sheet's own, which it then stops. Events bubble, so a `dismiss` from
|
|
760
|
+
* a modal in the content arrives here too, and the sheet's own would go on to a listener around
|
|
761
|
+
* it.
|
|
762
|
+
*/
|
|
763
|
+
private own;
|
|
764
|
+
/**
|
|
765
|
+
* `open` turned false. SwiftUI dismisses its sheet on the prop, and says when it has gone.
|
|
766
|
+
* Compose shows a sheet for as long as it is in the tree, so one taken out is gone at once:
|
|
767
|
+
* its `hide` slides it away first, as `@expo/ui`'s own component calls it before unmounting.
|
|
768
|
+
*/
|
|
769
|
+
private close;
|
|
770
|
+
/** The sheet has gone: its content leaves the tree. */
|
|
771
|
+
private finish;
|
|
772
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<UiBottomSheet, never>;
|
|
773
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<UiBottomSheet, "ui-bottom-sheet", never, { "open": { "alias": "open"; "required": false; "isSignal": true; }; "fitToContents": { "alias": "fitToContents"; "required": false; "isSignal": true; }; "detents": { "alias": "detents"; "required": false; "isSignal": true; }; "showDragIndicator": { "alias": "showDragIndicator"; "required": false; "isSignal": true; }; }, { "open": "openChange"; "dismissed": "dismissed"; }, never, ["*"], true, never>;
|
|
774
|
+
}
|
|
607
775
|
export {};
|
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
* Still register the names with `registerExpoUiViews`: the component types the element, the
|
|
24
24
|
* registration is what makes it commit as the SwiftUI or Compose view.
|
|
25
25
|
*/
|
|
26
|
-
import { Component, DestroyRef, forwardRef, ElementRef, booleanAttribute, computed, inject, input, model, output, signal, } from '@angular/core';
|
|
27
|
-
import { HostEngine, keepNativeView, nativePlatform, } from '@ng-native/fabric';
|
|
26
|
+
import { Component, DestroyRef, forwardRef, ElementRef, InjectionToken, Injector, afterNextRender, booleanAttribute, computed, effect, inject, input, linkedSignal, model, output, signal, untracked, viewChild, } from '@angular/core';
|
|
27
|
+
import { Engine, HostEngine, keepNativeView, nativePlatform, } from '@ng-native/fabric';
|
|
28
|
+
import { optional } from "./native.js";
|
|
28
29
|
import { optionalBoolean, optionalNumber } from "./transforms.js";
|
|
30
|
+
import { UiBottomSheetContent } from "./ui-bottom-sheet-content.js";
|
|
31
|
+
import { UiBottomSheetView } from "./ui-bottom-sheet-view.js";
|
|
29
32
|
import * as i0 from "@angular/core";
|
|
30
33
|
/**
|
|
31
34
|
* Says the component's host is the `@expo/ui` view its selector is registered as. A component of
|
|
@@ -1194,4 +1197,310 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
|
|
|
1194
1197
|
host: { '[title]': 'title()', '[modifiers]': 'modifiers()' },
|
|
1195
1198
|
}]
|
|
1196
1199
|
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
|
|
1200
|
+
/**
|
|
1201
|
+
* A SwiftUI `Group`, iOS only: its children as they are, with no layout of its own. What it is for
|
|
1202
|
+
* is `modifiers` that belong to several views at once, or to content that takes none itself, as a
|
|
1203
|
+
* sheet's presentation modifiers go on a group around a `ui-view-host`.
|
|
1204
|
+
*/
|
|
1205
|
+
export class UiGroup {
|
|
1206
|
+
nativeView = nativeView();
|
|
1207
|
+
modifiers = input(/* @ts-ignore */
|
|
1208
|
+
...(ngDevMode ? [undefined, { debugName: "modifiers" }] : /* istanbul ignore next */ []));
|
|
1209
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1210
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: UiGroup, isStandalone: true, selector: "ui-group", inputs: { modifiers: { classPropertyName: "modifiers", publicName: "modifiers", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "modifiers": "modifiers()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
|
|
1211
|
+
}
|
|
1212
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiGroup, decorators: [{
|
|
1213
|
+
type: Component,
|
|
1214
|
+
args: [{
|
|
1215
|
+
selector: 'ui-group',
|
|
1216
|
+
template: '<ng-content />',
|
|
1217
|
+
host: { '[modifiers]': 'modifiers()' },
|
|
1218
|
+
}]
|
|
1219
|
+
}], propDecorators: { modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
|
|
1220
|
+
/** The detents of a sheet given none and not sized to its content. */
|
|
1221
|
+
const HALF_AND_FULL = ['medium', 'large'];
|
|
1222
|
+
/** The window's width as the engine knows it, or undefined before the platform has said. */
|
|
1223
|
+
function windowWidth() {
|
|
1224
|
+
const node = inject(ElementRef).nativeElement;
|
|
1225
|
+
const engine = (node.host ?? inject(HostEngine));
|
|
1226
|
+
const read = () => engine.viewport?.width || undefined;
|
|
1227
|
+
const width = signal(read(), /* @ts-ignore */
|
|
1228
|
+
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
1229
|
+
const stop = engine.watchViewport?.(() => width.set(read()));
|
|
1230
|
+
if (stop)
|
|
1231
|
+
inject(DestroyRef).onDestroy(stop);
|
|
1232
|
+
return width;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* A sheet that slides up from the bottom of the screen, opened from any component: SwiftUI's on
|
|
1236
|
+
* iOS, Material's on Android.
|
|
1237
|
+
*
|
|
1238
|
+
* ```html
|
|
1239
|
+
* <ui-bottom-sheet [(open)]="sorting" fitToContents>
|
|
1240
|
+
* <app-sort-options (done)="sorting.set(false)" />
|
|
1241
|
+
* </ui-bottom-sheet>
|
|
1242
|
+
* ```
|
|
1243
|
+
*
|
|
1244
|
+
* `open` presents and dismisses it, and is written back when the user dismisses it. Its content is
|
|
1245
|
+
* what is written inside it, the app's own components, in the tree while the sheet is on screen.
|
|
1246
|
+
*
|
|
1247
|
+
* The two platforms drive a sheet differently, and this is where that difference ends. SwiftUI's
|
|
1248
|
+
* stays in the tree and is presented by `isPresented`, reporting `isPresentedChange` and
|
|
1249
|
+
* `dismiss`. Compose's shows for as long as it is in the tree, and reports `dismissRequest`.
|
|
1250
|
+
*
|
|
1251
|
+
* Unlike the other components here, the element this is mounted on is a plain view, which takes
|
|
1252
|
+
* no room and needs no `ui-host` around it. The sheet is inside it, wrapped as `@expo/ui`'s own
|
|
1253
|
+
* React component wraps one: a `ui-host`, the sheet, on iOS a `ui-group` that carries the
|
|
1254
|
+
* presentation modifiers, and a `ui-view-host` holding the one view the content goes in.
|
|
1255
|
+
*/
|
|
1256
|
+
export class UiBottomSheet {
|
|
1257
|
+
/**
|
|
1258
|
+
* The functions of Compose's sheet, none of them on iOS, or null where `@expo/ui` is not in the
|
|
1259
|
+
* build and there is no sheet. Overridden in a test to hide a sheet without a device.
|
|
1260
|
+
*/
|
|
1261
|
+
static SOURCE = new InjectionToken('angular-native.bottomSheetSource', {
|
|
1262
|
+
factory: () => optional(() => {
|
|
1263
|
+
const core = require('expo-modules-core');
|
|
1264
|
+
const module = core.requireOptionalNativeModule('ExpoUI');
|
|
1265
|
+
return module ? (module.ViewPrototypes?.['ExpoUI_ModalBottomSheetView'] ?? {}) : null;
|
|
1266
|
+
}),
|
|
1267
|
+
});
|
|
1268
|
+
functions = inject(UiBottomSheet.SOURCE);
|
|
1269
|
+
engine = inject(Engine, { optional: true });
|
|
1270
|
+
injector = inject(Injector);
|
|
1271
|
+
sheet = viewChild('sheet', { ...(ngDevMode ? { debugName: "sheet" } : /* istanbul ignore next */ {}), read: ElementRef });
|
|
1272
|
+
/** Whether the sheet is on screen. Written back as false when the user dismisses it. */
|
|
1273
|
+
open = model(false, /* @ts-ignore */
|
|
1274
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
1275
|
+
/**
|
|
1276
|
+
* The sheet is as tall as its content. Without it the sheet rests at half the screen's height
|
|
1277
|
+
* and drags up to all of it. Read as the sheet opens.
|
|
1278
|
+
*/
|
|
1279
|
+
fitToContents = input(false, { ...(ngDevMode ? { debugName: "fitToContents" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1280
|
+
/**
|
|
1281
|
+
* The heights the sheet rests at, iOS only: SwiftUI's `presentationDetents`. Compose's sheet has
|
|
1282
|
+
* a half and a full height and no others, so Android takes no notice of this. On iOS a sheet
|
|
1283
|
+
* given detents rests at them rather than at the height of its content, whatever `fitToContents`
|
|
1284
|
+
* says, as `@expo/ui`'s own sheets do. None, or an empty list, is the sheet without them.
|
|
1285
|
+
*/
|
|
1286
|
+
detents = input(/* @ts-ignore */
|
|
1287
|
+
...(ngDevMode ? [undefined, { debugName: "detents" }] : /* istanbul ignore next */ []));
|
|
1288
|
+
/** Whether the grabber shows at the top of the sheet. */
|
|
1289
|
+
showDragIndicator = input(true, { ...(ngDevMode ? { debugName: "showDragIndicator" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1290
|
+
/**
|
|
1291
|
+
* The sheet has finished closing, whether the user dismissed it or `open` was set false: the
|
|
1292
|
+
* moment another sheet or a dialog can be presented.
|
|
1293
|
+
*/
|
|
1294
|
+
dismissed = output();
|
|
1295
|
+
/** SwiftUI's sheet stays in the tree; anywhere else it is there while it shows. */
|
|
1296
|
+
swiftUI = nativePlatform() === 'ios';
|
|
1297
|
+
width = windowWidth();
|
|
1298
|
+
/**
|
|
1299
|
+
* Whether the content is in the tree: from when the sheet opens until it has gone, which is
|
|
1300
|
+
* after `open` turns false, so that it slides away with its content still in it.
|
|
1301
|
+
*/
|
|
1302
|
+
mounted = linkedSignal({ ...(ngDevMode ? { debugName: "mounted" } : /* istanbul ignore next */ {}), source: this.open,
|
|
1303
|
+
computation: (open, previous) => open || (previous?.value ?? false) });
|
|
1304
|
+
/**
|
|
1305
|
+
* Whether the sheet is sized to its content, as it opened: a view host takes its sizing once,
|
|
1306
|
+
* as it mounts. SwiftUI's sheet is not where it has detents to rest at.
|
|
1307
|
+
*/
|
|
1308
|
+
fits = computed(() => {
|
|
1309
|
+
this.mounted();
|
|
1310
|
+
return untracked(() => this.fitToContents() && !(this.swiftUI && this.detents()?.length));
|
|
1311
|
+
}, /* @ts-ignore */
|
|
1312
|
+
...(ngDevMode ? [{ debugName: "fits" }] : /* istanbul ignore next */ []));
|
|
1313
|
+
/**
|
|
1314
|
+
* SwiftUI reads how a sheet is presented from modifiers on its content, not from props. The
|
|
1315
|
+
* detents of a sheet not sized to its content are followed while it is open.
|
|
1316
|
+
*/
|
|
1317
|
+
presentation = computed(() => [
|
|
1318
|
+
this.fits()
|
|
1319
|
+
? { $type: 'presentationSizing', sizing: 'fitted' }
|
|
1320
|
+
: {
|
|
1321
|
+
$type: 'presentationDetents',
|
|
1322
|
+
detents: this.detents()?.length ? this.detents() : HALF_AND_FULL,
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
$type: 'presentationDragIndicator',
|
|
1326
|
+
visibility: this.showDragIndicator() ? 'visible' : 'hidden',
|
|
1327
|
+
},
|
|
1328
|
+
], /* @ts-ignore */
|
|
1329
|
+
...(ngDevMode ? [{ debugName: "presentation" }] : /* istanbul ignore next */ []));
|
|
1330
|
+
/**
|
|
1331
|
+
* The one view a view host holds. Sized to its content it needs a width of its own, the
|
|
1332
|
+
* window's; otherwise it fills the height the sheet gives it.
|
|
1333
|
+
*/
|
|
1334
|
+
body = computed(() => this.fits() ? { width: this.width() } : { flexGrow: 1, height: 0 }, /* @ts-ignore */
|
|
1335
|
+
...(ngDevMode ? [{ debugName: "body" }] : /* istanbul ignore next */ []));
|
|
1336
|
+
/** Whether `open` has turned false on a sheet that has yet to go. */
|
|
1337
|
+
closing = false;
|
|
1338
|
+
/** Whether Compose is sliding the sheet away. */
|
|
1339
|
+
hiding = false;
|
|
1340
|
+
constructor() {
|
|
1341
|
+
effect(() => {
|
|
1342
|
+
if (!this.open())
|
|
1343
|
+
untracked(() => this.close());
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
presented(event) {
|
|
1347
|
+
if (!this.own(event))
|
|
1348
|
+
return;
|
|
1349
|
+
const isPresented = event.nativeEvent?.isPresented;
|
|
1350
|
+
if (typeof isPresented === 'boolean')
|
|
1351
|
+
this.open.set(isPresented);
|
|
1352
|
+
}
|
|
1353
|
+
dismissRequested(event) {
|
|
1354
|
+
if (this.own(event))
|
|
1355
|
+
this.open.set(false);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* SwiftUI has finished dismissing the sheet. After a swipe it says so before it reports that the
|
|
1359
|
+
* sheet is no longer presented, so a sheet that has gone while `open`, and was not closing, is
|
|
1360
|
+
* one the user dismissed.
|
|
1361
|
+
*/
|
|
1362
|
+
gone(event) {
|
|
1363
|
+
if (!this.own(event))
|
|
1364
|
+
return;
|
|
1365
|
+
if (!this.closing)
|
|
1366
|
+
this.open.set(false);
|
|
1367
|
+
this.finish();
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Whether an event is this sheet's own, which it then stops. Events bubble, so a `dismiss` from
|
|
1371
|
+
* a modal in the content arrives here too, and the sheet's own would go on to a listener around
|
|
1372
|
+
* it.
|
|
1373
|
+
*/
|
|
1374
|
+
own(event) {
|
|
1375
|
+
const target = event.target;
|
|
1376
|
+
if (target != null && target !== this.sheet()?.nativeElement)
|
|
1377
|
+
return false;
|
|
1378
|
+
event.stopPropagation();
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* `open` turned false. SwiftUI dismisses its sheet on the prop, and says when it has gone.
|
|
1383
|
+
* Compose shows a sheet for as long as it is in the tree, so one taken out is gone at once:
|
|
1384
|
+
* its `hide` slides it away first, as `@expo/ui`'s own component calls it before unmounting.
|
|
1385
|
+
*/
|
|
1386
|
+
close() {
|
|
1387
|
+
if (!this.mounted())
|
|
1388
|
+
return;
|
|
1389
|
+
// No `@expo/ui` in the build: no sheet is there to slide away, or to say that it has.
|
|
1390
|
+
if (!this.functions)
|
|
1391
|
+
return this.finish();
|
|
1392
|
+
this.closing = true;
|
|
1393
|
+
if (this.swiftUI || this.hiding)
|
|
1394
|
+
return;
|
|
1395
|
+
const node = this.sheet()?.nativeElement;
|
|
1396
|
+
const nativeTag = node ? (this.engine?.tagOf(node) ?? null) : null;
|
|
1397
|
+
const hidden = nativeTag === null ? undefined : optional(() => this.functions?.hide?.call({ nativeTag }));
|
|
1398
|
+
if (!hidden)
|
|
1399
|
+
return this.finish();
|
|
1400
|
+
this.hiding = true;
|
|
1401
|
+
const done = () => {
|
|
1402
|
+
this.hiding = false;
|
|
1403
|
+
this.finish();
|
|
1404
|
+
};
|
|
1405
|
+
hidden.then(done, done);
|
|
1406
|
+
}
|
|
1407
|
+
/** The sheet has gone: its content leaves the tree. */
|
|
1408
|
+
finish() {
|
|
1409
|
+
this.closing = false;
|
|
1410
|
+
if (!this.mounted())
|
|
1411
|
+
return;
|
|
1412
|
+
if (!this.open()) {
|
|
1413
|
+
this.mounted.set(false);
|
|
1414
|
+
this.dismissed.emit();
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
// Opened again before it had gone. SwiftUI presents the sheet it still has; Compose hid the
|
|
1418
|
+
// one it was asked to, and shows a sheet only as it enters the tree.
|
|
1419
|
+
if (this.swiftUI)
|
|
1420
|
+
return;
|
|
1421
|
+
this.mounted.set(false);
|
|
1422
|
+
afterNextRender(() => this.mounted.set(this.open()), { injector: this.injector });
|
|
1423
|
+
}
|
|
1424
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiBottomSheet, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1425
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: UiBottomSheet, isStandalone: true, selector: "ui-bottom-sheet", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, fitToContents: { classPropertyName: "fitToContents", publicName: "fitToContents", isSignal: true, isRequired: false, transformFunction: null }, detents: { classPropertyName: "detents", publicName: "detents", isSignal: true, isRequired: false, transformFunction: null }, showDragIndicator: { classPropertyName: "showDragIndicator", publicName: "showDragIndicator", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", dismissed: "dismissed" }, host: { properties: { "style.position": "\"absolute\"" } }, viewQueries: [{ propertyName: "sheet", first: true, predicate: ["sheet"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: `
|
|
1426
|
+
<ng-template #content><ng-content /></ng-template>
|
|
1427
|
+
@if (swiftUI) {
|
|
1428
|
+
<ui-host style="position: absolute" pointerEvents="none" [style.width.px]="width()">
|
|
1429
|
+
<ui-bottom-sheet-view
|
|
1430
|
+
#sheet
|
|
1431
|
+
[isPresented]="open()"
|
|
1432
|
+
[fitToContents]="fits()"
|
|
1433
|
+
(isPresentedChange)="presented($event)"
|
|
1434
|
+
(dismiss)="gone($event)"
|
|
1435
|
+
>
|
|
1436
|
+
@if (mounted()) {
|
|
1437
|
+
<ui-group [modifiers]="presentation()">
|
|
1438
|
+
<ui-view-host layoutRoot [matchContents]="fits() || undefined">
|
|
1439
|
+
<ui-bottom-sheet-content [style]="body()" [content]="content" />
|
|
1440
|
+
</ui-view-host>
|
|
1441
|
+
</ui-group>
|
|
1442
|
+
}
|
|
1443
|
+
</ui-bottom-sheet-view>
|
|
1444
|
+
</ui-host>
|
|
1445
|
+
} @else if (mounted()) {
|
|
1446
|
+
<ui-host style="position: absolute" pointerEvents="none" [style.width.px]="width()">
|
|
1447
|
+
<ui-bottom-sheet-view
|
|
1448
|
+
#sheet
|
|
1449
|
+
[showDragHandle]="showDragIndicator()"
|
|
1450
|
+
[skipPartiallyExpanded]="fits()"
|
|
1451
|
+
(dismissRequest)="dismissRequested($event)"
|
|
1452
|
+
>
|
|
1453
|
+
<ui-view-host layoutRoot [matchContents]="fits() || undefined">
|
|
1454
|
+
<ui-bottom-sheet-content [style]="body()" [content]="content" />
|
|
1455
|
+
</ui-view-host>
|
|
1456
|
+
</ui-bottom-sheet-view>
|
|
1457
|
+
</ui-host>
|
|
1458
|
+
}
|
|
1459
|
+
`, isInline: true, dependencies: [{ kind: "component", type: UiBottomSheetContent, selector: "ui-bottom-sheet-content", inputs: ["content"] }, { kind: "component", type: UiBottomSheetView, selector: "ui-bottom-sheet-view", inputs: ["isPresented", "fitToContents", "showDragHandle", "skipPartiallyExpanded"], outputs: ["isPresentedChange", "dismiss", "dismissRequest"] }, { kind: "component", type: UiGroup, selector: "ui-group", inputs: ["modifiers"] }, { kind: "component", type: UiHost, selector: "ui-host", inputs: ["matchContents", "ignoreSafeArea", "useViewportSizeMeasurement"] }, { kind: "component", type: UiViewHost, selector: "ui-view-host", inputs: ["matchContents", "layoutRoot", "modifiers"] }] });
|
|
1460
|
+
}
|
|
1461
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiBottomSheet, decorators: [{
|
|
1462
|
+
type: Component,
|
|
1463
|
+
args: [{
|
|
1464
|
+
selector: 'ui-bottom-sheet',
|
|
1465
|
+
imports: [UiBottomSheetContent, UiBottomSheetView, UiGroup, UiHost, UiViewHost],
|
|
1466
|
+
template: `
|
|
1467
|
+
<ng-template #content><ng-content /></ng-template>
|
|
1468
|
+
@if (swiftUI) {
|
|
1469
|
+
<ui-host style="position: absolute" pointerEvents="none" [style.width.px]="width()">
|
|
1470
|
+
<ui-bottom-sheet-view
|
|
1471
|
+
#sheet
|
|
1472
|
+
[isPresented]="open()"
|
|
1473
|
+
[fitToContents]="fits()"
|
|
1474
|
+
(isPresentedChange)="presented($event)"
|
|
1475
|
+
(dismiss)="gone($event)"
|
|
1476
|
+
>
|
|
1477
|
+
@if (mounted()) {
|
|
1478
|
+
<ui-group [modifiers]="presentation()">
|
|
1479
|
+
<ui-view-host layoutRoot [matchContents]="fits() || undefined">
|
|
1480
|
+
<ui-bottom-sheet-content [style]="body()" [content]="content" />
|
|
1481
|
+
</ui-view-host>
|
|
1482
|
+
</ui-group>
|
|
1483
|
+
}
|
|
1484
|
+
</ui-bottom-sheet-view>
|
|
1485
|
+
</ui-host>
|
|
1486
|
+
} @else if (mounted()) {
|
|
1487
|
+
<ui-host style="position: absolute" pointerEvents="none" [style.width.px]="width()">
|
|
1488
|
+
<ui-bottom-sheet-view
|
|
1489
|
+
#sheet
|
|
1490
|
+
[showDragHandle]="showDragIndicator()"
|
|
1491
|
+
[skipPartiallyExpanded]="fits()"
|
|
1492
|
+
(dismissRequest)="dismissRequested($event)"
|
|
1493
|
+
>
|
|
1494
|
+
<ui-view-host layoutRoot [matchContents]="fits() || undefined">
|
|
1495
|
+
<ui-bottom-sheet-content [style]="body()" [content]="content" />
|
|
1496
|
+
</ui-view-host>
|
|
1497
|
+
</ui-bottom-sheet-view>
|
|
1498
|
+
</ui-host>
|
|
1499
|
+
}
|
|
1500
|
+
`,
|
|
1501
|
+
host: { '[style.position]': '"absolute"' },
|
|
1502
|
+
}]
|
|
1503
|
+
}], ctorParameters: () => [], propDecorators: { sheet: [{ type: i0.ViewChild, args: ['sheet', { ...{
|
|
1504
|
+
read: ElementRef,
|
|
1505
|
+
}, isSignal: true }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], fitToContents: [{ type: i0.Input, args: [{ isSignal: true, alias: "fitToContents", required: false }] }], detents: [{ type: i0.Input, args: [{ isSignal: true, alias: "detents", required: false }] }], showDragIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDragIndicator", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
|
|
1197
1506
|
//# sourceMappingURL=expo-ui-components.js.map
|