@ng-native/expo 0.18.1 → 0.19.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.
@@ -54,6 +54,17 @@ class KeyboardModule : Module() {
54
54
 
55
55
  Events("onKeyboardChange", "onKeyboardMove")
56
56
 
57
+ // How far below the top of the screen the app's window starts, in points: not zero in the
58
+ // lower half of a split screen or in a freeform window. React Native measures a view in the
59
+ // window and reports the keyboard's top on the screen. Read where it is compared, since the
60
+ // window can move.
61
+ Function("windowTopOnScreen") {
62
+ val decor = appContext.currentActivity?.window?.decorView ?: return@Function 0.0
63
+ val location = IntArray(2)
64
+ decor.getLocationOnScreen(location)
65
+ location[1] / decor.resources.displayMetrics.density.toDouble()
66
+ }
67
+
57
68
  OnStartObserving {
58
69
  observing = true
59
70
  watchActivity()
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `Calendar`, bound to `expo-calendar`: the calendars on the device, and the events and reminders
3
+ * in them.
4
+ *
5
+ * ```ts
6
+ * import { Calendar } from '@ng-native/expo/calendar';
7
+ *
8
+ * private readonly calendar = inject(Calendar);
9
+ *
10
+ * async thisWeek(): Promise<ExpoCalendarEvent[]> {
11
+ * if (!(await this.calendar.permission.ensure())) return [];
12
+ * const calendars = await this.calendar.calendars();
13
+ * const start = new Date();
14
+ * const end = new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);
15
+ * return this.calendar.events(calendars, start, end);
16
+ * }
17
+ *
18
+ * async book(calendar: ExpoCalendar): Promise<void> {
19
+ * await calendar.createEvent({ title: 'Padel', startDate, endDate, alarms: [{ relativeOffset: -30 }] });
20
+ * }
21
+ * ```
22
+ *
23
+ * This is the module's object API: a calendar, an event, a reminder and an attendee are handles
24
+ * with their own `update()` and `delete()`, and a calendar makes its own events and reminders. The
25
+ * service is where the handles are reached from, so a test can provide them, and where the
26
+ * permissions are: full access to the calendars, the write-only access iOS 17 added for an app
27
+ * that only adds events, and on iOS the reminders.
28
+ *
29
+ * What exists on iOS alone - the default calendar, the calendar picker, the sources and the
30
+ * reminders - answers null, nothing or refused on Android rather than throwing, so code shared by
31
+ * both runs on both. Without the module installed, every list answers empty, every lookup null,
32
+ * and the permissions are refused.
33
+ */
34
+ import { InjectionToken } from '@angular/core';
35
+ import type { Availability as ExpoAvailability, EntityTypes as ExpoEntityTypes, ExpoCalendar, ExpoCalendarEvent, ExpoCalendarReminder, Frequency as ExpoFrequency, Source } from 'expo-calendar';
36
+ import { type ModulePlatform } from './native.ts';
37
+ import { Permission } from './permissions.ts';
38
+ import * as i0 from "@angular/core";
39
+ type Expo = typeof import('expo-calendar');
40
+ /** The module's functions and classes this service calls. A test provides fakes of each. */
41
+ export type NativeCalendar = Pick<Expo, 'getCalendarPermissions' | 'requestCalendarPermissions' | 'getRemindersPermissions' | 'requestRemindersPermissions' | 'getCalendars' | 'getDefaultCalendarSync' | 'createCalendar' | 'presentPicker' | 'listEvents' | 'getSourcesSync' | 'ExpoCalendar' | 'ExpoCalendarEvent' | 'ExpoCalendarReminder'>;
42
+ type Args<K extends keyof NativeCalendar> = NativeCalendar[K] extends (...args: infer A) => unknown ? A : never;
43
+ /**
44
+ * What a calendar holds, as `expo-calendar`'s own enum. Importing the enum from the module loads
45
+ * the module, which a test in Node cannot; these are the same strings, typed as the enum.
46
+ */
47
+ export declare const EntityTypes: typeof ExpoEntityTypes;
48
+ /** How often a recurrence rule repeats, as the module's own enum. */
49
+ export declare const Frequency: typeof ExpoFrequency;
50
+ /** Whether an event shows its time as taken, as the module's own enum. */
51
+ export declare const Availability: typeof ExpoAvailability;
52
+ export declare class Calendar {
53
+ /** Overridden in a test to read calendars that are not there. */
54
+ static readonly SOURCE: InjectionToken<NativeCalendar | null>;
55
+ /** Overridden in a test to be on iOS, where the calls only iOS has can be made. */
56
+ static readonly PLATFORM: InjectionToken<ModulePlatform | null>;
57
+ private readonly native;
58
+ private readonly platform;
59
+ /** Full access: reading the calendars and their events, as well as adding to them. */
60
+ readonly permission: Permission;
61
+ /**
62
+ * Write-only access, on iOS 17 and later: enough to add events, and nothing to read. Elsewhere
63
+ * the same as {@link permission}.
64
+ */
65
+ readonly writePermission: Permission;
66
+ /** The reminders, on iOS. Refused on Android, which keeps none in its calendars. */
67
+ readonly remindersPermission: Permission;
68
+ /**
69
+ * The calendars on the device, of events unless {@link EntityTypes.REMINDER} is asked for. The
70
+ * module itself lists both kinds when given none, and on iOS refuses that without reminders access.
71
+ */
72
+ calendars(entityType?: ExpoEntityTypes): Promise<ExpoCalendar[]>;
73
+ /** The calendar with this id, or null. */
74
+ calendar(id: string): Promise<ExpoCalendar | null>;
75
+ /** The calendar new events go in unless another is chosen, on iOS. Null on Android. */
76
+ defaultCalendar(): ExpoCalendar | null;
77
+ /** A new calendar, with the module's own details: `title`, `color`, `entityType` and the rest. */
78
+ createCalendar(...details: Args<'createCalendar'>): Promise<ExpoCalendar | null>;
79
+ /** On iOS, the system's calendar picker. Null if the user cancels it, and on Android. */
80
+ pickCalendar(): Promise<ExpoCalendar | null>;
81
+ /**
82
+ * The events from `start` to `end` in the calendars given, as handles or their ids, each
83
+ * occurrence of a recurring event as an event of its own. An event that overlaps the range at
84
+ * all is in it, one running across either end included.
85
+ */
86
+ events(calendars: readonly (string | ExpoCalendar)[], start: Date, end: Date): Promise<ExpoCalendarEvent[]>;
87
+ /** The event with this id, such as one kept from an earlier list, or null. */
88
+ event(id: string): Promise<ExpoCalendarEvent | null>;
89
+ /** The reminder with this id, on iOS, or null. */
90
+ reminder(id: string): Promise<ExpoCalendarReminder | null>;
91
+ /** The accounts the calendars belong to, on iOS: iCloud, Exchange, a subscription. */
92
+ sources(): Source[];
93
+ private calendarPermission;
94
+ /** The module is here and the app is on iOS, where the calls it has only there exist. */
95
+ private ios;
96
+ static ɵfac: i0.ɵɵFactoryDeclaration<Calendar, never>;
97
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
98
+ }
99
+ export {};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `Calendar`, bound to `expo-calendar`: the calendars on the device, and the events and reminders
3
+ * in them.
4
+ *
5
+ * ```ts
6
+ * import { Calendar } from '@ng-native/expo/calendar';
7
+ *
8
+ * private readonly calendar = inject(Calendar);
9
+ *
10
+ * async thisWeek(): Promise<ExpoCalendarEvent[]> {
11
+ * if (!(await this.calendar.permission.ensure())) return [];
12
+ * const calendars = await this.calendar.calendars();
13
+ * const start = new Date();
14
+ * const end = new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);
15
+ * return this.calendar.events(calendars, start, end);
16
+ * }
17
+ *
18
+ * async book(calendar: ExpoCalendar): Promise<void> {
19
+ * await calendar.createEvent({ title: 'Padel', startDate, endDate, alarms: [{ relativeOffset: -30 }] });
20
+ * }
21
+ * ```
22
+ *
23
+ * This is the module's object API: a calendar, an event, a reminder and an attendee are handles
24
+ * with their own `update()` and `delete()`, and a calendar makes its own events and reminders. The
25
+ * service is where the handles are reached from, so a test can provide them, and where the
26
+ * permissions are: full access to the calendars, the write-only access iOS 17 added for an app
27
+ * that only adds events, and on iOS the reminders.
28
+ *
29
+ * What exists on iOS alone - the default calendar, the calendar picker, the sources and the
30
+ * reminders - answers null, nothing or refused on Android rather than throwing, so code shared by
31
+ * both runs on both. Without the module installed, every list answers empty, every lookup null,
32
+ * and the permissions are refused.
33
+ */
34
+ import { InjectionToken, Service, inject } from '@angular/core';
35
+ import { currentPlatform, expoModule } from "./native.js";
36
+ import { Permission, UNAVAILABLE } from "./permissions.js";
37
+ import * as i0 from "@angular/core";
38
+ /**
39
+ * What a calendar holds, as `expo-calendar`'s own enum. Importing the enum from the module loads
40
+ * the module, which a test in Node cannot; these are the same strings, typed as the enum.
41
+ */
42
+ export const EntityTypes = {
43
+ EVENT: 'event',
44
+ REMINDER: 'reminder',
45
+ };
46
+ /** How often a recurrence rule repeats, as the module's own enum. */
47
+ export const Frequency = {
48
+ DAILY: 'daily',
49
+ WEEKLY: 'weekly',
50
+ MONTHLY: 'monthly',
51
+ YEARLY: 'yearly',
52
+ };
53
+ /** Whether an event shows its time as taken, as the module's own enum. */
54
+ export const Availability = {
55
+ NOT_SUPPORTED: 'notSupported',
56
+ BUSY: 'busy',
57
+ FREE: 'free',
58
+ TENTATIVE: 'tentative',
59
+ UNAVAILABLE: 'unavailable',
60
+ };
61
+ export class Calendar {
62
+ /** Overridden in a test to read calendars that are not there. */
63
+ static SOURCE = new InjectionToken('angular-native.calendarSource', { factory: () => expoModule('expo-calendar', () => require('expo-calendar')) });
64
+ /** Overridden in a test to be on iOS, where the calls only iOS has can be made. */
65
+ static PLATFORM = new InjectionToken('angular-native.calendarPlatform', { factory: currentPlatform });
66
+ native = inject(Calendar.SOURCE);
67
+ platform = inject(Calendar.PLATFORM);
68
+ /** Full access: reading the calendars and their events, as well as adding to them. */
69
+ permission = this.calendarPermission(false);
70
+ /**
71
+ * Write-only access, on iOS 17 and later: enough to add events, and nothing to read. Elsewhere
72
+ * the same as {@link permission}.
73
+ */
74
+ writePermission = this.calendarPermission(true);
75
+ /** The reminders, on iOS. Refused on Android, which keeps none in its calendars. */
76
+ remindersPermission = Permission.of(() => (this.ios() ? this.native.getRemindersPermissions() : Promise.resolve(UNAVAILABLE)), () => (this.ios() ? this.native.requestRemindersPermissions() : Promise.resolve(UNAVAILABLE)));
77
+ /**
78
+ * The calendars on the device, of events unless {@link EntityTypes.REMINDER} is asked for. The
79
+ * module itself lists both kinds when given none, and on iOS refuses that without reminders access.
80
+ */
81
+ async calendars(entityType = EntityTypes.EVENT) {
82
+ return (await this.native?.getCalendars(entityType)) ?? [];
83
+ }
84
+ /** The calendar with this id, or null. */
85
+ async calendar(id) {
86
+ return (await this.native?.ExpoCalendar.get(id)) ?? null;
87
+ }
88
+ /** The calendar new events go in unless another is chosen, on iOS. Null on Android. */
89
+ defaultCalendar() {
90
+ return this.ios() ? this.native.getDefaultCalendarSync() : null;
91
+ }
92
+ /** A new calendar, with the module's own details: `title`, `color`, `entityType` and the rest. */
93
+ async createCalendar(...details) {
94
+ return (await this.native?.createCalendar(...details)) ?? null;
95
+ }
96
+ /** On iOS, the system's calendar picker. Null if the user cancels it, and on Android. */
97
+ async pickCalendar() {
98
+ return this.ios() ? this.native.presentPicker() : null;
99
+ }
100
+ /**
101
+ * The events from `start` to `end` in the calendars given, as handles or their ids, each
102
+ * occurrence of a recurring event as an event of its own. An event that overlaps the range at
103
+ * all is in it, one running across either end included.
104
+ */
105
+ async events(calendars, start, end) {
106
+ if (!this.native || calendars.length === 0)
107
+ return [];
108
+ return this.native.listEvents([...calendars], start, end);
109
+ }
110
+ /** The event with this id, such as one kept from an earlier list, or null. */
111
+ async event(id) {
112
+ return (await this.native?.ExpoCalendarEvent.get(id)) ?? null;
113
+ }
114
+ /** The reminder with this id, on iOS, or null. */
115
+ async reminder(id) {
116
+ return this.ios() ? this.native.ExpoCalendarReminder.get(id) : null;
117
+ }
118
+ /** The accounts the calendars belong to, on iOS: iCloud, Exchange, a subscription. */
119
+ sources() {
120
+ return this.ios() ? this.native.getSourcesSync() : [];
121
+ }
122
+ calendarPermission(writeOnly) {
123
+ return Permission.of(() => this.native?.getCalendarPermissions(writeOnly) ?? Promise.resolve(UNAVAILABLE), () => this.native?.requestCalendarPermissions(writeOnly) ?? Promise.resolve(UNAVAILABLE));
124
+ }
125
+ /** The module is here and the app is on iOS, where the calls it has only there exist. */
126
+ ios() {
127
+ return this.native !== null && this.platform === 'ios';
128
+ }
129
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Calendar, deps: [], target: i0.ɵɵFactoryTarget.Service });
130
+ static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.2.0", ngImport: i0, type: Calendar });
131
+ }
132
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Calendar, decorators: [{
133
+ type: Service
134
+ }] });
135
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../src/calendar.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Calendar`, bound to `expo-calendar`: the calendars on the device, and the events and reminders\n * in them.\n *\n * ```ts\n * import { Calendar } from '@ng-native/expo/calendar';\n *\n * private readonly calendar = inject(Calendar);\n *\n * async thisWeek(): Promise<ExpoCalendarEvent[]> {\n * if (!(await this.calendar.permission.ensure())) return [];\n * const calendars = await this.calendar.calendars();\n * const start = new Date();\n * const end = new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);\n * return this.calendar.events(calendars, start, end);\n * }\n *\n * async book(calendar: ExpoCalendar): Promise<void> {\n * await calendar.createEvent({ title: 'Padel', startDate, endDate, alarms: [{ relativeOffset: -30 }] });\n * }\n * ```\n *\n * This is the module's object API: a calendar, an event, a reminder and an attendee are handles\n * with their own `update()` and `delete()`, and a calendar makes its own events and reminders. The\n * service is where the handles are reached from, so a test can provide them, and where the\n * permissions are: full access to the calendars, the write-only access iOS 17 added for an app\n * that only adds events, and on iOS the reminders.\n *\n * What exists on iOS alone - the default calendar, the calendar picker, the sources and the\n * reminders - answers null, nothing or refused on Android rather than throwing, so code shared by\n * both runs on both. Without the module installed, every list answers empty, every lookup null,\n * and the permissions are refused.\n */\nimport { InjectionToken, Service, inject } from '@angular/core';\nimport type {\n Availability as ExpoAvailability,\n EntityTypes as ExpoEntityTypes,\n ExpoCalendar,\n ExpoCalendarEvent,\n ExpoCalendarReminder,\n Frequency as ExpoFrequency,\n Source,\n} from 'expo-calendar';\nimport { currentPlatform, expoModule, type ModulePlatform } from './native.ts';\nimport { Permission, UNAVAILABLE } from './permissions.ts';\n\ntype Expo = typeof import('expo-calendar');\n\n/** The module's functions and classes this service calls. A test provides fakes of each. */\nexport type NativeCalendar = Pick<\n Expo,\n | 'getCalendarPermissions'\n | 'requestCalendarPermissions'\n | 'getRemindersPermissions'\n | 'requestRemindersPermissions'\n | 'getCalendars'\n | 'getDefaultCalendarSync'\n | 'createCalendar'\n | 'presentPicker'\n | 'listEvents'\n | 'getSourcesSync'\n | 'ExpoCalendar'\n | 'ExpoCalendarEvent'\n | 'ExpoCalendarReminder'\n>;\n\ntype Args<K extends keyof NativeCalendar> = NativeCalendar[K] extends (...args: infer A) => unknown\n ? A\n : never;\n\n/**\n * What a calendar holds, as `expo-calendar`'s own enum. Importing the enum from the module loads\n * the module, which a test in Node cannot; these are the same strings, typed as the enum.\n */\nexport const EntityTypes = {\n EVENT: 'event',\n REMINDER: 'reminder',\n} as unknown as typeof ExpoEntityTypes;\n\n/** How often a recurrence rule repeats, as the module's own enum. */\nexport const Frequency = {\n DAILY: 'daily',\n WEEKLY: 'weekly',\n MONTHLY: 'monthly',\n YEARLY: 'yearly',\n} as unknown as typeof ExpoFrequency;\n\n/** Whether an event shows its time as taken, as the module's own enum. */\nexport const Availability = {\n NOT_SUPPORTED: 'notSupported',\n BUSY: 'busy',\n FREE: 'free',\n TENTATIVE: 'tentative',\n UNAVAILABLE: 'unavailable',\n} as unknown as typeof ExpoAvailability;\n\n@Service()\nexport class Calendar {\n /** Overridden in a test to read calendars that are not there. */\n static readonly SOURCE = new InjectionToken<NativeCalendar | null>(\n 'angular-native.calendarSource',\n { factory: () => expoModule('expo-calendar', () => require('expo-calendar') as Expo) },\n );\n\n /** Overridden in a test to be on iOS, where the calls only iOS has can be made. */\n static readonly PLATFORM = new InjectionToken<ModulePlatform | null>(\n 'angular-native.calendarPlatform',\n { factory: currentPlatform },\n );\n\n private readonly native = inject(Calendar.SOURCE);\n private readonly platform = inject(Calendar.PLATFORM);\n\n /** Full access: reading the calendars and their events, as well as adding to them. */\n readonly permission: Permission = this.calendarPermission(false);\n\n /**\n * Write-only access, on iOS 17 and later: enough to add events, and nothing to read. Elsewhere\n * the same as {@link permission}.\n */\n readonly writePermission: Permission = this.calendarPermission(true);\n\n /** The reminders, on iOS. Refused on Android, which keeps none in its calendars. */\n readonly remindersPermission: Permission = Permission.of(\n () => (this.ios() ? this.native!.getRemindersPermissions() : Promise.resolve(UNAVAILABLE)),\n () => (this.ios() ? this.native!.requestRemindersPermissions() : Promise.resolve(UNAVAILABLE)),\n );\n\n /**\n * The calendars on the device, of events unless {@link EntityTypes.REMINDER} is asked for. The\n * module itself lists both kinds when given none, and on iOS refuses that without reminders access.\n */\n async calendars(entityType: ExpoEntityTypes = EntityTypes.EVENT): Promise<ExpoCalendar[]> {\n return (await this.native?.getCalendars(entityType)) ?? [];\n }\n\n /** The calendar with this id, or null. */\n async calendar(id: string): Promise<ExpoCalendar | null> {\n return (await this.native?.ExpoCalendar.get(id)) ?? null;\n }\n\n /** The calendar new events go in unless another is chosen, on iOS. Null on Android. */\n defaultCalendar(): ExpoCalendar | null {\n return this.ios() ? this.native!.getDefaultCalendarSync() : null;\n }\n\n /** A new calendar, with the module's own details: `title`, `color`, `entityType` and the rest. */\n async createCalendar(...details: Args<'createCalendar'>): Promise<ExpoCalendar | null> {\n return (await this.native?.createCalendar(...details)) ?? null;\n }\n\n /** On iOS, the system's calendar picker. Null if the user cancels it, and on Android. */\n async pickCalendar(): Promise<ExpoCalendar | null> {\n return this.ios() ? this.native!.presentPicker() : null;\n }\n\n /**\n * The events from `start` to `end` in the calendars given, as handles or their ids, each\n * occurrence of a recurring event as an event of its own. An event that overlaps the range at\n * all is in it, one running across either end included.\n */\n async events(\n calendars: readonly (string | ExpoCalendar)[],\n start: Date,\n end: Date,\n ): Promise<ExpoCalendarEvent[]> {\n if (!this.native || calendars.length === 0) return [];\n return this.native.listEvents([...calendars], start, end);\n }\n\n /** The event with this id, such as one kept from an earlier list, or null. */\n async event(id: string): Promise<ExpoCalendarEvent | null> {\n return (await this.native?.ExpoCalendarEvent.get(id)) ?? null;\n }\n\n /** The reminder with this id, on iOS, or null. */\n async reminder(id: string): Promise<ExpoCalendarReminder | null> {\n return this.ios() ? this.native!.ExpoCalendarReminder.get(id) : null;\n }\n\n /** The accounts the calendars belong to, on iOS: iCloud, Exchange, a subscription. */\n sources(): Source[] {\n return this.ios() ? this.native!.getSourcesSync() : [];\n }\n\n private calendarPermission(writeOnly: boolean): Permission {\n return Permission.of(\n () => this.native?.getCalendarPermissions(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestCalendarPermissions(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n );\n }\n\n /** The module is here and the app is on iOS, where the calls it has only there exist. */\n private ios(): boolean {\n return this.native !== null && this.platform === 'ios';\n }\n}\n"]}
@@ -31,9 +31,9 @@
31
31
  * has no output for is an error naming the ones it does have, as `<ng-native-island>`'s is.
32
32
  * - **Errors** the page throws reach the app's `ErrorHandler`.
33
33
  * - **Only its own page is heard.** A message from any other page the web view shows, after a link
34
- * in the content, say, is ignored, as is one that is not JSON. The inputs given before the page
35
- * loads are the web view's to hand out, though, and it hands them to whatever page it is showing,
36
- * so they are no place for a secret.
34
+ * in the content, say, is ignored, as is one that is not JSON, and a changed input is sent to
35
+ * its own page alone. The inputs given before the page loads are the web view's to hand out,
36
+ * though, and it hands them to whatever page it is showing, so they are no place for a secret.
37
37
  */
38
38
  import { InjectionToken } from '@angular/core';
39
39
  import { type NativeSyntheticEvent } from '@ng-native/fabric';
@@ -31,9 +31,9 @@
31
31
  * has no output for is an error naming the ones it does have, as `<ng-native-island>`'s is.
32
32
  * - **Errors** the page throws reach the app's `ErrorHandler`.
33
33
  * - **Only its own page is heard.** A message from any other page the web view shows, after a link
34
- * in the content, say, is ignored, as is one that is not JSON. The inputs given before the page
35
- * loads are the web view's to hand out, though, and it hands them to whatever page it is showing,
36
- * so they are no place for a secret.
34
+ * in the content, say, is ignored, as is one that is not JSON, and a changed input is sent to
35
+ * its own page alone. The inputs given before the page loads are the web view's to hand out,
36
+ * though, and it hands them to whatever page it is showing, so they are no place for a secret.
37
37
  */
38
38
  import { Component, ElementRef, InjectionToken, computed, effect, inject, input, untracked, } from '@angular/core';
39
39
  import { Engine } from '@ng-native/fabric';
@@ -136,10 +136,11 @@ export class DomComponent {
136
136
  }
137
137
  send(inputs) {
138
138
  const nativeTag = this.engine.tagOf(this.node);
139
- if (!this.native.functions || nativeTag === null)
139
+ const source = this.source();
140
+ if (!this.native.functions || nativeTag === null || !source)
140
141
  return;
141
142
  const message = JSON.stringify({ type: 'inputs', inputs });
142
- void this.native.functions.injectJavaScript.call({ nativeTag }, `window.__ngNative && window.__ngNative.receive(${message}); true;`);
143
+ void this.native.functions.injectJavaScript.call({ nativeTag }, inOwnPage(source.uri, `window.__ngNative && window.__ngNative.receive(${message});`));
143
144
  }
144
145
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: DomComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
145
146
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: DomComponent, isStandalone: true, selector: "dom-component", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, inputs: { classPropertyName: "inputs", publicName: "inputs", isSignal: true, isRequired: false, transformFunction: null }, outputs: { classPropertyName: "outputs", publicName: "outputs", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "message": "receive($event)" }, properties: { "source": "source()", "injectedJavaScriptObject": "page()", "webviewDebuggingEnabled": "debuggable" } }, ngImport: i0, template: '', isInline: true });
@@ -174,11 +175,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
174
175
  function isOwnPage(url, source) {
175
176
  if (!url)
176
177
  return false;
177
- const served = /^https?:\/\/[^/?#]+/i.exec(source)?.[0];
178
+ const served = servedFrom(source);
178
179
  if (served)
179
- return url.toLowerCase().startsWith(`${served.toLowerCase()}/`);
180
+ return url.toLowerCase().startsWith(`${served}/`);
180
181
  return /^file:/i.test(url) && fileName(url) === fileName(source);
181
182
  }
183
+ /**
184
+ * A script that runs `call` only where the web view shows the component's own page, by
185
+ * `isOwnPage`'s rule, as the page itself reads its address. The web view runs what it is given
186
+ * in whatever page it is showing, and one a link took it to may define `__ngNative` to be handed
187
+ * the app's inputs, and may replace any method of a string or a pattern to pass a check made with
188
+ * one. So the check reads only what such a page cannot change: `location`'s own properties, a
189
+ * string's length and characters, and `===`.
190
+ */
191
+ function inOwnPage(source, call) {
192
+ const served = servedFrom(source);
193
+ const own = served
194
+ ? `location.origin === ${JSON.stringify(served)}`
195
+ : `location.protocol === 'file:' && (function (path, end) {
196
+ var from = path.length - end.length;
197
+ for (var at = 0; at < end.length; at++) if (path[from + at] !== end[at]) return false;
198
+ return true;
199
+ })(location.pathname, ${JSON.stringify(`/${fileName(source)}`)})`;
200
+ return `if (${own}) { ${call} } true;`;
201
+ }
202
+ /** The origin of the dev server that serves the page, lowercased, as `location.origin` has it. */
203
+ function servedFrom(source) {
204
+ return /^https?:\/\/[^/?#]+/i.exec(source)?.[0]?.toLowerCase();
205
+ }
182
206
  function fileName(url) {
183
207
  return url.split(/[?#]/)[0].split('/').at(-1);
184
208
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dom-component.js","sourceRoot":"","sources":["../src/dom-component.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `<dom-component>`: an Angular component rendered by a browser - DOM, CSS, anything a browser\n * runs - in a web view inside a native screen, with its inputs and outputs bound from the app's\n * template. Expo calls this a DOM component; it is not the Web Components standard.\n *\n * ```ts\n * // web/note.ts: an ordinary Angular component for the browser, in the app's own source\n * 'use dom';\n * @Component({ selector: 'app-note', template: '<textarea ...></textarea>', styles: [...] })\n * export class Note {\n * readonly label = input('');\n * readonly sent = output<string>();\n * }\n * export default mountInWebView(Note); // from '@ng-native/web/web-view'\n *\n * // a native screen\n * import note from './web/note.ts';\n * <dom-component [src]=\"note\" class=\"flex-1\"\n * [inputs]=\"{ label: title() }\" [outputs]=\"{ sent: save }\" />\n * ```\n *\n * The component runs in Expo's own web view (`@expo/dom-webview`, which `expo` already installs),\n * so it is a separate JavaScript runtime from the app and only JSON crosses. It is shipped the way\n * Expo ships its `'use dom'` components: the import is a reference to a page that Expo's dev server\n * serves in development and `expo export:embed` bundles into the app for a release build (see\n * `@ng-native/metro`'s `dom-component.cjs`), and the page is found where Expo finds its own.\n *\n * - **`inputs`** go into the page before it loads, and a changed value is sent to it afterwards\n * without reloading. Anything sent before the page is ready is sent again once it is.\n * - **`outputs`** map the component's outputs, by public name, to handlers. A name the component\n * has no output for is an error naming the ones it does have, as `<ng-native-island>`'s is.\n * - **Errors** the page throws reach the app's `ErrorHandler`.\n * - **Only its own page is heard.** A message from any other page the web view shows, after a link\n * in the content, say, is ignored, as is one that is not JSON. The inputs given before the page\n * loads are the web view's to hand out, though, and it hands them to whatever page it is showing,\n * so they are no place for a secret.\n */\nimport {\n Component,\n ElementRef,\n InjectionToken,\n computed,\n effect,\n inject,\n input,\n untracked,\n} from '@angular/core';\nimport { Engine, type EngineNode, type NativeSyntheticEvent } from '@ng-native/fabric';\nimport { optional } from './native.ts';\nimport { registerExpoView } from './register-expo-view.ts';\n\n/** The element is this package's, so it needs no `registerExpoViews` call of its own. */\nregisterExpoView('dom-component', 'ExpoDomWebViewModule');\n\n/** A handler for one of the DOM component's outputs. */\nexport type DomComponentOutputHandler = (value: never) => void;\n\n/** The functions Expo's web view defines on its view, called with the view's tag as `this`. */\nexport interface WebViewFunctions {\n injectJavaScript(this: { nativeTag: number }, script: string): Promise<void>;\n}\n\n/** Where the pages are, and how to reach the view once it is on screen. */\nexport interface DomComponentSource {\n /** Where Expo keeps DOM components' pages: the dev server, `www.bundle`, or an update. */\n readonly baseUrl: string | null;\n readonly functions: WebViewFunctions | null;\n}\n\n/** What native code gets for `import note from './web/note.ts'`. */\nexport interface DomComponentReference {\n readonly domComponent: string;\n}\n\ntype PageMessage =\n | { type: 'ready'; outputs: string[] }\n | { type: 'output'; name: string; value: unknown }\n | { type: 'error'; message: string };\n\n@Component({\n selector: 'dom-component',\n template: '',\n host: {\n '[source]': 'source()',\n '[injectedJavaScriptObject]': 'page()',\n '[webviewDebuggingEnabled]': 'debuggable',\n '(message)': 'receive($event)',\n },\n})\nexport class DomComponent {\n /** Overridden in a test to load pages without a dev server. */\n static readonly SOURCE = new InjectionToken<DomComponentSource>(\n 'angular-native.domComponentSource',\n {\n factory: () => {\n // Expo's own resolution, so a page is found wherever Expo would look for a DOM\n // component's: the dev server, the app's `www.bundle`, or a downloaded update.\n const base = optional(() => require('expo/src/dom/base') as { getBaseURL(): string });\n const core = optional(\n () => require('expo-modules-core') as typeof import('expo-modules-core'),\n );\n const module = core?.requireOptionalNativeModule<{\n ViewPrototypes?: Record<string, WebViewFunctions>;\n }>('ExpoDomWebViewModule');\n return {\n baseUrl: optional(() => base?.getBaseURL() ?? null),\n functions: module?.ViewPrototypes?.['ExpoDomWebViewModule'] ?? null,\n };\n },\n },\n );\n\n private readonly native = inject(DomComponent.SOURCE);\n private readonly engine = inject(Engine);\n private readonly node = inject<ElementRef<EngineNode>>(ElementRef).nativeElement;\n private ready = false;\n\n /** The DOM component, as imported: `import note from './web/note.ts'`. */\n readonly src = input.required<DomComponentReference>();\n /** Its inputs, by public name. JSON only: this crosses into another runtime. */\n readonly inputs = input<Readonly<Record<string, unknown>>>({});\n /** Handlers for its outputs, by public name. */\n readonly outputs = input<Readonly<Record<string, DomComponentOutputHandler>>>({});\n\n protected readonly debuggable = typeof __DEV__ === 'undefined' || __DEV__;\n\n protected readonly source = computed(() => {\n const { domComponent } = this.src();\n return this.native.baseUrl === null ? null : { uri: `${this.native.baseUrl}/${domComponent}` };\n });\n\n /** The file's name for an error message: the dev page names it, a release page is a hash. */\n private readonly name = computed(() => this.src().domComponent.split('?')[0]!);\n\n /**\n * What the page reads before it loads. The inputs are read untracked: a change to this prop\n * reloads the page, so a changed input is sent to the running page instead.\n */\n protected readonly page = computed(() => {\n this.src();\n return JSON.stringify({ inputs: untracked(this.inputs) });\n });\n\n constructor() {\n effect(() => {\n const inputs = this.inputs();\n if (this.ready) untracked(() => this.send(inputs));\n });\n }\n\n protected receive(event: NativeSyntheticEvent<{ data: string; url?: string }>): void {\n const { data, url } = event.nativeEvent;\n const source = this.source();\n if (!source || !isOwnPage(url, source.uri)) {\n if (this.debuggable) {\n console.warn(\n `[angular-native] <dom-component> ${this.name()}: ignored a message from ` +\n `${url || 'an unnamed page'}, which is not its own page.`,\n );\n }\n return;\n }\n const message = parse(data);\n if (!message) {\n if (this.debuggable) {\n console.warn(\n `[angular-native] <dom-component> ${this.name()}: dropped a message that is not JSON.`,\n );\n }\n return;\n }\n if (message.type === 'ready') {\n this.checkOutputs(message.outputs);\n this.ready = true;\n this.send(this.inputs());\n } else if (message.type === 'output') {\n const outputs = this.outputs();\n if (Object.hasOwn(outputs, message.name)) outputs[message.name]!(message.value as never);\n } else if (message.type === 'error') {\n throw new Error(`[angular-native] <dom-component> ${this.name()}: ${message.message}`);\n }\n }\n\n private checkOutputs(declared: readonly string[]): void {\n for (const name of Object.keys(this.outputs())) {\n if (!declared.includes(name)) {\n throw new Error(\n `[angular-native] <dom-component>: ${this.name()} has no output '${name}' ` +\n `(its outputs: ${declared.join(', ') || 'none'}).`,\n );\n }\n }\n }\n\n private send(inputs: Readonly<Record<string, unknown>>): void {\n const nativeTag = this.engine.tagOf(this.node);\n if (!this.native.functions || nativeTag === null) return;\n const message = JSON.stringify({ type: 'inputs', inputs });\n void this.native.functions.injectJavaScript.call(\n { nativeTag },\n `window.__ngNative && window.__ngNative.receive(${message}); true;`,\n );\n }\n}\n\n/**\n * Whether the page a message came from is the one the component loaded, so a page the web view\n * was taken to (by a link in the content, say) cannot fire the app's outputs or ask for its inputs.\n *\n * The web view names the page it is showing, not the frame that posted, so this tells pages apart\n * and not an iframe from the page that holds it. Every message is named on both platforms, from\n * the web view's own URL; one with no URL cannot be placed, so it is not trusted either.\n *\n * - **Served by the dev server**, the page is trusted by origin: that server serves the app itself.\n * - **Loaded from a file** in a release build or an update, the page is trusted by file name. The\n * web view reports the file as the native side resolved it, which differs from the source URI\n * before the name (on iOS the source is `www.bundle/...`, relative to the app), so the page must\n * be a `file:` URL whose file name is the source's, a hash of the component's path.\n */\nfunction isOwnPage(url: string | undefined, source: string): boolean {\n if (!url) return false;\n const served = /^https?:\\/\\/[^/?#]+/i.exec(source)?.[0];\n if (served) return url.toLowerCase().startsWith(`${served.toLowerCase()}/`);\n return /^file:/i.test(url) && fileName(url) === fileName(source);\n}\n\nfunction fileName(url: string): string {\n return url.split(/[?#]/)[0]!.split('/').at(-1)!;\n}\n\nfunction parse(data: string): PageMessage | undefined {\n try {\n const message: unknown = JSON.parse(data);\n return typeof message === 'object' && message !== null ? (message as PageMessage) : undefined;\n } catch {\n return undefined;\n }\n}\n\ndeclare const __DEV__: boolean | undefined;\n"]}
1
+ {"version":3,"file":"dom-component.js","sourceRoot":"","sources":["../src/dom-component.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `<dom-component>`: an Angular component rendered by a browser - DOM, CSS, anything a browser\n * runs - in a web view inside a native screen, with its inputs and outputs bound from the app's\n * template. Expo calls this a DOM component; it is not the Web Components standard.\n *\n * ```ts\n * // web/note.ts: an ordinary Angular component for the browser, in the app's own source\n * 'use dom';\n * @Component({ selector: 'app-note', template: '<textarea ...></textarea>', styles: [...] })\n * export class Note {\n * readonly label = input('');\n * readonly sent = output<string>();\n * }\n * export default mountInWebView(Note); // from '@ng-native/web/web-view'\n *\n * // a native screen\n * import note from './web/note.ts';\n * <dom-component [src]=\"note\" class=\"flex-1\"\n * [inputs]=\"{ label: title() }\" [outputs]=\"{ sent: save }\" />\n * ```\n *\n * The component runs in Expo's own web view (`@expo/dom-webview`, which `expo` already installs),\n * so it is a separate JavaScript runtime from the app and only JSON crosses. It is shipped the way\n * Expo ships its `'use dom'` components: the import is a reference to a page that Expo's dev server\n * serves in development and `expo export:embed` bundles into the app for a release build (see\n * `@ng-native/metro`'s `dom-component.cjs`), and the page is found where Expo finds its own.\n *\n * - **`inputs`** go into the page before it loads, and a changed value is sent to it afterwards\n * without reloading. Anything sent before the page is ready is sent again once it is.\n * - **`outputs`** map the component's outputs, by public name, to handlers. A name the component\n * has no output for is an error naming the ones it does have, as `<ng-native-island>`'s is.\n * - **Errors** the page throws reach the app's `ErrorHandler`.\n * - **Only its own page is heard.** A message from any other page the web view shows, after a link\n * in the content, say, is ignored, as is one that is not JSON, and a changed input is sent to\n * its own page alone. The inputs given before the page loads are the web view's to hand out,\n * though, and it hands them to whatever page it is showing, so they are no place for a secret.\n */\nimport {\n Component,\n ElementRef,\n InjectionToken,\n computed,\n effect,\n inject,\n input,\n untracked,\n} from '@angular/core';\nimport { Engine, type EngineNode, type NativeSyntheticEvent } from '@ng-native/fabric';\nimport { optional } from './native.ts';\nimport { registerExpoView } from './register-expo-view.ts';\n\n/** The element is this package's, so it needs no `registerExpoViews` call of its own. */\nregisterExpoView('dom-component', 'ExpoDomWebViewModule');\n\n/** A handler for one of the DOM component's outputs. */\nexport type DomComponentOutputHandler = (value: never) => void;\n\n/** The functions Expo's web view defines on its view, called with the view's tag as `this`. */\nexport interface WebViewFunctions {\n injectJavaScript(this: { nativeTag: number }, script: string): Promise<void>;\n}\n\n/** Where the pages are, and how to reach the view once it is on screen. */\nexport interface DomComponentSource {\n /** Where Expo keeps DOM components' pages: the dev server, `www.bundle`, or an update. */\n readonly baseUrl: string | null;\n readonly functions: WebViewFunctions | null;\n}\n\n/** What native code gets for `import note from './web/note.ts'`. */\nexport interface DomComponentReference {\n readonly domComponent: string;\n}\n\ntype PageMessage =\n | { type: 'ready'; outputs: string[] }\n | { type: 'output'; name: string; value: unknown }\n | { type: 'error'; message: string };\n\n@Component({\n selector: 'dom-component',\n template: '',\n host: {\n '[source]': 'source()',\n '[injectedJavaScriptObject]': 'page()',\n '[webviewDebuggingEnabled]': 'debuggable',\n '(message)': 'receive($event)',\n },\n})\nexport class DomComponent {\n /** Overridden in a test to load pages without a dev server. */\n static readonly SOURCE = new InjectionToken<DomComponentSource>(\n 'angular-native.domComponentSource',\n {\n factory: () => {\n // Expo's own resolution, so a page is found wherever Expo would look for a DOM\n // component's: the dev server, the app's `www.bundle`, or a downloaded update.\n const base = optional(() => require('expo/src/dom/base') as { getBaseURL(): string });\n const core = optional(\n () => require('expo-modules-core') as typeof import('expo-modules-core'),\n );\n const module = core?.requireOptionalNativeModule<{\n ViewPrototypes?: Record<string, WebViewFunctions>;\n }>('ExpoDomWebViewModule');\n return {\n baseUrl: optional(() => base?.getBaseURL() ?? null),\n functions: module?.ViewPrototypes?.['ExpoDomWebViewModule'] ?? null,\n };\n },\n },\n );\n\n private readonly native = inject(DomComponent.SOURCE);\n private readonly engine = inject(Engine);\n private readonly node = inject<ElementRef<EngineNode>>(ElementRef).nativeElement;\n private ready = false;\n\n /** The DOM component, as imported: `import note from './web/note.ts'`. */\n readonly src = input.required<DomComponentReference>();\n /** Its inputs, by public name. JSON only: this crosses into another runtime. */\n readonly inputs = input<Readonly<Record<string, unknown>>>({});\n /** Handlers for its outputs, by public name. */\n readonly outputs = input<Readonly<Record<string, DomComponentOutputHandler>>>({});\n\n protected readonly debuggable = typeof __DEV__ === 'undefined' || __DEV__;\n\n protected readonly source = computed(() => {\n const { domComponent } = this.src();\n return this.native.baseUrl === null ? null : { uri: `${this.native.baseUrl}/${domComponent}` };\n });\n\n /** The file's name for an error message: the dev page names it, a release page is a hash. */\n private readonly name = computed(() => this.src().domComponent.split('?')[0]!);\n\n /**\n * What the page reads before it loads. The inputs are read untracked: a change to this prop\n * reloads the page, so a changed input is sent to the running page instead.\n */\n protected readonly page = computed(() => {\n this.src();\n return JSON.stringify({ inputs: untracked(this.inputs) });\n });\n\n constructor() {\n effect(() => {\n const inputs = this.inputs();\n if (this.ready) untracked(() => this.send(inputs));\n });\n }\n\n protected receive(event: NativeSyntheticEvent<{ data: string; url?: string }>): void {\n const { data, url } = event.nativeEvent;\n const source = this.source();\n if (!source || !isOwnPage(url, source.uri)) {\n if (this.debuggable) {\n console.warn(\n `[angular-native] <dom-component> ${this.name()}: ignored a message from ` +\n `${url || 'an unnamed page'}, which is not its own page.`,\n );\n }\n return;\n }\n const message = parse(data);\n if (!message) {\n if (this.debuggable) {\n console.warn(\n `[angular-native] <dom-component> ${this.name()}: dropped a message that is not JSON.`,\n );\n }\n return;\n }\n if (message.type === 'ready') {\n this.checkOutputs(message.outputs);\n this.ready = true;\n this.send(this.inputs());\n } else if (message.type === 'output') {\n const outputs = this.outputs();\n if (Object.hasOwn(outputs, message.name)) outputs[message.name]!(message.value as never);\n } else if (message.type === 'error') {\n throw new Error(`[angular-native] <dom-component> ${this.name()}: ${message.message}`);\n }\n }\n\n private checkOutputs(declared: readonly string[]): void {\n for (const name of Object.keys(this.outputs())) {\n if (!declared.includes(name)) {\n throw new Error(\n `[angular-native] <dom-component>: ${this.name()} has no output '${name}' ` +\n `(its outputs: ${declared.join(', ') || 'none'}).`,\n );\n }\n }\n }\n\n private send(inputs: Readonly<Record<string, unknown>>): void {\n const nativeTag = this.engine.tagOf(this.node);\n const source = this.source();\n if (!this.native.functions || nativeTag === null || !source) return;\n const message = JSON.stringify({ type: 'inputs', inputs });\n void this.native.functions.injectJavaScript.call(\n { nativeTag },\n inOwnPage(source.uri, `window.__ngNative && window.__ngNative.receive(${message});`),\n );\n }\n}\n\n/**\n * Whether the page a message came from is the one the component loaded, so a page the web view\n * was taken to (by a link in the content, say) cannot fire the app's outputs or ask for its inputs.\n *\n * The web view names the page it is showing, not the frame that posted, so this tells pages apart\n * and not an iframe from the page that holds it. Every message is named on both platforms, from\n * the web view's own URL; one with no URL cannot be placed, so it is not trusted either.\n *\n * - **Served by the dev server**, the page is trusted by origin: that server serves the app itself.\n * - **Loaded from a file** in a release build or an update, the page is trusted by file name. The\n * web view reports the file as the native side resolved it, which differs from the source URI\n * before the name (on iOS the source is `www.bundle/...`, relative to the app), so the page must\n * be a `file:` URL whose file name is the source's, a hash of the component's path.\n */\nfunction isOwnPage(url: string | undefined, source: string): boolean {\n if (!url) return false;\n const served = servedFrom(source);\n if (served) return url.toLowerCase().startsWith(`${served}/`);\n return /^file:/i.test(url) && fileName(url) === fileName(source);\n}\n\n/**\n * A script that runs `call` only where the web view shows the component's own page, by\n * `isOwnPage`'s rule, as the page itself reads its address. The web view runs what it is given\n * in whatever page it is showing, and one a link took it to may define `__ngNative` to be handed\n * the app's inputs, and may replace any method of a string or a pattern to pass a check made with\n * one. So the check reads only what such a page cannot change: `location`'s own properties, a\n * string's length and characters, and `===`.\n */\nfunction inOwnPage(source: string, call: string): string {\n const served = servedFrom(source);\n const own = served\n ? `location.origin === ${JSON.stringify(served)}`\n : `location.protocol === 'file:' && (function (path, end) {\n var from = path.length - end.length;\n for (var at = 0; at < end.length; at++) if (path[from + at] !== end[at]) return false;\n return true;\n })(location.pathname, ${JSON.stringify(`/${fileName(source)}`)})`;\n return `if (${own}) { ${call} } true;`;\n}\n\n/** The origin of the dev server that serves the page, lowercased, as `location.origin` has it. */\nfunction servedFrom(source: string): string | undefined {\n return /^https?:\\/\\/[^/?#]+/i.exec(source)?.[0]?.toLowerCase();\n}\n\nfunction fileName(url: string): string {\n return url.split(/[?#]/)[0]!.split('/').at(-1)!;\n}\n\nfunction parse(data: string): PageMessage | undefined {\n try {\n const message: unknown = JSON.parse(data);\n return typeof message === 'object' && message !== null ? (message as PageMessage) : undefined;\n } catch {\n return undefined;\n }\n}\n\ndeclare const __DEV__: boolean | undefined;\n"]}
@@ -967,9 +967,13 @@ export declare class UiBottomSheet {
967
967
  */
968
968
  readonly backgroundInteraction: import("@angular/core").InputSignal<UiBottomSheetBackgroundInteraction | undefined>;
969
969
  /**
970
- * More SwiftUI modifiers, iOS only, after the sheet's own on the `ui-group` around its content,
971
- * where SwiftUI reads presentation modifiers from: `presentationCornerRadius`, for one. They
972
- * apply to the content too. Android takes no notice of them.
970
+ * More SwiftUI modifiers, iOS only, for presentation modifiers such as `presentationCornerRadius`
971
+ * and for ones that apply to the content, such as `ignoreSafeArea`. Where the content is in a
972
+ * navigation stack (the build has SwiftUI's toolbar and the sheet is not sized to its content)
973
+ * they are on the `ui-zstack` inside that stack, since the stack sets up the safe area again for
974
+ * what is inside it, and a content modifier such as `background` or `padding` applies below the
975
+ * sheet's bar, where it has one. Otherwise they are after the sheet's own on the `ui-group`
976
+ * around its content. Android takes no notice of them.
973
977
  */
974
978
  readonly modifiers: import("@angular/core").InputSignal<readonly UiModifier[] | undefined>;
975
979
  /**
@@ -1845,9 +1845,13 @@ export class UiBottomSheet {
1845
1845
  backgroundInteraction = input(/* @ts-ignore */
1846
1846
  ...(ngDevMode ? [undefined, { debugName: "backgroundInteraction" }] : /* istanbul ignore next */ []));
