@symbiote-native/angular 0.6.2 → 0.8.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/README.md +3 -3
- package/bin/symbiote-angular-dev.cjs +20 -19
- package/build/angular/anchor-host-registry.js +2 -2
- package/build/angular/anchor-host-registry.js.map +1 -1
- package/build/angular/bootstrap.js +1 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.android.js +12 -4
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.ios.js +8 -3
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
- package/build/angular/components/activity-indicator/shared.d.ts +1 -1
- package/build/angular/components/activity-indicator/shared.js +4 -4
- package/build/angular/components/activity-indicator/shared.js.map +1 -1
- package/build/angular/components/button.d.ts +4 -2
- package/build/angular/components/button.js +41 -22
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +6 -4
- package/build/angular/components/flat-list/index.js +80 -50
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.d.ts +8 -3
- package/build/angular/components/image/index.android.js +42 -10
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.d.ts +8 -3
- package/build/angular/components/image/index.ios.js +42 -10
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -1
- package/build/angular/components/image/shared.js +51 -8
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +2 -1
- package/build/angular/components/image-background.js +26 -10
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +9 -4
- package/build/angular/components/input-accessory-view/index.js +49 -10
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +44 -12
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +11 -8
- package/build/angular/components/modal/index.js +51 -29
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +12 -6
- package/build/angular/components/pressable/index.js +66 -13
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +8 -4
- package/build/angular/components/refresh-control/index.js +54 -16
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +9 -4
- package/build/angular/components/safe-area-view/index.js +48 -9
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.d.ts +2 -3
- package/build/angular/components/scroll-view/index.android.js +88 -61
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
- package/build/angular/components/scroll-view/index.ios.js +24 -18
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +35 -0
- package/build/angular/components/scroll-view/projection.js +279 -53
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.d.ts +14 -5
- package/build/angular/components/scroll-view/shared.js +225 -75
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +21 -22
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +7 -1
- package/build/angular/components/section-list/index.js +91 -54
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.d.ts +7 -3
- package/build/angular/components/switch/index.android.js +52 -10
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.d.ts +7 -3
- package/build/angular/components/switch/index.ios.js +52 -10
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +13 -4
- package/build/angular/components/switch/shared.js +71 -7
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +11 -3
- package/build/angular/components/text-input.js +68 -8
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +40 -9
- package/build/angular/components/touchable/index.js +303 -67
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
- package/build/angular/components/touchable-native-feedback/index.js +63 -27
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +22 -18
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +15 -4
- package/build/angular/components/virtualized-list/index.js +326 -94
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +12 -12
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +19 -1
- package/build/angular/components/virtualized-section-list/index.js +93 -22
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +2 -2
- package/build/angular/components.js +5 -0
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +34 -36
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +19 -22
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +6 -4
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +33 -0
- package/build/angular/diagnostics.js +82 -0
- package/build/angular/diagnostics.js.map +1 -0
- package/build/angular/host-instance/index.js +1 -1
- package/build/angular/host-instance/index.js.map +1 -1
- package/build/angular/index.d.ts +9 -4
- package/build/angular/index.js +17 -3
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
- package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
- package/build/angular/modules/animated/create-animated-component.js +106 -55
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +4 -4
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +20 -3
- package/build/angular/primitives/index.js +84 -48
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +48 -2
- package/build/angular/primitives/shared.js +158 -11
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +69 -38
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +37 -2
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js +3 -3
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +4 -4
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/package.json +10 -8
- package/src/anchor-host-registry.ts +2 -2
- package/src/bootstrap.ts +4 -1
- package/src/components/activity-indicator/index.android.ts +8 -1
- package/src/components/activity-indicator/index.ios.ts +4 -0
- package/src/components/activity-indicator/shared.ts +32 -13
- package/src/components/button.ts +46 -19
- package/src/components/flat-list/index.ts +122 -62
- package/src/components/image/index.android.ts +58 -7
- package/src/components/image/index.ios.ts +58 -7
- package/src/components/image/shared.ts +92 -21
- package/src/components/image-background.ts +29 -9
- package/src/components/input-accessory-view/index.ts +67 -12
- package/src/components/keyboard-avoiding-view/index.ts +61 -15
- package/src/components/modal/index.ts +72 -33
- package/src/components/pressable/index.ts +117 -28
- package/src/components/refresh-control/index.ts +74 -18
- package/src/components/safe-area-view/index.ts +64 -10
- package/src/components/scroll-view/index.android.ts +62 -47
- package/src/components/scroll-view/index.ios.ts +19 -17
- package/src/components/scroll-view/projection.ts +363 -64
- package/src/components/scroll-view/shared.ts +312 -90
- package/src/components/scroll-view/sticky-header.ts +36 -24
- package/src/components/section-list/index.ts +86 -53
- package/src/components/switch/index.android.ts +60 -8
- package/src/components/switch/index.ios.ts +60 -8
- package/src/components/switch/shared.ts +93 -6
- package/src/components/text-input.ts +95 -13
- package/src/components/touchable/index.ts +348 -64
- package/src/components/touchable-native-feedback/index.ts +94 -31
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +380 -85
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +146 -33
- package/src/components.ts +9 -1
- package/src/create-portal/index.ts +31 -31
- package/src/create-tunnel/index.ts +16 -17
- package/src/descriptor-to-angular/index.ts +29 -8
- package/src/diagnostics.ts +138 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +61 -3
- package/src/modules/animated/animated-leaf-binder.ts +28 -77
- package/src/modules/animated/create-animated-component.ts +115 -47
- package/src/modules/app-registry/index.ts +9 -9
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +47 -2
- package/src/primitives/shared.ts +170 -10
- package/src/render/index.ts +77 -39
- package/src/renderer/index.ts +76 -14
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
- package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
- package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
|
@@ -13,11 +13,14 @@ import {
|
|
|
13
13
|
ChangeDetectionStrategy,
|
|
14
14
|
ChangeDetectorRef,
|
|
15
15
|
Component,
|
|
16
|
+
computed,
|
|
16
17
|
ElementRef,
|
|
17
18
|
EventEmitter,
|
|
18
19
|
inject,
|
|
19
20
|
Input,
|
|
20
21
|
Output,
|
|
22
|
+
signal,
|
|
23
|
+
type DoCheck,
|
|
21
24
|
type OnChanges,
|
|
22
25
|
type OnInit,
|
|
23
26
|
type SimpleChanges,
|
|
@@ -33,7 +36,6 @@ import {
|
|
|
33
36
|
type IAriaProps,
|
|
34
37
|
type IModalAnimationType,
|
|
35
38
|
type IModalOrientation,
|
|
36
|
-
type IModalOrientationChangeEvent,
|
|
37
39
|
type IModalPresentationStyle,
|
|
38
40
|
type IModalState,
|
|
39
41
|
} from '@symbiote-native/components';
|
|
@@ -44,7 +46,13 @@ import {
|
|
|
44
46
|
type ISymbioteEvent,
|
|
45
47
|
type IViewStyle,
|
|
46
48
|
} from '@symbiote-native/engine';
|
|
47
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
anchorHostStyle,
|
|
51
|
+
ModalHost,
|
|
52
|
+
SymbioteHostPropsDirective,
|
|
53
|
+
SymbioteStyleInputDirective,
|
|
54
|
+
ViewHost,
|
|
55
|
+
} from '../../primitives';
|
|
48
56
|
|
|
49
57
|
export type {
|
|
50
58
|
IModalAnimationType,
|
|
@@ -53,17 +61,6 @@ export type {
|
|
|
53
61
|
IModalOrientationChangeEvent,
|
|
54
62
|
} from '@symbiote-native/components';
|
|
55
63
|
|
|
56
|
-
const ORIENTATIONS: ReadonlyArray<IModalOrientation> = ['portrait', 'landscape'];
|
|
57
|
-
|
|
58
|
-
function readOrientation(event: ISymbioteEvent): IModalOrientationChangeEvent | undefined {
|
|
59
|
-
const native = event.nativeEvent;
|
|
60
|
-
if (typeof native !== 'object' || native === null || !('orientation' in native)) return undefined;
|
|
61
|
-
const orientation = native.orientation;
|
|
62
|
-
return ORIENTATIONS.some(value => value === orientation)
|
|
63
|
-
? { orientation: orientation === 'landscape' ? 'landscape' : 'portrait' }
|
|
64
|
-
: undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
64
|
// Mirrors React's IModalProps minus children (Angular takes children via <ng-content>).
|
|
68
65
|
export interface IAngularModalProps extends IAccessibilityProps, IAriaProps {
|
|
69
66
|
visible?: boolean;
|
|
@@ -79,7 +76,9 @@ export interface IAngularModalProps extends IAccessibilityProps, IAriaProps {
|
|
|
79
76
|
onShow?: () => void;
|
|
80
77
|
onDismiss?: () => void;
|
|
81
78
|
onRequestClose?: () => void;
|
|
82
|
-
|
|
79
|
+
// The engine hands every listener the ISymbioteEvent wrapper, so the orientation is read at
|
|
80
|
+
// event.nativeEvent.orientation (IModalOrientationChangeEvent describes that payload).
|
|
81
|
+
onOrientationChange?: (event: ISymbioteEvent) => void;
|
|
83
82
|
style?: IStyleProp<IViewStyle>;
|
|
84
83
|
}
|
|
85
84
|
|
|
@@ -102,17 +101,20 @@ export type IAngularModalInputs = Omit<
|
|
|
102
101
|
@Component({
|
|
103
102
|
selector: 'Modal',
|
|
104
103
|
standalone: true,
|
|
104
|
+
hostDirectives: [
|
|
105
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
106
|
+
],
|
|
105
107
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
106
108
|
imports: [ModalHost, ViewHost, SymbioteHostPropsDirective],
|
|
107
109
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
108
110
|
template: `
|
|
109
111
|
@if (shouldRender) {
|
|
110
112
|
<symbiote-modal
|
|
111
|
-
[symbioteHostProps]="hostProps"
|
|
113
|
+
[symbioteHostProps]="hostProps()"
|
|
112
114
|
(show)="show.emit()"
|
|
113
115
|
(dismiss)="dismiss.emit()"
|
|
114
116
|
(requestClose)="requestClose.emit()"
|
|
115
|
-
(orientationChange)="
|
|
117
|
+
(orientationChange)="emit(orientationChange, $event)"
|
|
116
118
|
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
117
119
|
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
118
120
|
(magicTap)="emit(magicTap, $event)"
|
|
@@ -125,11 +127,11 @@ export type IAngularModalInputs = Omit<
|
|
|
125
127
|
}
|
|
126
128
|
`,
|
|
127
129
|
})
|
|
128
|
-
export class Modal implements IAngularModalInputs, OnInit, OnChanges {
|
|
130
|
+
export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
|
|
129
131
|
@Output() readonly show = new EventEmitter<void>();
|
|
130
132
|
@Output() readonly dismiss = new EventEmitter<void>();
|
|
131
133
|
@Output() readonly requestClose = new EventEmitter<void>();
|
|
132
|
-
@Output() readonly orientationChange = new EventEmitter<
|
|
134
|
+
@Output() readonly orientationChange = new EventEmitter<ISymbioteEvent>();
|
|
133
135
|
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
134
136
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
135
137
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
@@ -154,8 +156,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
|
|
|
154
156
|
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
155
157
|
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
156
158
|
@Input() accessibilityLabelledBy?: string | string[];
|
|
157
|
-
@Input()
|
|
158
|
-
|
|
159
|
+
@Input()
|
|
160
|
+
importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
161
|
+
@Input()
|
|
162
|
+
accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
159
163
|
@Input() screenReaderFocusable?: boolean;
|
|
160
164
|
@Input() accessibilityViewIsModal?: boolean;
|
|
161
165
|
@Input() accessibilityElementsHidden?: boolean;
|
|
@@ -195,7 +199,20 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
|
|
|
195
199
|
this.state = createInitialModalState(this.visible === true);
|
|
196
200
|
}
|
|
197
201
|
|
|
202
|
+
// Bridges the non-reactive @Input fields `hostProps` reads into the reactive graph, so it can
|
|
203
|
+
// memoize. Plain fields read inside a computed() are UNTRACKED - something must signal "a
|
|
204
|
+
// dependency changed" or the bag goes stale. Signal inputs would do this natively, but `input()`
|
|
205
|
+
// is visible only to the AOT compiler and this package's unit suite runs on JIT (see the
|
|
206
|
+
// `angular-adapter-change-detection` skill, §6); `signal()`/`computed()` are plain runtime APIs.
|
|
207
|
+
// The keep-alive `state` is deliberately NOT a dependency: it gates `shouldRender`, never the bag.
|
|
208
|
+
private readonly hostPropsRevision = signal(0);
|
|
209
|
+
// What the anchor's class-derived style was when the bag was last built (identity, not value).
|
|
210
|
+
private lastAnchorStyle: unknown;
|
|
211
|
+
|
|
198
212
|
ngOnChanges(changes: SimpleChanges): void {
|
|
213
|
+
// Before the keep-alive early-return below: this is the single moment Angular has finished
|
|
214
|
+
// writing every changed @Input, and `hostProps` depends on far more than `visible`.
|
|
215
|
+
this.hostPropsRevision.update(revision => revision + 1);
|
|
199
216
|
const visibleChange = changes.visible;
|
|
200
217
|
// First change is reflected by the ngOnInit seed; only later toggles drive the keep-alive.
|
|
201
218
|
if (visibleChange === undefined || visibleChange.firstChange) return;
|
|
@@ -203,7 +220,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
|
|
|
203
220
|
// Queue on a microtask so the reducer runs AFTER this CD pass renders with the OLD state —
|
|
204
221
|
// the keep-alive frame. A synchronous dispatch here would unmount in the same pass.
|
|
205
222
|
queueMicrotask(() => {
|
|
206
|
-
this.state = modalReducer(
|
|
223
|
+
this.state = modalReducer(
|
|
224
|
+
this.state,
|
|
225
|
+
isVisible ? { type: 'show' } : { type: 'hide' },
|
|
226
|
+
);
|
|
207
227
|
this.changeDetector.markForCheck();
|
|
208
228
|
});
|
|
209
229
|
}
|
|
@@ -233,13 +253,34 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
|
|
|
233
253
|
});
|
|
234
254
|
}
|
|
235
255
|
|
|
256
|
+
// The anchor's class-derived style is NOT an @Input: `class="..."`/`[ngClass]` at the use site
|
|
257
|
+
// resolves through the renderer's addClass onto this component's anchor host (see
|
|
258
|
+
// anchorHostStyle's doc comment), so it never shows up in SimpleChanges and ngOnChanges alone
|
|
259
|
+
// would leave a later class toggle stranded. ngDoCheck runs at exactly the cadence the old
|
|
260
|
+
// getter was re-read, and bumps only on a real identity change.
|
|
261
|
+
ngDoCheck(): void {
|
|
262
|
+
const anchorStyle = anchorHostStyle(this.elementRef);
|
|
263
|
+
if (anchorStyle === this.lastAnchorStyle) return;
|
|
264
|
+
this.lastAnchorStyle = anchorStyle;
|
|
265
|
+
this.hostPropsRevision.update(revision => revision + 1);
|
|
266
|
+
}
|
|
267
|
+
|
|
236
268
|
// renderModal's own MODAL_HOST_STYLE (position:'absolute') is the outer symbiote-modal node's
|
|
237
269
|
// style; the anchor's class-derived style goes FIRST, that resolved style SECOND —
|
|
238
270
|
// flattenStyle's later-wins collapse keeps the modal's own style winning over its ambient class.
|
|
239
|
-
|
|
271
|
+
//
|
|
272
|
+
// A computed(), not a getter: Angular re-reads a template getter on every refresh of this view
|
|
273
|
+
// and `[symbioteHostProps]` compares by REFERENCE, so a freshly rebuilt (but identical) bag
|
|
274
|
+
// re-pushes every key through renderer.setProperty -> the engine's prop routing - and this one
|
|
275
|
+
// re-runs renderModal to build it.
|
|
276
|
+
readonly hostProps = computed<Record<string, unknown>>(() => {
|
|
277
|
+
this.hostPropsRevision();
|
|
240
278
|
const descriptorProps = this.descriptor.props;
|
|
241
|
-
return {
|
|
242
|
-
|
|
279
|
+
return {
|
|
280
|
+
...descriptorProps,
|
|
281
|
+
style: [anchorHostStyle(this.elementRef), descriptorProps.style],
|
|
282
|
+
};
|
|
283
|
+
});
|
|
243
284
|
|
|
244
285
|
get containerStyle(): unknown {
|
|
245
286
|
const container = this.descriptor.children[0];
|
|
@@ -250,15 +291,11 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
|
|
|
250
291
|
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
251
292
|
}
|
|
252
293
|
|
|
253
|
-
emitOrientation(event: unknown): void {
|
|
254
|
-
if (!isSymbioteEvent(event)) return;
|
|
255
|
-
const change = readOrientation(event);
|
|
256
|
-
if (change !== undefined) this.orientationChange.emit(change);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
294
|
// Typed as the a11y intersection WITH the string index (the bag renderModal spreads into the
|
|
260
295
|
// host props), so resolveAccessibilityProps's result stays assignable to passthrough.
|
|
261
|
-
private accessibilityInputs(): IAccessibilityProps &
|
|
296
|
+
private accessibilityInputs(): IAccessibilityProps &
|
|
297
|
+
IAriaProps &
|
|
298
|
+
Record<string, unknown> {
|
|
262
299
|
return {
|
|
263
300
|
testID: this.testID,
|
|
264
301
|
accessible: this.accessible,
|
|
@@ -276,8 +313,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
|
|
|
276
313
|
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
277
314
|
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
278
315
|
accessibilityLanguage: this.accessibilityLanguage,
|
|
279
|
-
accessibilityRespondsToUserInteraction:
|
|
280
|
-
|
|
316
|
+
accessibilityRespondsToUserInteraction:
|
|
317
|
+
this.accessibilityRespondsToUserInteraction,
|
|
318
|
+
accessibilityShowsLargeContentViewer:
|
|
319
|
+
this.accessibilityShowsLargeContentViewer,
|
|
281
320
|
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
282
321
|
role: this.role,
|
|
283
322
|
'aria-label': this.ariaLabel,
|
|
@@ -5,10 +5,14 @@ import {
|
|
|
5
5
|
Component,
|
|
6
6
|
ElementRef,
|
|
7
7
|
EventEmitter,
|
|
8
|
+
computed,
|
|
8
9
|
inject,
|
|
9
10
|
Input,
|
|
10
11
|
Output,
|
|
12
|
+
signal,
|
|
11
13
|
ViewChild,
|
|
14
|
+
type DoCheck,
|
|
15
|
+
type OnChanges,
|
|
12
16
|
} from '@angular/core';
|
|
13
17
|
import {
|
|
14
18
|
createPressHandlers,
|
|
@@ -40,15 +44,24 @@ import {
|
|
|
40
44
|
type IStyleProp,
|
|
41
45
|
type IViewStyle,
|
|
42
46
|
} from '@symbiote-native/engine';
|
|
43
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
anchorHostStyle,
|
|
49
|
+
SymbioteHostPropsDirective,
|
|
50
|
+
SymbioteStyleInputDirective,
|
|
51
|
+
ViewHost,
|
|
52
|
+
} from '../../primitives';
|
|
44
53
|
|
|
45
|
-
export type {
|
|
54
|
+
export type {
|
|
55
|
+
IPressState,
|
|
56
|
+
IPressableAndroidRippleConfig,
|
|
57
|
+
} from '@symbiote-native/components';
|
|
46
58
|
|
|
47
59
|
// The full logical Pressable surface, callback-shaped press/hover events included. Touchable* and
|
|
48
60
|
// TouchableNativeFeedback still take their OWN onPress/onPressIn/... as @Input() callbacks (that is
|
|
49
61
|
// a separate, not-yet-converted decision) and derive their public prop types from this - so it keeps
|
|
50
62
|
// the callback fields even though the Pressable component below no longer implements them directly.
|
|
51
|
-
export interface IAngularPressableProps
|
|
63
|
+
export interface IAngularPressableProps
|
|
64
|
+
extends IAccessibilityProps, IAriaProps {
|
|
52
65
|
onPress?: IPressHandler;
|
|
53
66
|
onPressIn?: IPressHandler;
|
|
54
67
|
onPressOut?: IPressHandler;
|
|
@@ -74,7 +87,8 @@ export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps
|
|
|
74
87
|
nextFocusLeft?: number;
|
|
75
88
|
nextFocusRight?: number;
|
|
76
89
|
nextFocusUp?: number;
|
|
77
|
-
style?:
|
|
90
|
+
style?:
|
|
91
|
+
IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
// What the Pressable component itself takes as plain @Input()s: the full surface minus the press/
|
|
@@ -96,7 +110,9 @@ export type IAngularPressableInputs = Omit<
|
|
|
96
110
|
| 'onAccessibilityEscape'
|
|
97
111
|
>;
|
|
98
112
|
|
|
99
|
-
function isStyleFn(
|
|
113
|
+
function isStyleFn(
|
|
114
|
+
value: unknown,
|
|
115
|
+
): value is (state: IPressState) => IStyleProp<IViewStyle> {
|
|
100
116
|
return typeof value === 'function';
|
|
101
117
|
}
|
|
102
118
|
|
|
@@ -109,12 +125,15 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
|
|
|
109
125
|
@Component({
|
|
110
126
|
selector: 'Pressable, symbiote-pressable',
|
|
111
127
|
standalone: true,
|
|
128
|
+
hostDirectives: [
|
|
129
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
130
|
+
],
|
|
112
131
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
113
132
|
imports: [ViewHost, SymbioteHostPropsDirective],
|
|
114
133
|
template: `
|
|
115
134
|
<symbiote-view
|
|
116
135
|
#host
|
|
117
|
-
[symbioteHostProps]="hostProps"
|
|
136
|
+
[symbioteHostProps]="hostProps()"
|
|
118
137
|
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
119
138
|
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
120
139
|
(magicTap)="emit(magicTap, $event)"
|
|
@@ -131,7 +150,7 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
|
|
|
131
150
|
`,
|
|
132
151
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
133
152
|
})
|
|
134
|
-
export class Pressable implements IAngularPressableInputs {
|
|
153
|
+
export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
135
154
|
// The press/hover lifecycle as real Angular events: `(press)="onTap($event)"`, not
|
|
136
155
|
// `[onPress]="onTap"`. createPressHandlers still wants plain IPressHandler callbacks, so
|
|
137
156
|
// emitterHandler() below adapts each EventEmitter into one - only while something is actually
|
|
@@ -176,10 +195,13 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
176
195
|
@Input() accessibilityRole?: IAngularPressableProps['accessibilityRole'];
|
|
177
196
|
@Input() accessibilityState?: IAccessibilityStateValue;
|
|
178
197
|
@Input() accessibilityValue?: IAngularPressableProps['accessibilityValue'];
|
|
179
|
-
@Input()
|
|
198
|
+
@Input()
|
|
199
|
+
accessibilityActions?: IAngularPressableProps['accessibilityActions'];
|
|
180
200
|
@Input() accessibilityLabelledBy?: string | string[];
|
|
181
|
-
@Input()
|
|
182
|
-
|
|
201
|
+
@Input()
|
|
202
|
+
importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
203
|
+
@Input()
|
|
204
|
+
accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
183
205
|
@Input() screenReaderFocusable?: boolean;
|
|
184
206
|
@Input() accessibilityViewIsModal?: boolean;
|
|
185
207
|
@Input() accessibilityElementsHidden?: boolean;
|
|
@@ -205,13 +227,23 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
205
227
|
@Input() id?: string;
|
|
206
228
|
@Input() role?: IAngularPressableProps['role'];
|
|
207
229
|
|
|
208
|
-
@ViewChild('host', { read: ElementRef })
|
|
230
|
+
@ViewChild('host', { read: ElementRef })
|
|
231
|
+
private hostElement?: ElementRef<unknown>;
|
|
232
|
+
|
|
233
|
+
// INTERNAL mutable state, driven by the press machine rather than by an @Input, so ngOnChanges
|
|
234
|
+
// can never cover it - it carries its OWN signal, which is what lets the memoized bag below stay
|
|
235
|
+
// correct across a press. Exposed as a plain boolean getter so a use site's `#ref.pressed` keeps
|
|
236
|
+
// reading a boolean and the public surface is unchanged.
|
|
237
|
+
private readonly pressedState = signal(false);
|
|
238
|
+
|
|
239
|
+
get pressed(): boolean {
|
|
240
|
+
return this.pressedState();
|
|
241
|
+
}
|
|
209
242
|
|
|
210
|
-
pressed = false;
|
|
211
243
|
private readonly runtime: IPressRuntime = createPressRuntime();
|
|
212
244
|
private readonly host: IPressHost = {
|
|
213
245
|
setPressed: pressed => {
|
|
214
|
-
this.pressed
|
|
246
|
+
this.pressedState.set(pressed);
|
|
215
247
|
this.changeDetector.detectChanges();
|
|
216
248
|
},
|
|
217
249
|
getMeasureFn: () => {
|
|
@@ -231,6 +263,39 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
231
263
|
// inner `symbiote-view` one level down.
|
|
232
264
|
private readonly elementRef = inject(ElementRef);
|
|
233
265
|
|
|
266
|
+
// Bridges the non-reactive @Input fields above into the reactive graph, so the computed() bag
|
|
267
|
+
// below can memoize a value derived from them. Signal inputs would make this unnecessary, but
|
|
268
|
+
// `input()` is visible only to the AOT compiler and this package's unit suite runs on JIT - see
|
|
269
|
+
// the `angular-adapter-change-detection` skill, §6. Mirrors ScrollView's identical bridge
|
|
270
|
+
// (components/scroll-view/shared.ts), including its constraint: ONLY safe for a bag whose every
|
|
271
|
+
// dependency is an @Input or carries its own signal, never for plain internal mutable state.
|
|
272
|
+
private readonly inputsRevision = signal(0);
|
|
273
|
+
|
|
274
|
+
// The anchor's class-derived style is the one hostProps dependency that is NOT an @Input:
|
|
275
|
+
// `class="..."`/`[class.x]`/`[ngClass]` at the use site resolves through the renderer's
|
|
276
|
+
// addClass/removeClass onto the anchor node (anchorHostStyle's doc comment), so it never appears
|
|
277
|
+
// in SimpleChanges and ngOnChanges cannot cover it. The engine's commitClassStyle allocates a
|
|
278
|
+
// fresh [classStyle, explicitStyle] array on exactly the passes where a class token actually
|
|
279
|
+
// moved, so signal.set's own Object.is check makes an unchanged poll a no-op that dirties nothing.
|
|
280
|
+
private readonly anchorStyle = signal<unknown>(undefined);
|
|
281
|
+
|
|
282
|
+
// ngDoCheck is the only hook that observes the anchor AFTER the parent wrote it. Angular flushes
|
|
283
|
+
// a node's pre-order hooks at the next `advance` PAST it (hooks.ts callHooks runs "until that
|
|
284
|
+
// node index exclusive") or in refreshView's post-template flush - both strictly after the
|
|
285
|
+
// parent's classProp on this element, and strictly before this component's own view refreshes.
|
|
286
|
+
// Angular also nulls the active consumer around lifecycle hooks (render3/hooks.ts), so this
|
|
287
|
+
// write registers no dependency on the caller's view and cannot throw NG0600.
|
|
288
|
+
ngDoCheck(): void {
|
|
289
|
+
this.anchorStyle.set(anchorHostStyle(this.elementRef));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
ngOnChanges(): void {
|
|
293
|
+
// Tells the computed bag below that an @Input moved - plain class fields are not reactive on
|
|
294
|
+
// their own. Must stay in ngOnChanges: that is the single moment Angular has finished writing
|
|
295
|
+
// every changed input for this pass.
|
|
296
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
297
|
+
}
|
|
298
|
+
|
|
234
299
|
get resolvedStyle(): IStyleProp<IViewStyle> | undefined {
|
|
235
300
|
const state: IPressState = { pressed: this.pressed };
|
|
236
301
|
return isStyleFn(this.style) ? this.style(state) : this.style;
|
|
@@ -241,7 +306,10 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
241
306
|
accessibilityLabel: this.accessibilityLabel,
|
|
242
307
|
accessibilityHint: this.accessibilityHint,
|
|
243
308
|
accessibilityRole: this.accessibilityRole,
|
|
244
|
-
accessibilityState: resolveDisabledAccessibilityState(
|
|
309
|
+
accessibilityState: resolveDisabledAccessibilityState(
|
|
310
|
+
this.accessibilityState,
|
|
311
|
+
this.disabled,
|
|
312
|
+
),
|
|
245
313
|
accessibilityValue: this.accessibilityValue,
|
|
246
314
|
accessibilityActions: this.accessibilityActions,
|
|
247
315
|
accessibilityLabelledBy: this.accessibilityLabelledBy,
|
|
@@ -252,8 +320,10 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
252
320
|
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
253
321
|
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
254
322
|
accessibilityLanguage: this.accessibilityLanguage,
|
|
255
|
-
accessibilityRespondsToUserInteraction:
|
|
256
|
-
|
|
323
|
+
accessibilityRespondsToUserInteraction:
|
|
324
|
+
this.accessibilityRespondsToUserInteraction,
|
|
325
|
+
accessibilityShowsLargeContentViewer:
|
|
326
|
+
this.accessibilityShowsLargeContentViewer,
|
|
257
327
|
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
258
328
|
'aria-label': this.ariaLabel,
|
|
259
329
|
'aria-busy': this.ariaBusy,
|
|
@@ -275,11 +345,20 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
275
345
|
}
|
|
276
346
|
|
|
277
347
|
get resolvedRippleProps(): Record<string, unknown> | undefined {
|
|
278
|
-
return this.android_ripple === undefined
|
|
348
|
+
return this.android_ripple === undefined
|
|
349
|
+
? undefined
|
|
350
|
+
: rippleProps(this.android_ripple);
|
|
279
351
|
}
|
|
280
352
|
|
|
281
353
|
// Flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) - see primitives/shared.ts.
|
|
282
|
-
|
|
354
|
+
//
|
|
355
|
+
// MEASURED 2026-08-16: as a getter this rebuilt the bag on every refresh of this view, and a
|
|
356
|
+
// press dirties this very view (SymbioteHostPropsDirective wraps each flat-bag onX with
|
|
357
|
+
// markForCheck, and setPressed calls detectChanges outright), so `[symbioteHostProps]`'s
|
|
358
|
+
// reference check failed every time and re-pushed every key through Renderer2.setProperty ->
|
|
359
|
+
// routeProp for an identical bag. Memoized, an unchanged bag costs one reference compare.
|
|
360
|
+
readonly hostProps = computed<Record<string, unknown>>(() => {
|
|
361
|
+
this.inputsRevision();
|
|
283
362
|
return {
|
|
284
363
|
testID: this.testID,
|
|
285
364
|
nativeID: this.nativeID,
|
|
@@ -289,36 +368,42 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
289
368
|
nextFocusLeft: this.nextFocusLeft,
|
|
290
369
|
nextFocusRight: this.nextFocusRight,
|
|
291
370
|
nextFocusUp: this.nextFocusUp,
|
|
292
|
-
style: [
|
|
371
|
+
style: [this.anchorStyle(), this.resolvedStyle],
|
|
293
372
|
accessible: this.accessible,
|
|
294
373
|
...this.foldedAccessibility,
|
|
295
374
|
android_disableSound: this.android_disableSound,
|
|
296
|
-
nativeBackgroundAndroid:
|
|
297
|
-
|
|
375
|
+
nativeBackgroundAndroid:
|
|
376
|
+
this.resolvedRippleProps?.nativeBackgroundAndroid,
|
|
377
|
+
nativeForegroundAndroid:
|
|
378
|
+
this.resolvedRippleProps?.nativeForegroundAndroid,
|
|
298
379
|
};
|
|
299
|
-
}
|
|
380
|
+
});
|
|
300
381
|
|
|
301
382
|
handlePressIn(event: unknown): void {
|
|
302
383
|
const symbioteEvent = asSymbioteEvent(event);
|
|
303
|
-
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
|
|
384
|
+
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
|
|
385
|
+
return;
|
|
304
386
|
this.handlers.handlePressIn(symbioteEvent);
|
|
305
387
|
}
|
|
306
388
|
|
|
307
389
|
handlePressOut(event: unknown): void {
|
|
308
390
|
const symbioteEvent = asSymbioteEvent(event);
|
|
309
|
-
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
|
|
391
|
+
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
|
|
392
|
+
return;
|
|
310
393
|
this.handlers.handlePressOut(symbioteEvent);
|
|
311
394
|
}
|
|
312
395
|
|
|
313
396
|
handlePress(event: unknown): void {
|
|
314
397
|
const symbioteEvent = asSymbioteEvent(event);
|
|
315
|
-
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
|
|
398
|
+
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
|
|
399
|
+
return;
|
|
316
400
|
this.handlers.handlePress(symbioteEvent);
|
|
317
401
|
}
|
|
318
402
|
|
|
319
403
|
handleResponderMove(event: unknown): void {
|
|
320
404
|
const symbioteEvent = asSymbioteEvent(event);
|
|
321
|
-
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
|
|
405
|
+
if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
|
|
406
|
+
return;
|
|
322
407
|
this.handlers.handleResponderMove(symbioteEvent);
|
|
323
408
|
}
|
|
324
409
|
|
|
@@ -339,14 +424,18 @@ export class Pressable implements IAngularPressableInputs {
|
|
|
339
424
|
// is listening anyway, mirroring core's noteHoverNoop but off `.observed` instead of an @Input.
|
|
340
425
|
private noteHoverNoop(): void {
|
|
341
426
|
if (this.hoverIn.observed || this.hoverOut.observed) {
|
|
342
|
-
dlog(
|
|
427
|
+
dlog(
|
|
428
|
+
'Pressable hover is a no-op on this host (no pointer-enter/leave event)',
|
|
429
|
+
);
|
|
343
430
|
}
|
|
344
431
|
}
|
|
345
432
|
|
|
346
433
|
// Wraps an @Output() as an IPressHandler only while it has a subscriber, so an unbound
|
|
347
434
|
// (longPress) still skips arming the timer, matching createPressHandlers' "undefined means
|
|
348
435
|
// nobody cares" contract.
|
|
349
|
-
private emitterHandler(
|
|
436
|
+
private emitterHandler(
|
|
437
|
+
emitter: EventEmitter<ISymbioteEvent>,
|
|
438
|
+
): IPressHandler | undefined {
|
|
350
439
|
return emitter.observed ? event => emitter.emit(event) : undefined;
|
|
351
440
|
}
|
|
352
441
|
|