1847
1847
  /**
1848
- * More SwiftUI modifiers, iOS only, after the sheet's own on the `ui-group` around its content,
1849
- * where SwiftUI reads presentation modifiers from: `presentationCornerRadius`, for one. They
1850
- * apply to the content too. Android takes no notice of them.
1848
+ * More SwiftUI modifiers, iOS only, for presentation modifiers such as `presentationCornerRadius`
1849
+ * and for ones that apply to the content, such as `ignoreSafeArea`. Where the content is in a
1850
+ * navigation stack (the build has SwiftUI's toolbar and the sheet is not sized to its content)
1851
+ * they are on the `ui-zstack` inside that stack, since the stack sets up the safe area again for
1852
+ * what is inside it, and a content modifier such as `background` or `padding` applies below the
1853
+ * sheet's bar, where it has one. Otherwise they are after the sheet's own on the `ui-group`
1854
+ * around its content. Android takes no notice of them.
1851
1855
  */
1852
1856
  modifiers = input(/* @ts-ignore */
1853
1857
  ...(ngDevMode ? [undefined, { debugName: "modifiers" }] : /* istanbul ignore next */ []));
@@ -1923,7 +1927,7 @@ export class UiBottomSheet {
1923
1927
  ? [{ $type: 'interactiveDismissDisabled', isDisabled: true }]
1924
1928
  : []),
1925
1929
  ...this.interaction(),
1926
- ...(this.modifiers() ?? []),
1930
+ ...(this.stacked() ? [] : (this.modifiers() ?? [])),
1927
1931
  ], /* @ts-ignore */
1928
1932
  ...(ngDevMode ? [{ debugName: "presentation" }] : /* istanbul ignore next */ []));
1929
1933
  /** Whether the screen behind the sheet takes touches, in `@expo/ui`'s shape, where one is set. */
@@ -2090,7 +2094,7 @@ export class UiBottomSheet {
2090
2094
  <ui-group [modifiers]="presentation()">
2091
2095
  @if (stacked()) {
2092
2096
  <ui-navigation-stack>
2093
- <ui-zstack>
2097
+ <ui-zstack [modifiers]="modifiers()">
2094
2098
  <ui-toolbar [modifiers]="inlineTitle">
2095
2099
  <ui-spacer />
2096
2100
  <ui-slot name="content"><ng-content select="ui-toolbar-item" /></ui-slot>
@@ -2159,7 +2163,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
2159
2163
  <ui-group [modifiers]="presentation()">
2160
2164
  @if (stacked()) {
2161
2165
  <ui-navigation-stack>
2162
- <ui-zstack>
2166
+ <ui-zstack [modifiers]="modifiers()">
2163
2167
  <ui-toolbar [modifiers]="inlineTitle">
2164
2168
  <ui-spacer />
2165
2169
  <ui-slot name="content"><ng-content select="ui-toolbar-item" /></ui-slot>